diff --git a/apps/acls/__init__.py b/apps/acls/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/acls/admin.py b/apps/acls/admin.py new file mode 100644 index 000000000..8c38f3f3d --- /dev/null +++ b/apps/acls/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/acls/api/__init__.py b/apps/acls/api/__init__.py new file mode 100644 index 000000000..ff52a1ce9 --- /dev/null +++ b/apps/acls/api/__init__.py @@ -0,0 +1,3 @@ +from .login_acl import * +from .login_asset_acl import * +from .login_asset_check import * diff --git a/apps/acls/api/login_acl.py b/apps/acls/api/login_acl.py new file mode 100644 index 000000000..2a74e2d0b --- /dev/null +++ b/apps/acls/api/login_acl.py @@ -0,0 +1,19 @@ +from common.permissions import IsOrgAdmin, HasQueryParamsUserAndIsCurrentOrgMember +from common.drf.api import JMSBulkModelViewSet +from ..models import LoginACL +from .. import serializers + +__all__ = ['LoginACLViewSet', ] + + +class LoginACLViewSet(JMSBulkModelViewSet): + queryset = LoginACL.objects.all() + filterset_fields = ('name', 'user', ) + search_fields = filterset_fields + permission_classes = (IsOrgAdmin, ) + serializer_class = serializers.LoginACLSerializer + + def get_permissions(self): + if self.action in ["retrieve", "list"]: + self.permission_classes = (IsOrgAdmin, HasQueryParamsUserAndIsCurrentOrgMember) + return super().get_permissions() diff --git a/apps/acls/api/login_asset_acl.py b/apps/acls/api/login_asset_acl.py new file mode 100644 index 000000000..ab966184f --- /dev/null +++ b/apps/acls/api/login_asset_acl.py @@ -0,0 +1,15 @@ + +from orgs.mixins.api import OrgBulkModelViewSet +from common.permissions import IsOrgAdmin +from .. import models, serializers + + +__all__ = ['LoginAssetACLViewSet'] + + +class LoginAssetACLViewSet(OrgBulkModelViewSet): + model = models.LoginAssetACL + filterset_fields = ('name', ) + search_fields = filterset_fields + permission_classes = (IsOrgAdmin, ) + serializer_class = serializers.LoginAssetACLSerializer diff --git a/apps/acls/api/login_asset_check.py b/apps/acls/api/login_asset_check.py new file mode 100644 index 000000000..ed525e6cf --- /dev/null +++ b/apps/acls/api/login_asset_check.py @@ -0,0 +1,105 @@ +from django.shortcuts import get_object_or_404 +from rest_framework.response import Response +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 ..models import LoginAssetACL +from .. import serializers + + +__all__ = ['LoginAssetCheckAPI', 'LoginAssetConfirmStatusAPI'] + + +class LoginAssetCheckAPI(CreateAPIView): + permission_classes = (IsAppUser, ) + serializer_class = serializers.LoginAssetCheckSerializer + + def create(self, request, *args, **kwargs): + is_need_confirm, response_data = self.check_if_need_confirm() + return Response(data=response_data, status=200) + + def check_if_need_confirm(self): + queries = { + 'user': self.serializer.user, 'asset': self.serializer.asset, + 'system_user': self.serializer.system_user, + 'action': LoginAssetACL.ActionChoices.login_confirm + } + with tmp_to_org(self.serializer.org): + acl = LoginAssetACL.filter(**queries).valid().first() + + if not acl: + is_need_confirm = False + response_data = {} + else: + is_need_confirm = True + response_data = self._get_response_data_of_need_confirm(acl) + response_data['need_confirm'] = is_need_confirm + return is_need_confirm, response_data + + def _get_response_data_of_need_confirm(self, acl): + ticket = LoginAssetACL.create_login_asset_confirm_ticket( + user=self.serializer.user, + asset=self.serializer.asset, + system_user=self.serializer.system_user, + assignees=acl.reviewers.all(), + org_id=self.serializer.org.id + ) + confirm_status_url = reverse( + view_name='acls:login-asset-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) + data = { + '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()], + } + return data + + @lazyproperty + def serializer(self): + serializer = self.get_serializer(data=self.request.data) + serializer.is_valid(raise_exception=True) + return serializer + + +class LoginAssetConfirmStatusAPI(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/acls/apps.py b/apps/acls/apps.py new file mode 100644 index 000000000..2456a1b4f --- /dev/null +++ b/apps/acls/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AclsConfig(AppConfig): + name = 'acls' diff --git a/apps/acls/const.py b/apps/acls/const.py new file mode 100644 index 000000000..e40d679c8 --- /dev/null +++ b/apps/acls/const.py @@ -0,0 +1,9 @@ +from django.utils.translation import ugettext as _ + + +common_help_text = _('Format for comma-delimited string, with * indicating a match all. ') + +ip_group_help_text = common_help_text + _( + 'Such as: ' + '192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 ' +) diff --git a/apps/acls/migrations/0001_initial.py b/apps/acls/migrations/0001_initial.py new file mode 100644 index 000000000..83ebc4bf1 --- /dev/null +++ b/apps/acls/migrations/0001_initial.py @@ -0,0 +1,61 @@ +# Generated by Django 3.1 on 2021-03-11 09:53 + +from django.conf import settings +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='LoginACL', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('priority', models.IntegerField(default=50, help_text='1-100, the lower the value will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority')), + ('is_active', models.BooleanField(default=True, verbose_name='Active')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('ip_group', models.JSONField(default=list, verbose_name='Login IP')), + ('action', models.CharField(choices=[('reject', 'Reject'), ('allow', 'Allow')], default='reject', max_length=64, verbose_name='Action')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='login_acls', to=settings.AUTH_USER_MODEL, verbose_name='User')), + ], + options={ + 'ordering': ('priority', '-date_updated', 'name'), + }, + ), + migrations.CreateModel( + name='LoginAssetACL', + fields=[ + ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('priority', models.IntegerField(default=50, help_text='1-100, the lower the value will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority')), + ('is_active', models.BooleanField(default=True, verbose_name='Active')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('users', models.JSONField(verbose_name='User')), + ('system_users', models.JSONField(verbose_name='System User')), + ('assets', models.JSONField(verbose_name='Asset')), + ('action', models.CharField(choices=[('login_confirm', 'Login confirm')], default='login_confirm', max_length=64, verbose_name='Action')), + ('reviewers', models.ManyToManyField(blank=True, related_name='review_login_asset_acls', to=settings.AUTH_USER_MODEL, verbose_name='Reviewers')), + ], + options={ + 'ordering': ('priority', '-date_updated', 'name'), + 'unique_together': {('name', 'org_id')}, + }, + ), + ] diff --git a/apps/acls/migrations/__init__.py b/apps/acls/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/acls/models/__init__.py b/apps/acls/models/__init__.py new file mode 100644 index 000000000..45d49c378 --- /dev/null +++ b/apps/acls/models/__init__.py @@ -0,0 +1,2 @@ +from .login_acl import * +from .login_asset_acl import * diff --git a/apps/acls/models/base.py b/apps/acls/models/base.py new file mode 100644 index 000000000..73ab5c59c --- /dev/null +++ b/apps/acls/models/base.py @@ -0,0 +1,35 @@ +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from django.core.validators import MinValueValidator, MaxValueValidator +from common.mixins import CommonModelMixin + + +__all__ = ['BaseACL', 'BaseACLQuerySet'] + + +class BaseACLQuerySet(models.QuerySet): + def active(self): + return self.filter(is_active=True) + + def inactive(self): + return self.filter(is_active=False) + + def valid(self): + return self.active() + + def invalid(self): + return self.inactive() + + +class BaseACL(CommonModelMixin): + name = models.CharField(max_length=128, verbose_name=_('Name')) + 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)] + ) + is_active = models.BooleanField(default=True, verbose_name=_("Active")) + comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) + + class Meta: + abstract = True diff --git a/apps/acls/models/login_acl.py b/apps/acls/models/login_acl.py new file mode 100644 index 000000000..44d100098 --- /dev/null +++ b/apps/acls/models/login_acl.py @@ -0,0 +1,54 @@ + +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from .base import BaseACL, BaseACLQuerySet +from ..utils import contains_ip + + +class ACLManager(models.Manager): + + def valid(self): + return self.get_queryset().valid() + + +class LoginACL(BaseACL): + class ActionChoices(models.TextChoices): + reject = 'reject', _('Reject') + allow = 'allow', _('Allow') + + # 条件 + ip_group = models.JSONField(default=list, verbose_name=_('Login IP')) + # 动作 + action = models.CharField( + max_length=64, choices=ActionChoices.choices, default=ActionChoices.reject, + verbose_name=_('Action') + ) + # 关联 + user = models.ForeignKey( + 'users.User', on_delete=models.CASCADE, related_name='login_acls', verbose_name=_('User') + ) + + objects = ACLManager.from_queryset(BaseACLQuerySet)() + + class Meta: + ordering = ('priority', '-date_updated', 'name') + + @property + def action_reject(self): + return self.action == self.ActionChoices.reject + + @property + def action_allow(self): + return self.action == self.ActionChoices.allow + + @staticmethod + def allow_user_to_login(user, ip): + acl = user.login_acls.valid().first() + if not acl: + return True + is_contained = contains_ip(ip, acl.ip_group) + if acl.action_allow and is_contained: + return True + if acl.action_reject and not is_contained: + return True + return False diff --git a/apps/acls/models/login_asset_acl.py b/apps/acls/models/login_asset_acl.py new file mode 100644 index 000000000..162665c83 --- /dev/null +++ b/apps/acls/models/login_asset_acl.py @@ -0,0 +1,99 @@ +from django.db import models +from django.db.models import Q +from django.utils.translation import ugettext_lazy as _ +from orgs.mixins.models import OrgModelMixin, OrgManager +from .base import BaseACL, BaseACLQuerySet +from ..utils import contains_ip + + +class ACLManager(OrgManager): + + def valid(self): + return self.get_queryset().valid() + + +class LoginAssetACL(BaseACL, OrgModelMixin): + class ActionChoices(models.TextChoices): + login_confirm = 'login_confirm', _('Login confirm') + + # 条件 + users = models.JSONField(verbose_name=_('User')) + system_users = models.JSONField(verbose_name=_('System User')) + assets = models.JSONField(verbose_name=_('Asset')) + # 动作 + action = models.CharField( + max_length=64, choices=ActionChoices.choices, default=ActionChoices.login_confirm, + verbose_name=_('Action') + ) + # 动作: 附加字段 + # - login_confirm + reviewers = models.ManyToManyField( + 'users.User', related_name='review_login_asset_acls', blank=True, + verbose_name=_("Reviewers") + ) + + objects = ACLManager.from_queryset(BaseACLQuerySet)() + + class Meta: + unique_together = ('name', 'org_id') + ordering = ('priority', '-date_updated', 'name') + + @classmethod + def filter(cls, user, asset, system_user, action): + queryset = cls.objects.filter(action=action) + queryset = cls.filter_user(user, queryset) + queryset = cls.filter_asset(asset, queryset) + queryset = cls.filter_system_user(system_user, queryset) + return queryset + + @classmethod + def filter_user(cls, user, queryset): + queryset = queryset.filter( + Q(users__username_group__contains=user.username) | + Q(users__username_group__contains='*') + ) + return queryset + + @classmethod + def filter_asset(cls, asset, queryset): + queryset = queryset.filter( + Q(assets__hostname_group__contains=asset.hostname) | + Q(assets__hostname_group__contains='*') + ) + ids = [q.id for q in queryset if contains_ip(asset.ip, q.assets.get('ip_group', []))] + queryset = cls.objects.filter(id__in=ids) + return queryset + + @classmethod + def filter_system_user(cls, system_user, queryset): + queryset = queryset.filter( + Q(system_users__name_group__contains=system_user.name) | + Q(system_users__name_group__contains='*') + ).filter( + Q(system_users__username_group__contains=system_user.username) | + Q(system_users__username_group__contains='*') + ).filter( + Q(system_users__protocol_group__contains=system_user.protocol) | + Q(system_users__protocol_group__contains='*') + ) + return queryset + + @classmethod + def create_login_asset_confirm_ticket(cls, user, asset, system_user, assignees, org_id): + from tickets.const import TicketTypeChoices + from tickets.models import Ticket + data = { + 'title': _('Login asset confirm') + ' ({})'.format(user), + 'type': TicketTypeChoices.login_asset_confirm, + 'meta': { + 'apply_login_user': str(user), + 'apply_login_asset': str(asset), + 'apply_login_system_user': str(system_user), + }, + 'org_id': org_id, + } + ticket = Ticket.objects.create(**data) + ticket.assignees.set(assignees) + ticket.open(applicant=user) + return ticket + diff --git a/apps/acls/serializers/__init__.py b/apps/acls/serializers/__init__.py new file mode 100644 index 000000000..ff52a1ce9 --- /dev/null +++ b/apps/acls/serializers/__init__.py @@ -0,0 +1,3 @@ +from .login_acl import * +from .login_asset_acl import * +from .login_asset_check import * diff --git a/apps/acls/serializers/login_acl.py b/apps/acls/serializers/login_acl.py new file mode 100644 index 000000000..23d31b13e --- /dev/null +++ b/apps/acls/serializers/login_acl.py @@ -0,0 +1,49 @@ +from django.utils.translation import ugettext as _ +from rest_framework import serializers +from common.drf.serializers import BulkModelSerializer +from orgs.utils import current_org +from ..models import LoginACL +from ..utils import is_ip_address, is_ip_network, is_ip_segment +from .. import const + + +__all__ = ['LoginACLSerializer', ] + + +def ip_group_child_validator(ip_group_child): + is_valid = ip_group_child == '*' \ + or is_ip_address(ip_group_child) \ + or is_ip_network(ip_group_child) \ + or is_ip_segment(ip_group_child) + if not is_valid: + error = _('IP address invalid: `{}`').format(ip_group_child) + raise serializers.ValidationError(error) + + +class LoginACLSerializer(BulkModelSerializer): + ip_group = serializers.ListField( + default=['*'], label=_('IP'), help_text=const.ip_group_help_text, + child=serializers.CharField(max_length=1024, validators=[ip_group_child_validator]) + ) + user_display = serializers.ReadOnlyField(source='user.name', label=_('User')) + action_display = serializers.ReadOnlyField(source='get_action_display', label=_('Action')) + + 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' + ] + extra_kwargs = { + 'priority': {'default': 50}, + 'is_active': {'default': True}, + } + + @staticmethod + def validate_user(user): + if user not in current_org.get_members(): + error = _('The user `{}` is not in the current organization: `{}`').format( + user, current_org + ) + raise serializers.ValidationError(error) + return user diff --git a/apps/acls/serializers/login_asset_acl.py b/apps/acls/serializers/login_asset_acl.py new file mode 100644 index 000000000..863d636dc --- /dev/null +++ b/apps/acls/serializers/login_asset_acl.py @@ -0,0 +1,87 @@ +from rest_framework import serializers +from django.utils.translation import ugettext as _ +from orgs.mixins.serializers import BulkOrgResourceModelSerializer +from assets.models import SystemUser +from acls import models +from orgs.models import Organization +from .. import const + + +__all__ = ['LoginAssetACLSerializer'] + + +class LoginAssetACLUsersSerializer(serializers.Serializer): + username_group = serializers.ListField( + default=['*'], child=serializers.CharField(max_length=128), label=_('Username'), + help_text=const.common_help_text + ) + + +class LoginAssetACLAssestsSerializer(serializers.Serializer): + ip_group = serializers.ListField( + default=['*'], child=serializers.CharField(max_length=1024), label=_('IP'), + help_text=const.ip_group_help_text + _('(Domain name support)') + ) + hostname_group = serializers.ListField( + default=['*'], child=serializers.CharField(max_length=128), label=_('Hostname'), + help_text=const.common_help_text + ) + + +class LoginAssetACLSystemUsersSerializer(serializers.Serializer): + name_group = serializers.ListField( + default=['*'], child=serializers.CharField(max_length=128), label=_('Name'), + help_text=const.common_help_text + ) + username_group = serializers.ListField( + default=['*'], child=serializers.CharField(max_length=128), label=_('Username'), + help_text=const.common_help_text + ) + protocol_group = serializers.ListField( + default=['*'], child=serializers.CharField(max_length=16), label=_('Protocol'), + help_text=const.common_help_text + _('Protocol options: {}').format( + ', '.join(SystemUser.ASSET_CATEGORY_PROTOCOLS) + ) + ) + + @staticmethod + def validate_protocol_group(protocol_group): + unsupported_protocols = set(protocol_group) - set(SystemUser.ASSET_CATEGORY_PROTOCOLS + ['*']) + if unsupported_protocols: + error = _('Unsupported protocols: {}').format(unsupported_protocols) + raise serializers.ValidationError(error) + return protocol_group + + +class LoginAssetACLSerializer(BulkOrgResourceModelSerializer): + users = LoginAssetACLUsersSerializer() + assets = LoginAssetACLAssestsSerializer() + system_users = LoginAssetACLSystemUsersSerializer() + reviewers_amount = serializers.IntegerField(read_only=True, source='reviewers.count') + action_display = serializers.ReadOnlyField(source='get_action_display', label=_('Action')) + + 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' + ] + extra_kwargs = { + "reviewers": {'allow_null': False, 'required': True}, + 'priority': {'default': 50}, + 'is_active': {'default': True}, + } + + def validate_reviewers(self, reviewers): + org_id = self.fields['org_id'].default() + org = Organization.get_instance(org_id) + if not org: + error = _('The organization `{}` does not exist'.format(org_id)) + raise serializers.ValidationError(error) + users = org.get_members() + valid_reviewers = list(set(reviewers) & set(users)) + if not valid_reviewers: + error = _('None of the reviewers belong to Organization `{}`'.format(org.name)) + raise serializers.ValidationError(error) + return valid_reviewers diff --git a/apps/acls/serializers/login_asset_check.py b/apps/acls/serializers/login_asset_check.py new file mode 100644 index 000000000..ec7a7c35c --- /dev/null +++ b/apps/acls/serializers/login_asset_check.py @@ -0,0 +1,71 @@ +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers +from orgs.utils import tmp_to_root_org +from common.utils import get_object_or_none, lazyproperty +from users.models import User +from assets.models import Asset, SystemUser + + +__all__ = ['LoginAssetCheckSerializer'] + + +class LoginAssetCheckSerializer(serializers.Serializer): + user_id = serializers.UUIDField(required=True, allow_null=False) + asset_id = serializers.UUIDField(required=True, allow_null=False) + system_user_id = serializers.UUIDField(required=True, allow_null=False) + system_user_username = serializers.CharField(max_length=128, default='') + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.user = None + self.asset = None + self._system_user = None + self._system_user_username = None + + def validate_user_id(self, user_id): + self.user = self.validate_object_exist(User, user_id) + return user_id + + def validate_asset_id(self, asset_id): + self.asset = self.validate_object_exist(Asset, asset_id) + return asset_id + + def validate_system_user_id(self, system_user_id): + self._system_user = self.validate_object_exist(SystemUser, system_user_id) + return system_user_id + + def validate_system_user_username(self, system_user_username): + system_user_id = self.initial_data.get('system_user_id') + system_user = self.validate_object_exist(SystemUser, system_user_id) + if self._system_user.login_mode == SystemUser.LOGIN_MANUAL \ + and not system_user.username \ + and not system_user.username_same_with_user \ + and not system_user_username: + error = 'Missing parameter: system_user_username' + raise serializers.ValidationError(error) + self._system_user_username = system_user_username + return system_user_username + + @staticmethod + def validate_object_exist(model, field_id): + with tmp_to_root_org(): + obj = get_object_or_none(model, pk=field_id) + if not obj: + error = '{} Model object does not exist'.format(model.__name__) + raise serializers.ValidationError(error) + return obj + + @lazyproperty + def system_user(self): + if self._system_user.username_same_with_user: + username = self.user.username + elif self._system_user.login_mode == SystemUser.LOGIN_MANUAL: + username = self._system_user_username + else: + username = self._system_user.username + self._system_user.username = username + return self._system_user + + @lazyproperty + def org(self): + return self.asset.org diff --git a/apps/acls/tests.py b/apps/acls/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/apps/acls/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/acls/urls/__init__.py b/apps/acls/urls/__init__.py new file mode 100644 index 000000000..4d8423837 --- /dev/null +++ b/apps/acls/urls/__init__.py @@ -0,0 +1 @@ +from .api_urls import * diff --git a/apps/acls/urls/api_urls.py b/apps/acls/urls/api_urls.py new file mode 100644 index 000000000..24fbc11b0 --- /dev/null +++ b/apps/acls/urls/api_urls.py @@ -0,0 +1,18 @@ +from django.urls import path +from rest_framework_bulk.routes import BulkRouter +from .. import api + + +app_name = 'acls' + + +router = BulkRouter() +router.register(r'login-acls', api.LoginACLViewSet, 'login-acl') +router.register(r'login-asset-acls', api.LoginAssetACLViewSet, 'login-asset-acl') + +urlpatterns = [ + path('login-asset/check/', api.LoginAssetCheckAPI.as_view(), name='login-asset-check'), + path('login-asset-confirm//status/', api.LoginAssetConfirmStatusAPI.as_view(), name='login-asset-confirm-status') +] + +urlpatterns += router.urls diff --git a/apps/acls/utils.py b/apps/acls/utils.py new file mode 100644 index 000000000..0000a5da7 --- /dev/null +++ b/apps/acls/utils.py @@ -0,0 +1,68 @@ +from ipaddress import ip_network, ip_address + + +def is_ip_address(address): + """ 192.168.10.1 """ + try: + ip_address(address) + except ValueError: + return False + else: + return True + + +def is_ip_network(ip): + """ 192.168.1.0/24 """ + try: + ip_network(ip) + except ValueError: + return False + else: + return True + + +def is_ip_segment(ip): + """ 10.1.1.1-10.1.1.20 """ + if '-' not in ip: + return False + ip_address1, ip_address2 = ip.split('-') + return is_ip_address(ip_address1) and is_ip_address(ip_address2) + + +def in_ip_segment(ip, ip_segment): + ip1, ip2 = ip_segment.split('-') + ip1 = int(ip_address(ip1)) + ip2 = int(ip_address(ip2)) + ip = int(ip_address(ip)) + return min(ip1, ip2) <= ip <= max(ip1, ip2) + + +def contains_ip(ip, ip_group): + """ + ip_group: + [192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64.] + + """ + + if '*' in ip_group: + return True + + for _ip in ip_group: + if is_ip_address(_ip): + # 192.168.10.1 + if ip == _ip: + return True + elif is_ip_network(_ip) and is_ip_address(ip): + # 192.168.1.0/24 + if ip_address(ip) in ip_network(_ip): + return True + elif is_ip_segment(_ip) and is_ip_address(ip): + # 10.1.1.1-10.1.1.20 + if in_ip_segment(ip, _ip): + return True + else: + # is domain name + if ip == _ip: + return True + + return False diff --git a/apps/assets/migrations/0067_auto_20210311_1113.py b/apps/assets/migrations/0067_auto_20210311_1113.py new file mode 100644 index 000000000..0b087d47c --- /dev/null +++ b/apps/assets/migrations/0067_auto_20210311_1113.py @@ -0,0 +1,48 @@ +# Generated by Django 3.1 on 2021-03-11 03:13 + +import django.core.validators +from django.db import migrations, models + + +def migrate_cmd_filter_priority(apps, schema_editor): + cmd_filter_rule_model = apps.get_model('assets', 'CommandFilterRule') + cmd_filter_rules = cmd_filter_rule_model.objects.all() + for cmd_filter_rule in cmd_filter_rules: + cmd_filter_rule.priority = 100 - cmd_filter_rule.priority + 1 + + cmd_filter_rule_model.objects.bulk_update(cmd_filter_rules, fields=['priority']) + + +def migrate_system_user_priority(apps, schema_editor): + system_user_model = apps.get_model('assets', 'SystemUser') + system_users = system_user_model.objects.all() + for system_user in system_users: + system_user.priority = 100 - system_user.priority + 1 + + system_user_model.objects.bulk_update(system_users, fields=['priority']) + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0066_auto_20210208_1802'), + ] + + operations = [ + migrations.RunPython(migrate_cmd_filter_priority), + migrations.RunPython(migrate_system_user_priority), + migrations.AlterModelOptions( + name='commandfilterrule', + options={'ordering': ('priority', 'action'), 'verbose_name': 'Command filter rule'}, + ), + migrations.AlterField( + model_name='commandfilterrule', + name='priority', + field=models.IntegerField(default=50, help_text='1-100, the lower the value will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority'), + ), + migrations.AlterField( + model_name='systemuser', + name='priority', + field=models.IntegerField(default=20, help_text='1-100, the lower the value will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority'), + ), + ] diff --git a/apps/assets/models/cmd_filter.py b/apps/assets/models/cmd_filter.py index 204e98f94..c1242fd7e 100644 --- a/apps/assets/models/cmd_filter.py +++ b/apps/assets/models/cmd_filter.py @@ -50,7 +50,7 @@ class CommandFilterRule(OrgModelMixin): id = models.UUIDField(default=uuid.uuid4, primary_key=True) filter = models.ForeignKey('CommandFilter', on_delete=models.CASCADE, verbose_name=_("Filter"), related_name='rules') type = models.CharField(max_length=16, default=TYPE_COMMAND, choices=TYPE_CHOICES, verbose_name=_("Type")) - priority = models.IntegerField(default=50, verbose_name=_("Priority"), help_text=_("1-100, the higher will be match first"), + 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")) @@ -60,7 +60,7 @@ class CommandFilterRule(OrgModelMixin): created_by = models.CharField(max_length=128, blank=True, default='', verbose_name=_('Created by')) class Meta: - ordering = ('-priority', 'action') + ordering = ('priority', 'action') verbose_name = _("Command filter rule") @lazyproperty diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index dd576b077..0145af2f9 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -116,7 +116,7 @@ class SystemUser(BaseUser): assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets")) users = models.ManyToManyField('users.User', blank=True, verbose_name=_("Users")) groups = models.ManyToManyField('users.UserGroup', blank=True, verbose_name=_("User groups")) - priority = models.IntegerField(default=20, verbose_name=_("Priority"), validators=[MinValueValidator(1), MaxValueValidator(100)]) + priority = models.IntegerField(default=20, verbose_name=_("Priority"), help_text=_("1-100, the lower the value will be match first"), validators=[MinValueValidator(1), MaxValueValidator(100)]) protocol = models.CharField(max_length=16, choices=PROTOCOL_CHOICES, default='ssh', verbose_name=_('Protocol')) auto_push = models.BooleanField(default=True, verbose_name=_('Auto push')) sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo')) diff --git a/apps/authentication/errors.py b/apps/authentication/errors.py index 679c0b748..68c85ff87 100644 --- a/apps/authentication/errors.py +++ b/apps/authentication/errors.py @@ -19,6 +19,7 @@ reason_password_expired = 'password_expired' reason_user_invalid = 'user_invalid' reason_user_inactive = 'user_inactive' reason_backend_not_match = 'backend_not_match' +reason_acl_not_allow = 'acl_not_allow' reason_choices = { reason_password_failed: _('Username/password check failed'), @@ -29,7 +30,8 @@ reason_choices = { reason_password_expired: _("Password expired"), reason_user_invalid: _('Disabled or expired'), reason_user_inactive: _("This account is inactive."), - reason_backend_not_match: _("Auth backend not match") + reason_backend_not_match: _("Auth backend not match"), + reason_acl_not_allow: _("ACL is not allowed") } old_reason_choices = { '0': '-', diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index 9702e6046..f89938e64 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -128,6 +128,13 @@ class AuthMixin: if auth_backend not in auth_backends_allowed: self.raise_credential_error(error=errors.reason_backend_not_match) + def _check_login_acl(self, user, ip): + # ACL 限制用户登录 + from acls.models import LoginACL + is_allowed = LoginACL.allow_user_to_login(user, ip) + if not is_allowed: + raise self.raise_credential_error(error=errors.reason_acl_not_allow) + def check_user_auth(self, decrypt_passwd=False): self.check_is_block() request = self.request @@ -135,8 +142,9 @@ class AuthMixin: self._check_only_allow_exists_user_auth(username) user = self._check_auth_user_is_valid(username, password, public_key) + # 校验login-acl规则 + self._check_login_acl(user, ip) # 限制只能从认证来源登录 - auth_backend = getattr(user, 'backend', 'django.contrib.auth.backends.ModelBackend') self._check_auth_source_is_valid(user, auth_backend) self._check_password_require_reset_or_not(user) diff --git a/apps/common/permissions.py b/apps/common/permissions.py index 7098cdb84..32ea8ca94 100644 --- a/apps/common/permissions.py +++ b/apps/common/permissions.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # import time - from rest_framework import permissions from django.contrib.auth.mixins import UserPassesTestMixin from django.conf import settings @@ -193,3 +192,12 @@ class IsObjectOwner(IsValidUser): def has_object_permission(self, request, view, obj): return (super().has_object_permission(request, view, obj) and request.user == getattr(obj, 'user', None)) + + +class HasQueryParamsUserAndIsCurrentOrgMember(permissions.BasePermission): + def has_permission(self, request, view): + query_user_id = request.query_params.get('user') + if not query_user_id: + return False + query_user = current_org.get_members().filter(id=query_user_id).first() + return bool(query_user) diff --git a/apps/common/utils/common.py b/apps/common/utils/common.py index e24793b55..0e910cf16 100644 --- a/apps/common/utils/common.py +++ b/apps/common/utils/common.py @@ -142,7 +142,7 @@ def is_uuid(seq): elif isinstance(seq, str) and UUID_PATTERN.match(seq): return True elif isinstance(seq, (list, tuple)): - all([is_uuid(x) for x in seq]) + return all([is_uuid(x) for x in seq]) return False diff --git a/apps/jumpserver/settings/base.py b/apps/jumpserver/settings/base.py index 1766fb41f..3e5a4ff6a 100644 --- a/apps/jumpserver/settings/base.py +++ b/apps/jumpserver/settings/base.py @@ -48,6 +48,7 @@ INSTALLED_APPS = [ 'authentication.apps.AuthenticationConfig', # authentication 'applications.apps.ApplicationsConfig', 'tickets.apps.TicketsConfig', + 'acls.apps.AclsConfig', 'jms_oidc_rp', 'rest_framework', 'rest_framework_swagger', diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index 5b8202854..044d09310 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -22,6 +22,7 @@ api_v1 = [ path('common/', include('common.urls.api_urls', namespace='api-common')), path('applications/', include('applications.urls.api_urls', namespace='api-applications')), path('tickets/', include('tickets.urls.api_urls', namespace='api-tickets')), + path('acls/', include('acls.urls.api_urls', namespace='api-acls')), path('prometheus/metrics/', api.PrometheusMetricsApi.as_view()) ] diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 850c92503..3a6dfef4d 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 a377a5384..20e7cc254 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-03-10 11:16+0800\n" +"POT-Creation-Date: 2021-03-11 17:54+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -17,21 +17,19 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: 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 "数据库" +#: acls/const.py:4 +msgid "Format for comma-delimited string, with * indicating a match all. " +msgstr "格式为逗号分隔的字符串, * 表示匹配所有. " -#: applications/const.py:10 -msgid "Remote app" -msgstr "远程应用" - -#: applications/const.py:29 -msgid "Custom" -msgstr "自定义" +#: acls/const.py:7 +msgid "" +"Such as: 192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::" +"e13, 2001:db8:1a:1110::/64 " +msgstr "" +"例如: 192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, " +"2001:db8:1a:1110::/64" +#: acls/models/base.py:25 acls/serializers/login_asset_acl.py:33 #: applications/models/application.py:11 assets/models/asset.py:142 #: assets/models/base.py:250 assets/models/cluster.py:18 #: assets/models/cmd_filter.py:21 assets/models/domain.py:21 @@ -57,6 +55,216 @@ msgstr "自定义" msgid "Name" msgstr "名称" +#: acls/models/base.py:27 assets/models/cmd_filter.py:53 +#: assets/models/user.py:119 +msgid "Priority" +msgstr "优先级" + +#: acls/models/base.py:28 assets/models/cmd_filter.py:53 +#: assets/models/user.py:119 +msgid "1-100, the lower the value will be match first" +msgstr "优先级可选范围为 1-100 (数值越小越优先)" + +#: acls/models/base.py:31 authentication/models.py:20 +#: authentication/templates/authentication/_access_key_modal.html:32 +#: perms/models/base.py:52 users/templates/users/_select_user_modal.html:18 +#: users/templates/users/user_detail.html:132 +#: users/templates/users/user_profile.html:63 +msgid "Active" +msgstr "激活中" + +# msgid "Date created" +# 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/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:81 +#: terminal/models/terminal.py:153 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 +#: 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 +msgid "Reject" +msgstr "拒绝" + +#: acls/models/login_acl.py:17 assets/models/cmd_filter.py:47 +msgid "Allow" +msgstr "允许" + +#: acls/models/login_acl.py:20 +msgid "Login IP" +msgstr "登录IP" + +#: acls/models/login_acl.py:24 acls/models/login_asset_acl.py:26 +#: acls/serializers/login_acl.py:29 acls/serializers/login_asset_acl.py:61 +#: assets/models/cmd_filter.py:56 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:28 assets/models/label.py:15 +#: audits/models.py:36 audits/models.py:56 audits/models.py:69 +#: audits/serializers.py:81 authentication/models.py:44 +#: authentication/models.py:95 orgs/models.py:18 orgs/models.py:403 +#: 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:677 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:71 +#: tickets/const.py:9 users/templates/users/user_detail.html:250 +msgid "Login confirm" +msgstr "登录复核" + +#: acls/models/login_asset_acl.py:21 +msgid "System User" +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:29 +#: assets/serializers/asset_user.py:47 assets/serializers/asset_user.py:84 +#: assets/serializers/system_user.py:191 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 +#: 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 +msgid "Reviewers" +msgstr "审批人" + +#: acls/models/login_asset_acl.py:86 tickets/const.py:12 +msgid "Login asset confirm" +msgstr "登录资产复核" + +#: acls/serializers/login_acl.py:19 +msgid "IP address invalid: `{}`" +msgstr "IP 地址无效: `{}`" + +#: acls/serializers/login_acl.py:25 acls/serializers/login_asset_acl.py:22 +#: 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:108 +#: users/templates/users/_granted_assets.html:26 +#: users/templates/users/user_asset_permission.html:156 +msgid "IP" +msgstr "IP" + +#: acls/serializers/login_acl.py:41 +msgid "The user `{}` is not in the current organization: `{}`" +msgstr "用户 `{}` 不在当前组织: `{}`" + +#: acls/serializers/login_asset_acl.py:15 +#: acls/serializers/login_asset_acl.py:37 +#: applications/serializers/attrs/application_type/chrome.py:20 +#: applications/serializers/attrs/application_type/custom.py:21 +#: applications/serializers/attrs/application_type/mysql_workbench.py:30 +#: applications/serializers/attrs/application_type/vmware_client.py:26 +#: assets/models/base.py:251 assets/models/gathered_user.py:15 +#: audits/models.py:99 authentication/forms.py:15 authentication/forms.py:17 +#: ops/models/adhoc.py:148 users/forms/profile.py:31 users/models/user.py:528 +#: 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:44 +msgid "Username" +msgstr "用户名" + +#: acls/serializers/login_asset_acl.py:23 +msgid "(Domain name support)" +msgstr "(支持域名)" + +#: acls/serializers/login_asset_acl.py:26 assets/models/asset.py:184 +#: assets/serializers/asset_user.py:45 assets/serializers/gathered_user.py:20 +#: settings/serializers/settings.py:107 +#: users/templates/users/_granted_assets.html:25 +#: users/templates/users/user_asset_permission.html:157 +msgid "Hostname" +msgstr "主机名" + +#: acls/serializers/login_asset_acl.py:41 assets/models/asset.py:187 +#: assets/models/domain.py:54 assets/models/user.py:120 +#: terminal/serializers/session.py:29 terminal/serializers/storage.py:69 +msgid "Protocol" +msgstr "协议" + +#: acls/serializers/login_asset_acl.py:42 +msgid "Protocol options: {}" +msgstr "协议选项: {}" + +#: acls/serializers/login_asset_acl.py:51 +msgid "Unsupported protocols: {}" +msgstr "不支持的协议: {}" + +#: acls/serializers/login_asset_acl.py:78 +#: tickets/serializers/ticket/ticket.py:109 +msgid "The organization `{}` does not exist" +msgstr "组织 `{}` 不存在" + +#: acls/serializers/login_asset_acl.py:83 +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 "数据库" + +#: applications/const.py:10 +msgid "Remote app" +msgstr "远程应用" + +#: applications/const.py:29 +msgid "Custom" +msgstr "自定义" + #: applications/models/application.py:13 #: applications/serializers/application.py:47 assets/models/label.py:21 #: perms/models/application_permission.py:20 @@ -87,28 +295,6 @@ msgstr "网域" msgid "Attrs" msgstr "" -# msgid "Date created" -# msgstr "创建日期" -#: 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/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:81 terminal/models/terminal.py:153 -#: 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 -#: 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 "备注" - #: applications/serializers/attrs/application_category/cloud.py:9 #: assets/models/cluster.py:40 msgid "Cluster" @@ -131,22 +317,6 @@ msgstr "主机" msgid "Port" msgstr "端口" -#: 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:29 -#: assets/serializers/asset_user.py:47 assets/serializers/asset_user.py:84 -#: assets/serializers/system_user.py:191 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 -#: 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 "资产" - #: applications/serializers/attrs/application_category/remote_app.py:36 #: applications/serializers/attrs/application_type/chrome.py:14 #: applications/serializers/attrs/application_type/mysql_workbench.py:14 @@ -159,23 +329,6 @@ msgstr "应用路径" msgid "Target URL" msgstr "目标URL" -#: applications/serializers/attrs/application_type/chrome.py:20 -#: applications/serializers/attrs/application_type/custom.py:21 -#: applications/serializers/attrs/application_type/mysql_workbench.py:30 -#: applications/serializers/attrs/application_type/vmware_client.py:26 -#: assets/models/base.py:251 assets/models/gathered_user.py:15 -#: audits/models.py:99 authentication/forms.py:15 authentication/forms.py:17 -#: ops/models/adhoc.py:148 users/forms/profile.py:31 users/models/user.py:528 -#: 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:44 -msgid "Username" -msgstr "用户名" - #: applications/serializers/attrs/application_type/chrome.py:23 #: applications/serializers/attrs/application_type/custom.py:25 #: applications/serializers/attrs/application_type/mysql_workbench.py:34 @@ -205,14 +358,6 @@ msgstr "运行参数" msgid "Target url" msgstr "目标URL" -#: 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:108 -#: users/templates/users/_granted_assets.html:26 -#: users/templates/users/user_asset_permission.html:156 -msgid "IP" -msgstr "IP" - #: assets/api/admin_user.py:50 msgid "Deleted failed, There are related assets" msgstr "删除失败,存在关联资产" @@ -262,19 +407,6 @@ msgstr "内部的" msgid "Platform" msgstr "系统平台" -#: assets/models/asset.py:184 assets/serializers/asset_user.py:45 -#: assets/serializers/gathered_user.py:20 settings/serializers/settings.py:107 -#: users/templates/users/_granted_assets.html:25 -#: users/templates/users/user_asset_permission.html:157 -msgid "Hostname" -msgstr "主机名" - -#: assets/models/asset.py:187 assets/models/domain.py:54 -#: assets/models/user.py:120 terminal/serializers/session.py:29 -#: terminal/serializers/storage.py:69 -msgid "Protocol" -msgstr "协议" - #: assets/models/asset.py:189 assets/serializers/asset.py:68 #: perms/serializers/asset/user_permission.py:41 msgid "Protocols" @@ -484,22 +616,10 @@ msgstr "命令" msgid "Deny" msgstr "拒绝" -#: assets/models/cmd_filter.py:47 -msgid "Allow" -msgstr "允许" - #: assets/models/cmd_filter.py:51 msgid "Filter" msgstr "过滤器" -#: assets/models/cmd_filter.py:53 assets/models/user.py:119 -msgid "Priority" -msgstr "优先级" - -#: assets/models/cmd_filter.py:53 -msgid "1-100, the higher will be match first" -msgstr "优先级可选范围为1-100,1最低优先级,100最高优先级" - #: assets/models/cmd_filter.py:55 xpack/plugins/license/models.py:29 msgid "Content" msgstr "内容" @@ -508,18 +628,6 @@ msgstr "内容" msgid "One line one command" msgstr "每行一个命令" -#: assets/models/cmd_filter.py:56 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 "动作" - #: assets/models/cmd_filter.py:64 msgid "Command filter rule" msgstr "命令过滤规则" @@ -556,27 +664,7 @@ msgstr "资产组" msgid "Default asset group" msgstr "默认资产组" -#: assets/models/label.py:15 audits/models.py:36 audits/models.py:56 -#: audits/models.py:69 audits/serializers.py:81 authentication/models.py:44 -#: authentication/models.py:95 orgs/models.py:18 orgs/models.py:403 -#: 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:677 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 "用户" - -#: assets/models/label.py:19 assets/models/node.py:553 settings/models.py:30 +#: assets/models/label.py:19 assets/models/node.py:575 settings/models.py:30 msgid "Value" msgstr "值" @@ -584,23 +672,23 @@ msgstr "值" msgid "New node" msgstr "新节点" -#: assets/models/node.py:445 users/templates/users/_granted_assets.html:130 +#: assets/models/node.py:467 users/templates/users/_granted_assets.html:130 msgid "empty" msgstr "空" -#: assets/models/node.py:552 perms/models/asset_permission.py:156 +#: assets/models/node.py:574 perms/models/asset_permission.py:156 msgid "Key" msgstr "键" -#: assets/models/node.py:554 +#: assets/models/node.py:576 msgid "Full value" msgstr "全称" -#: assets/models/node.py:557 perms/models/asset_permission.py:157 +#: assets/models/node.py:579 perms/models/asset_permission.py:157 msgid "Parent key" msgstr "ssh私钥" -#: assets/models/node.py:566 assets/serializers/system_user.py:190 +#: assets/models/node.py:588 assets/serializers/system_user.py:190 #: users/templates/users/user_asset_permission.html:41 #: users/templates/users/user_asset_permission.html:73 #: users/templates/users/user_asset_permission.html:158 @@ -1201,47 +1289,51 @@ 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 "Auth backend not match" -msgstr "" +msgstr "没有匹配到认证后端" -#: authentication/errors.py:42 +#: authentication/errors.py:34 +msgid "ACL is not allowed" +msgstr "ACL 不被允许" + +#: authentication/errors.py:44 msgid "No session found, check your cookie" msgstr "会话已变更,刷新页面" -#: authentication/errors.py:44 +#: authentication/errors.py:46 #, python-brace-format msgid "" "The username or password you entered is incorrect, please enter it again. " @@ -1251,46 +1343,46 @@ msgstr "" "您输入的用户名或密码不正确,请重新输入。 您还可以尝试 {times_try} 次(账号将" "被临时 锁定 {block_time} 分钟)" -#: authentication/errors.py:50 +#: authentication/errors.py:52 msgid "" "The account has been locked (please contact admin to unlock it or try again " "after {} minutes)" msgstr "账号已被锁定(请联系管理员解锁 或 {}分钟后重试)" -#: authentication/errors.py:53 users/views/profile/otp.py:110 +#: authentication/errors.py:55 users/views/profile/otp.py:110 #: users/views/profile/otp.py:149 users/views/profile/otp.py:169 msgid "MFA code invalid, or ntp sync server time" msgstr "MFA验证码不正确,或者服务器端时间不对" -#: authentication/errors.py:55 +#: authentication/errors.py:57 msgid "MFA required" msgstr "需要多因子认证" -#: authentication/errors.py:56 +#: authentication/errors.py:58 msgid "MFA not set, please set it first" msgstr "多因子认证没有设置,请先完成设置" -#: authentication/errors.py:57 +#: authentication/errors.py:59 msgid "Login confirm required" msgstr "需要登录复核" -#: authentication/errors.py:58 +#: authentication/errors.py:60 msgid "Wait login confirm ticket for accept" msgstr "等待登录复核处理" -#: authentication/errors.py:59 +#: authentication/errors.py:61 msgid "Login confirm ticket was {}" msgstr "登录复核 {}" -#: authentication/errors.py:215 +#: authentication/errors.py:217 msgid "SSO auth closed" msgstr "SSO 认证关闭了" -#: authentication/errors.py:220 authentication/views/login.py:232 +#: authentication/errors.py:222 authentication/views/login.py:232 msgid "Your password is too simple, please change it for security" msgstr "你的密码过于简单,为了安全,请修改" -#: authentication/errors.py:229 authentication/views/login.py:247 +#: authentication/errors.py:231 authentication/views/login.py:247 msgid "Your password has expired, please reset before logging in" msgstr "您的密码已过期,先修改再登录" @@ -1303,27 +1395,10 @@ msgstr "{} 天内自动登录" msgid "MFA code" msgstr "多因子认证验证码" -#: 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 "激活中" - #: authentication/models.py:40 msgid "Private Token" msgstr "SSH密钥" -#: authentication/models.py:45 users/templates/users/user_detail.html:258 -msgid "Reviewers" -msgstr "审批人" - -#: authentication/models.py:71 tickets/const.py:9 -#: users/templates/users/user_detail.html:250 -msgid "Login confirm" -msgstr "登录复核" - #: authentication/models.py:94 msgid "Expired" msgstr "过期时间" @@ -1375,7 +1450,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:18 +#: templates/_modal.html:22 tickets/const.py:19 msgid "Close" msgstr "关闭" @@ -3161,19 +3236,15 @@ msgstr "申请资产" msgid "Apply for application" msgstr "申请应用" -#: tickets/const.py:15 tickets/const.py:22 +#: tickets/const.py:16 tickets/const.py:23 msgid "Open" msgstr "打开" -#: tickets/const.py:16 +#: tickets/const.py:17 msgid "Approve" msgstr "同意" -#: tickets/const.py:17 -msgid "Reject" -msgstr "拒绝" - -#: tickets/const.py:23 +#: tickets/const.py:24 msgid "Closed" msgstr "关闭" @@ -3288,17 +3359,29 @@ msgstr "工单申请信息" msgid "Ticket approved info" msgstr "工单批准信息" +#: tickets/handler/login_asset_confirm.py:16 +msgid "Applied login user" +msgstr "申请登录的用户" + +#: tickets/handler/login_asset_confirm.py:17 +msgid "Applied login asset" +msgstr "申请登录的资产" + +#: tickets/handler/login_asset_confirm.py:18 +msgid "Applied login system user" +msgstr "申请登录的系统用户" + #: tickets/handler/login_confirm.py:16 msgid "Applied login IP" -msgstr "申请的登录IP" +msgstr "申请登录的IP" #: tickets/handler/login_confirm.py:17 msgid "Applied login city" -msgstr "申请的登录城市" +msgstr "申请登录的城市" #: tickets/handler/login_confirm.py:18 msgid "Applied login datetime" -msgstr "申请的登录日期" +msgstr "申请登录的日期" #: tickets/models/comment.py:19 msgid "User display name" @@ -3421,6 +3504,18 @@ msgstr "在组织 `{}` 下没有发现 `资产`" msgid "Created by ticket ({}-{})" msgstr "通过工单创建 ({}-{})" +#: tickets/serializers/ticket/meta/ticket_type/login_asset_confirm.py:13 +msgid "Login user" +msgstr "登录用户" + +#: tickets/serializers/ticket/meta/ticket_type/login_asset_confirm.py:14 +msgid "Login asset" +msgstr "登录资产" + +#: tickets/serializers/ticket/meta/ticket_type/login_asset_confirm.py:16 +msgid "Login system user" +msgstr "登录系统用户" + #: tickets/serializers/ticket/meta/ticket_type/login_confirm.py:20 msgid "Login datetime" msgstr "登录日期" @@ -3439,10 +3534,6 @@ msgid "" "request url (`{}`)" msgstr "提交数据中的类型 (`{}`) 与请求URL地址中的类型 (`{}`) 不一致" -#: tickets/serializers/ticket/ticket.py:109 -msgid "The organization `{}` does not exist" -msgstr "组织 `{}` 不存在" - #: tickets/serializers/ticket/ticket.py:120 msgid "None of the assignees belong to Organization `{}` admins" msgstr "所有受理人都不属于组织 `{}` 下的管理员" @@ -4764,95 +4855,95 @@ msgstr "实例" #: xpack/plugins/cloud/providers/aws_international.py:17 msgid "China (Beijing)" -msgstr "" +msgstr "中国(北京)" #: xpack/plugins/cloud/providers/aws_international.py:18 msgid "China (Ningxia)" -msgstr "" +msgstr "中国(宁夏)" #: xpack/plugins/cloud/providers/aws_international.py:21 msgid "US East (Ohio)" -msgstr "" +msgstr "美国东部(俄亥俄州)" #: xpack/plugins/cloud/providers/aws_international.py:22 msgid "US East (N. Virginia)" -msgstr "" +msgstr "美国东部(弗吉尼亚北部)" #: xpack/plugins/cloud/providers/aws_international.py:23 msgid "US West (N. California)" -msgstr "" +msgstr "美国西部(加利福尼亚北部)" #: xpack/plugins/cloud/providers/aws_international.py:24 msgid "US West (Oregon)" -msgstr "" +msgstr "美国西部(俄勒冈)" #: xpack/plugins/cloud/providers/aws_international.py:25 msgid "Africa (Cape Town)" -msgstr "" +msgstr "非洲(开普敦)" #: xpack/plugins/cloud/providers/aws_international.py:26 msgid "Asia Pacific (Hong Kong)" -msgstr "亚太-香港" +msgstr "亚太地区(香港)" #: xpack/plugins/cloud/providers/aws_international.py:27 msgid "Asia Pacific (Mumbai)" -msgstr "" +msgstr "亚太地区(孟买)" #: xpack/plugins/cloud/providers/aws_international.py:28 msgid "Asia Pacific (Osaka-Local)" -msgstr "" +msgstr "亚太区域(大阪当地)" #: xpack/plugins/cloud/providers/aws_international.py:29 msgid "Asia Pacific (Seoul)" -msgstr "" +msgstr "亚太区域(首尔)" #: xpack/plugins/cloud/providers/aws_international.py:30 msgid "Asia Pacific (Singapore)" -msgstr "亚太-新加坡" +msgstr "亚太区域(新加坡)" #: xpack/plugins/cloud/providers/aws_international.py:31 msgid "Asia Pacific (Sydney)" -msgstr "" +msgstr "亚太区域(悉尼)" #: xpack/plugins/cloud/providers/aws_international.py:32 msgid "Asia Pacific (Tokyo)" -msgstr "" +msgstr "亚太区域(东京)" #: xpack/plugins/cloud/providers/aws_international.py:33 msgid "Canada (Central)" -msgstr "" +msgstr "加拿大(中部)" #: xpack/plugins/cloud/providers/aws_international.py:34 msgid "Europe (Frankfurt)" -msgstr "" +msgstr "欧洲(法兰克福)" #: xpack/plugins/cloud/providers/aws_international.py:35 msgid "Europe (Ireland)" -msgstr "" +msgstr "欧洲(爱尔兰)" #: xpack/plugins/cloud/providers/aws_international.py:36 msgid "Europe (London)" -msgstr "" +msgstr "欧洲(伦敦)" #: xpack/plugins/cloud/providers/aws_international.py:37 msgid "Europe (Milan)" -msgstr "" +msgstr "欧洲(米兰)" #: xpack/plugins/cloud/providers/aws_international.py:38 msgid "Europe (Paris)" -msgstr "" +msgstr "欧洲(巴黎)" #: xpack/plugins/cloud/providers/aws_international.py:39 msgid "Europe (Stockholm)" -msgstr "" +msgstr "欧洲(斯德哥尔摩)" #: xpack/plugins/cloud/providers/aws_international.py:40 msgid "Middle East (Bahrain)" -msgstr "" +msgstr "中东(巴林)" #: xpack/plugins/cloud/providers/aws_international.py:41 msgid "South America (São Paulo)" -msgstr "" +msgstr "南美洲(圣保罗)" #: xpack/plugins/cloud/providers/huaweicloud.py:35 msgid "AF-Johannesburg" @@ -4912,19 +5003,19 @@ msgstr "" #: xpack/plugins/cloud/serializers.py:28 msgid "Client ID" -msgstr "Client ID" +msgstr "" #: xpack/plugins/cloud/serializers.py:31 msgid "Client Secret" -msgstr "Client Secret" +msgstr "" #: xpack/plugins/cloud/serializers.py:34 msgid "Tenant ID" -msgstr "租户ID" +msgstr "" #: xpack/plugins/cloud/serializers.py:37 msgid "Subscription ID" -msgstr "订阅ID" +msgstr "" #: xpack/plugins/cloud/serializers.py:115 msgid "History count" @@ -5026,31 +5117,3 @@ msgstr "旗舰版" #: xpack/plugins/license/models.py:77 msgid "Community edition" msgstr "社区版" - -#~ msgid "Captcha invalid" -#~ msgstr "验证码错误" - -#~ msgid "" -#~ "Not support openssh format key, using ssh-keygen -t rsa -m pem to generate" -#~ msgstr "暂不支持OPENSSH格式的密钥,使用 ssh-keygen -t rsa -m pem生成" - -#~ msgid "Select users" -#~ msgstr "选择用户" - -#~ msgid "Paste user id_rsa.pub here." -#~ msgstr "复制用户公钥到这里" - -#~ msgid "Available" -#~ msgstr "有效" - -#~ msgid "Unavailable" -#~ msgstr "无效" - -#~ msgid "Instances" -#~ msgstr "实例" - -#~ msgid "LA-Santiago" -#~ msgstr "拉美-圣地亚哥" - -#~ msgid "Please wait while your data is being initialized" -#~ msgstr "数据正在初始化,请稍等" diff --git a/apps/orgs/utils.py b/apps/orgs/utils.py index 2eaa9e9e6..29b1f03e9 100644 --- a/apps/orgs/utils.py +++ b/apps/orgs/utils.py @@ -67,8 +67,6 @@ def get_current_org_id(): def get_current_org_id_for_serializer(): org_id = get_current_org_id() - if org_id == Organization.DEFAULT_ID: - org_id = '' return org_id diff --git a/apps/terminal/models/storage.py b/apps/terminal/models/storage.py index 3f574985c..9497ea802 100644 --- a/apps/terminal/models/storage.py +++ b/apps/terminal/models/storage.py @@ -56,7 +56,7 @@ class CommandStorage(CommonModelMixin): return storage.ping() def is_use(self): - return Terminal.objects.filter(command_storage=self.name).exists() + return Terminal.objects.filter(command_storage=self.name, is_deleted=False).exists() def get_command_queryset(self): if self.type_server: diff --git a/apps/tickets/api/ticket.py b/apps/tickets/api/ticket.py index 91a585298..071bed755 100644 --- a/apps/tickets/api/ticket.py +++ b/apps/tickets/api/ticket.py @@ -54,14 +54,14 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet): def open(self, request, *args, **kwargs): return super().create(request, *args, **kwargs) - @action(detail=True, methods=[PUT], permission_classes=[IsOrgAdmin, IsAssignee, NotClosed]) + @action(detail=True, methods=[PUT], permission_classes=[IsAssignee, NotClosed]) def approve(self, request, *args, **kwargs): response = super().update(request, *args, **kwargs) instance = self.get_object() instance.approve(processor=self.request.user) return response - @action(detail=True, methods=[PUT], permission_classes=[IsOrgAdmin, IsAssignee, NotClosed]) + @action(detail=True, methods=[PUT], permission_classes=[IsAssignee, NotClosed]) def reject(self, request, *args, **kwargs): instance = self.get_object() serializer = self.get_serializer(instance) diff --git a/apps/tickets/const.py b/apps/tickets/const.py index 742d4e7d3..591ead607 100644 --- a/apps/tickets/const.py +++ b/apps/tickets/const.py @@ -9,6 +9,7 @@ class TicketTypeChoices(TextChoices): login_confirm = 'login_confirm', _("Login confirm") apply_asset = 'apply_asset', _('Apply for asset') apply_application = 'apply_application', _('Apply for application') + login_asset_confirm = 'login_asset_confirm', _('Login asset confirm') class TicketActionChoices(TextChoices): diff --git a/apps/tickets/handler/login_asset_confirm.py b/apps/tickets/handler/login_asset_confirm.py new file mode 100644 index 000000000..b967af29d --- /dev/null +++ b/apps/tickets/handler/login_asset_confirm.py @@ -0,0 +1,20 @@ +from django.utils.translation import ugettext as _ +from .base import BaseHandler + + +class Handler(BaseHandler): + + # body + def _construct_meta_body_of_open(self): + apply_login_user = self.ticket.meta.get('apply_login_user') + apply_login_asset = self.ticket.meta.get('apply_login_asset') + apply_login_system_user = self.ticket.meta.get('apply_login_system_user') + applied_body = '''{}: {}, + {}: {}, + {}: {} + '''.format( + _("Applied login user"), apply_login_user, + _("Applied login asset"), apply_login_asset, + _("Applied login system user"), apply_login_system_user, + ) + return applied_body diff --git a/apps/tickets/migrations/0008_auto_20210311_1113.py b/apps/tickets/migrations/0008_auto_20210311_1113.py new file mode 100644 index 000000000..be959ba0e --- /dev/null +++ b/apps/tickets/migrations/0008_auto_20210311_1113.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2021-03-11 03:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tickets', '0007_auto_20201224_1821'), + ] + + 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')], default='general', max_length=64, verbose_name='Type'), + ), + ] diff --git a/apps/tickets/serializers/ticket/meta/meta.py b/apps/tickets/serializers/ticket/meta/meta.py index b423805f4..ee8a402dd 100644 --- a/apps/tickets/serializers/ticket/meta/meta.py +++ b/apps/tickets/serializers/ticket/meta/meta.py @@ -1,5 +1,5 @@ from tickets import const -from .ticket_type import apply_asset, apply_application, login_confirm +from .ticket_type import apply_asset, apply_application, login_confirm, login_asset_confirm __all__ = [ 'type_serializer_classes_mapping', @@ -30,5 +30,10 @@ type_serializer_classes_mapping = { 'default': login_confirm.LoginConfirmSerializer, action_open: login_confirm.ApplySerializer, action_approve: login_confirm.LoginConfirmSerializer(read_only=True), + }, + const.TicketTypeChoices.login_asset_confirm.value: { + 'default': login_asset_confirm.LoginAssetConfirmSerializer, + action_open: login_asset_confirm.ApplySerializer, + action_approve: login_asset_confirm.LoginAssetConfirmSerializer(read_only=True), } } diff --git a/apps/tickets/serializers/ticket/meta/ticket_type/login_asset_confirm.py b/apps/tickets/serializers/ticket/meta/ticket_type/login_asset_confirm.py new file mode 100644 index 000000000..2570e4792 --- /dev/null +++ b/apps/tickets/serializers/ticket/meta/ticket_type/login_asset_confirm.py @@ -0,0 +1,21 @@ + +from rest_framework import serializers +from django.utils.translation import ugettext_lazy as _ + + +__all__ = [ + 'ApplySerializer', 'LoginAssetConfirmSerializer', +] + + +class ApplySerializer(serializers.Serializer): + # 申请信息 + apply_login_user = serializers.CharField(required=True, label=_('Login user')) + apply_login_asset = serializers.CharField(required=True, label=_('Login asset')) + apply_login_system_user = serializers.CharField( + required=True, max_length=64, label=_('Login system user') + ) + + +class LoginAssetConfirmSerializer(ApplySerializer): + pass