diff --git a/apps/acls/api/login_acl.py b/apps/acls/api/login_acl.py index 2a74e2d0b..c42e61a2c 100644 --- a/apps/acls/api/login_acl.py +++ b/apps/acls/api/login_acl.py @@ -2,15 +2,16 @@ from common.permissions import IsOrgAdmin, HasQueryParamsUserAndIsCurrentOrgMemb from common.drf.api import JMSBulkModelViewSet from ..models import LoginACL from .. import serializers +from ..filters import LoginAclFilter __all__ = ['LoginACLViewSet', ] class LoginACLViewSet(JMSBulkModelViewSet): queryset = LoginACL.objects.all() - filterset_fields = ('name', 'user', ) - search_fields = filterset_fields - permission_classes = (IsOrgAdmin, ) + filterset_class = LoginAclFilter + search_fields = ('name',) + permission_classes = (IsOrgAdmin,) serializer_class = serializers.LoginACLSerializer def get_permissions(self): diff --git a/apps/acls/api/login_asset_acl.py b/apps/acls/api/login_asset_acl.py index ab966184f..fa03851b9 100644 --- a/apps/acls/api/login_asset_acl.py +++ b/apps/acls/api/login_asset_acl.py @@ -1,4 +1,3 @@ - from orgs.mixins.api import OrgBulkModelViewSet from common.permissions import IsOrgAdmin from .. import models, serializers diff --git a/apps/acls/api/login_asset_check.py b/apps/acls/api/login_asset_check.py index 2adee4604..91c0b5b49 100644 --- a/apps/acls/api/login_asset_check.py +++ b/apps/acls/api/login_asset_check.py @@ -1,10 +1,9 @@ -from django.shortcuts import get_object_or_404 from rest_framework.response import Response -from rest_framework.generics import CreateAPIView, RetrieveDestroyAPIView +from rest_framework.generics import CreateAPIView from common.permissions import IsAppUser from common.utils import reverse, lazyproperty -from orgs.utils import tmp_to_org, tmp_to_root_org +from orgs.utils import tmp_to_org from tickets.api import GenericTicketStatusRetrieveCloseAPI from ..models import LoginAssetACL from .. import serializers diff --git a/apps/acls/filters.py b/apps/acls/filters.py new file mode 100644 index 000000000..23cd0bc61 --- /dev/null +++ b/apps/acls/filters.py @@ -0,0 +1,15 @@ +from django_filters import rest_framework as filters +from common.drf.filters import BaseFilterSet + +from acls.models import LoginACL + + +class LoginAclFilter(BaseFilterSet): + user = filters.UUIDFilter(field_name='user_id') + user_display = filters.CharFilter(field_name='user__name') + + class Meta: + model = LoginACL + fields = ( + 'name', 'user', 'user_display' + ) diff --git a/apps/acls/migrations/0002_auto_20210926_1047.py b/apps/acls/migrations/0002_auto_20210926_1047.py new file mode 100644 index 000000000..05e0e7cf1 --- /dev/null +++ b/apps/acls/migrations/0002_auto_20210926_1047.py @@ -0,0 +1,87 @@ +# Generated by Django 3.1.12 on 2021-09-26 02:47 +import django +from django.conf import settings +from django.db import migrations, models, transaction +from acls.models import LoginACL + +LOGIN_CONFIRM_ZH = '登录复核' +LOGIN_CONFIRM_EN = 'Login confirm' + + +def has_zh(name: str) -> bool: + for i in name: + if u'\u4e00' <= i <= u'\u9fff': + return True + return False + + +def migrate_login_confirm(apps, schema_editor): + login_acl_model = apps.get_model("acls", "LoginACL") + login_confirm_model = apps.get_model("authentication", "LoginConfirmSetting") + + with transaction.atomic(): + for instance in login_confirm_model.objects.filter(is_active=True): + user = instance.user + reviewers = instance.reviewers.all() + login_confirm = LOGIN_CONFIRM_ZH if has_zh(user.name) else LOGIN_CONFIRM_EN + date_created = instance.date_created.strftime('%Y-%m-%d %H:%M:%S') + if reviewers.count() == 0: + continue + data = { + 'user': user, + 'name': f'{user.name}-{login_confirm} ({date_created})', + 'created_by': instance.created_by, + 'action': LoginACL.ActionChoices.confirm + } + instance = login_acl_model.objects.create(**data) + instance.reviewers.set(reviewers) + + +def migrate_ip_group(apps, schema_editor): + login_acl_model = apps.get_model("acls", "LoginACL") + default_time_periods = [{'id': i, 'value': '00:00~00:00'} for i in range(7)] + updates = list() + with transaction.atomic(): + for instance in login_acl_model.objects.exclude(action=LoginACL.ActionChoices.confirm): + instance.rules = {'ip_group': instance.ip_group, 'time_period': default_time_periods} + updates.append(instance) + login_acl_model.objects.bulk_update(updates, ['rules', ]) + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('acls', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='loginacl', + name='action', + field=models.CharField(choices=[('reject', 'Reject'), ('allow', 'Allow'), ('confirm', 'Login confirm')], + default='reject', max_length=64, verbose_name='Action'), + ), + migrations.AddField( + model_name='loginacl', + name='reviewers', + field=models.ManyToManyField(blank=True, related_name='login_confirm_acls', + to=settings.AUTH_USER_MODEL, verbose_name='Reviewers'), + ), + migrations.AlterField( + model_name='loginacl', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, + related_name='login_acls', to=settings.AUTH_USER_MODEL, verbose_name='User'), + ), + migrations.RunPython(migrate_login_confirm), + migrations.AddField( + model_name='loginacl', + name='rules', + field=models.JSONField(default=dict, verbose_name='Rule'), + ), + migrations.RunPython(migrate_ip_group), + migrations.RemoveField( + model_name='loginacl', + name='ip_group', + ), + ] diff --git a/apps/acls/models/login_acl.py b/apps/acls/models/login_acl.py index 03fa1ef7a..f57c81a3c 100644 --- a/apps/acls/models/login_acl.py +++ b/apps/acls/models/login_acl.py @@ -1,8 +1,11 @@ - from django.db import models +from django.db.models import Q +from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from .base import BaseACL, BaseACLQuerySet +from common.utils import get_request_ip, get_ip_city from common.utils.ip import contains_ip +from common.utils.time_period import contains_time_period class ACLManager(models.Manager): @@ -15,19 +18,24 @@ class LoginACL(BaseACL): class ActionChoices(models.TextChoices): reject = 'reject', _('Reject') allow = 'allow', _('Allow') + confirm = 'confirm', _('Login confirm') - # 条件 - ip_group = models.JSONField(default=list, verbose_name=_('Login IP')) + # 用户 + user = models.ForeignKey( + 'users.User', on_delete=models.CASCADE, verbose_name=_('User'), + related_name='login_acls' + ) + # 规则 + rules = models.JSONField(default=dict, verbose_name=_('Rule')) # 动作 action = models.CharField( - max_length=64, choices=ActionChoices.choices, default=ActionChoices.reject, - verbose_name=_('Action') + max_length=64, verbose_name=_('Action'), + choices=ActionChoices.choices, default=ActionChoices.reject ) - # 关联 - user = models.ForeignKey( - 'users.User', on_delete=models.CASCADE, related_name='login_acls', verbose_name=_('User') + reviewers = models.ManyToManyField( + 'users.User', verbose_name=_("Reviewers"), + related_name="login_confirm_acls", blank=True ) - objects = ACLManager.from_queryset(BaseACLQuerySet)() class Meta: @@ -44,14 +52,75 @@ class LoginACL(BaseACL): def action_allow(self): return self.action == self.ActionChoices.allow + @classmethod + def filter_acl(cls, user): + return user.login_acls.all().valid().distinct() + + @staticmethod + def allow_user_confirm_if_need(user, ip): + acl = LoginACL.filter_acl(user).filter(action=LoginACL.ActionChoices.confirm).first() + acl = acl if acl and acl.reviewers.exists() else None + if not acl: + return False, acl + ip_group = acl.rules.get('ip_group') + time_periods = acl.rules.get('time_period') + is_contain_ip = contains_ip(ip, ip_group) + is_contain_time_period = contains_time_period(time_periods) + return is_contain_ip and is_contain_time_period, acl + @staticmethod def allow_user_to_login(user, ip): - acl = user.login_acls.valid().first() + acl = LoginACL.filter_acl(user).exclude(action=LoginACL.ActionChoices.confirm).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 + return True, '' + ip_group = acl.rules.get('ip_group') + time_periods = acl.rules.get('time_period') + is_contain_ip = contains_ip(ip, ip_group) + is_contain_time_period = contains_time_period(time_periods) + + reject_type = '' + if is_contain_ip and is_contain_time_period: + # 满足条件 + allow = acl.action_allow + if not allow: + reject_type = 'ip' if is_contain_ip else 'time' + else: + # 不满足条件 + # 如果acl本身允许,那就拒绝;如果本身拒绝,那就允许 + allow = not acl.action_allow + if not allow: + reject_type = 'ip' if not is_contain_ip else 'time' + + return allow, reject_type + + + + @staticmethod + def construct_confirm_ticket_meta(request=None): + login_ip = get_request_ip(request) if request else '' + login_ip = login_ip or '0.0.0.0' + login_city = get_ip_city(login_ip) + login_datetime = timezone.now().strftime('%Y-%m-%d %H:%M:%S') + ticket_meta = { + 'apply_login_ip': login_ip, + 'apply_login_city': login_city, + 'apply_login_datetime': login_datetime, + } + return ticket_meta + + def create_confirm_ticket(self, request=None): + from tickets import const + from tickets.models import Ticket + from orgs.models import Organization + ticket_title = _('Login confirm') + ' {}'.format(self.user) + ticket_meta = self.construct_confirm_ticket_meta(request) + data = { + 'title': ticket_title, + 'type': const.TicketType.login_confirm.value, + 'meta': ticket_meta, + 'org_id': Organization.ROOT_ID, + } + ticket = Ticket.objects.create(**data) + ticket.create_process_map_and_node(self.reviewers.all()) + ticket.open(self.user) + return ticket diff --git a/apps/acls/serializers/login_acl.py b/apps/acls/serializers/login_acl.py index c1b21f114..78236ea64 100644 --- a/apps/acls/serializers/login_acl.py +++ b/apps/acls/serializers/login_acl.py @@ -1,59 +1,42 @@ 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 common.drf.serializers import MethodSerializer from ..models import LoginACL -from common.utils.ip import is_ip_address, is_ip_network, is_ip_segment - +from .rules import RuleSerializer __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) +common_help_text = _('Format for comma-delimited string, with * indicating a match all. ') class LoginACLSerializer(BulkModelSerializer): - ip_group_help_text = _( - 'Format for comma-delimited string, with * indicating a match all. ' - '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 ' - ) - - ip_group = serializers.ListField( - default=['*'], label=_('IP'), help_text=ip_group_help_text, - child=serializers.CharField(max_length=1024, validators=[ip_group_child_validator]) - ) - user_display = serializers.ReadOnlyField(source='user.name', label=_('User')) + user_display = serializers.ReadOnlyField(source='user.name', label=_('Username')) + reviewers_display = serializers.SerializerMethodField(label=_('Reviewers')) action_display = serializers.ReadOnlyField(source='get_action_display', label=_('Action')) + reviewers_amount = serializers.IntegerField(read_only=True, source='reviewers.count') + rules = MethodSerializer() class Meta: model = LoginACL fields_mini = ['id', 'name'] fields_small = fields_mini + [ - 'priority', 'ip_group', 'action', 'action_display', - 'is_active', - 'date_created', 'date_updated', - 'comment', 'created_by', + 'priority', 'rules', 'action', 'action_display', + 'is_active', 'user', 'user_display', + 'date_created', 'date_updated', 'reviewers_amount', + 'comment', 'created_by' ] - fields_fk = ['user', 'user_display',] - fields = fields_small + fields_fk + fields_fk = ['user', 'user_display'] + fields_m2m = ['reviewers', 'reviewers_display'] + fields = fields_small + fields_fk + fields_m2m extra_kwargs = { 'priority': {'default': 50}, 'is_active': {'default': True}, + "reviewers": {'allow_null': False, 'required': 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 + def get_rules_serializer(self): + return RuleSerializer() + + def get_reviewers_display(self, obj): + return ','.join([str(user) for user in obj.reviewers.all()]) diff --git a/apps/acls/serializers/rules/__init__.py b/apps/acls/serializers/rules/__init__.py new file mode 100644 index 000000000..37ec2764e --- /dev/null +++ b/apps/acls/serializers/rules/__init__.py @@ -0,0 +1 @@ +from .rules import * \ No newline at end of file diff --git a/apps/acls/serializers/rules/rules.py b/apps/acls/serializers/rules/rules.py new file mode 100644 index 000000000..bfac5f65f --- /dev/null +++ b/apps/acls/serializers/rules/rules.py @@ -0,0 +1,34 @@ +# coding: utf-8 +# +from rest_framework import serializers +from django.utils.translation import ugettext_lazy as _ + +from common.utils import get_logger +from common.utils.ip import is_ip_address, is_ip_network, is_ip_segment + +logger = get_logger(__file__) + +__all__ = ['RuleSerializer'] + + +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 RuleSerializer(serializers.Serializer): + ip_group_help_text = _( + 'Format for comma-delimited string, with * indicating a match all. ' + '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 ' + ) + + ip_group = serializers.ListField( + default=['*'], label=_('IP'), help_text=ip_group_help_text, + child=serializers.CharField(max_length=1024, validators=[ip_group_child_validator])) + time_period = serializers.ListField(default=[], label=_('Time Period')) diff --git a/apps/authentication/api/login_confirm.py b/apps/authentication/api/login_confirm.py index 93b33c4f9..e3a1bc118 100644 --- a/apps/authentication/api/login_confirm.py +++ b/apps/authentication/api/login_confirm.py @@ -8,29 +8,12 @@ from django.shortcuts import get_object_or_404 from common.utils import get_logger from common.permissions import IsOrgAdmin -from ..models import LoginConfirmSetting -from ..serializers import LoginConfirmSettingSerializer from .. import errors, mixins -__all__ = ['LoginConfirmSettingUpdateApi', 'TicketStatusApi'] +__all__ = ['TicketStatusApi'] logger = get_logger(__name__) -class LoginConfirmSettingUpdateApi(UpdateAPIView): - permission_classes = (IsOrgAdmin,) - serializer_class = LoginConfirmSettingSerializer - - def get_object(self): - from users.models import User - user_id = self.kwargs.get('user_id') - user = get_object_or_404(User, pk=user_id) - defaults = {'user': user} - s, created = LoginConfirmSetting.objects.get_or_create( - defaults, user=user, - ) - return s - - class TicketStatusApi(mixins.AuthMixin, APIView): permission_classes = (AllowAny,) diff --git a/apps/authentication/errors.py b/apps/authentication/errors.py index ca190fe2b..79adb4904 100644 --- a/apps/authentication/errors.py +++ b/apps/authentication/errors.py @@ -261,6 +261,13 @@ class LoginIPNotAllowed(ACLError): super().__init__(_("IP is not allowed"), **kwargs) +class TimePeriodNotAllowed(ACLError): + def __init__(self, username, request, **kwargs): + self.username = username + self.request = request + super().__init__(_("Time Period is not allowed"), **kwargs) + + class LoginConfirmBaseError(NeedMoreInfoError): def __init__(self, ticket_id, **kwargs): self.ticket_id = ticket_id diff --git a/apps/authentication/migrations/0005_delete_loginconfirmsetting.py b/apps/authentication/migrations/0005_delete_loginconfirmsetting.py new file mode 100644 index 000000000..cbf01a735 --- /dev/null +++ b/apps/authentication/migrations/0005_delete_loginconfirmsetting.py @@ -0,0 +1,16 @@ +# Generated by Django 3.1.12 on 2021-09-26 11:13 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0004_ssotoken'), + ] + + operations = [ + migrations.DeleteModel( + name='LoginConfirmSetting', + ), + ] diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index 843bf8ede..5a3fc4bbc 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -17,9 +17,9 @@ from django.contrib.auth import ( from django.shortcuts import reverse, redirect from common.utils import get_object_or_none, get_request_ip, get_logger, bulk_get, FlashMessageUtil +from acls.models import LoginACL from users.models import User, MFAType from users.utils import LoginBlockUtil, MFABlockUtils -from users.exceptions import MFANotEnabled from . import errors from .utils import rsa_decrypt, gen_key_pair from .signals import post_auth_success, post_auth_failed @@ -247,10 +247,12 @@ class AuthMixin(PasswordEncryptionViewMixin): def _check_login_acl(self, user, ip): # ACL 限制用户登录 - from acls.models import LoginACL - is_allowed = LoginACL.allow_user_to_login(user, ip) + is_allowed, limit_type = LoginACL.allow_user_to_login(user, ip) if not is_allowed: - raise errors.LoginIPNotAllowed(username=user.username, request=self.request) + if limit_type == 'ip': + raise errors.LoginIPNotAllowed(username=user.username, request=self.request) + elif limit_type == 'time': + raise errors.TimePeriodNotAllowed(username=user.username, request=self.request) def set_login_failed_mark(self): ip = self.get_request_ip() @@ -463,10 +465,9 @@ class AuthMixin(PasswordEncryptionViewMixin): ) def check_user_login_confirm_if_need(self, user): - if not settings.LOGIN_CONFIRM_ENABLE: - return - confirm_setting = user.get_login_confirm_setting() - if self.request.session.get('auth_confirm') or not confirm_setting: + ip = self.get_request_ip() + is_allowed, confirm_setting = LoginACL.allow_user_confirm_if_need(user, ip) + if self.request.session.get('auth_confirm') or not is_allowed: return self.get_ticket_or_create(confirm_setting) self.check_user_login_confirm() diff --git a/apps/authentication/models.py b/apps/authentication/models.py index 6e7b59e54..9322fd7f9 100644 --- a/apps/authentication/models.py +++ b/apps/authentication/models.py @@ -1,13 +1,10 @@ import uuid -from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from rest_framework.authtoken.models import Token from django.conf import settings from common.db import models -from common.mixins.models import CommonModelMixin -from common.utils import get_object_or_none, get_request_ip, get_ip_city class AccessKey(models.Model): @@ -40,56 +37,6 @@ class PrivateToken(Token): verbose_name = _('Private Token') -class LoginConfirmSetting(CommonModelMixin): - user = models.OneToOneField('users.User', on_delete=models.CASCADE, verbose_name=_("User"), related_name="login_confirm_setting") - reviewers = models.ManyToManyField('users.User', verbose_name=_("Reviewers"), related_name="review_login_confirm_settings", blank=True) - is_active = models.BooleanField(default=True, verbose_name=_("Is active")) - - class Meta: - verbose_name = _('Login Confirm') - - @classmethod - def get_user_confirm_setting(cls, user): - return get_object_or_none(cls, user=user) - - @staticmethod - def construct_confirm_ticket_meta(request=None): - if request: - login_ip = get_request_ip(request) - else: - login_ip = '' - login_ip = login_ip or '0.0.0.0' - login_city = get_ip_city(login_ip) - login_datetime = timezone.now().strftime('%Y-%m-%d %H:%M:%S') - ticket_meta = { - 'apply_login_ip': login_ip, - 'apply_login_city': login_city, - 'apply_login_datetime': login_datetime, - } - return ticket_meta - - def create_confirm_ticket(self, request=None): - from tickets import const - from tickets.models import Ticket - from orgs.models import Organization - ticket_title = _('Login confirm') + ' {}'.format(self.user) - ticket_meta = self.construct_confirm_ticket_meta(request) - data = { - 'title': ticket_title, - 'type': const.TicketType.login_confirm.value, - 'meta': ticket_meta, - 'org_id': Organization.ROOT_ID, - } - ticket = Ticket.objects.create(**data) - ticket.create_process_map_and_node(self.reviewers.all()) - ticket.open(self.user) - return ticket - - def __str__(self): - reviewers = [u.username for u in self.reviewers.all()] - return _('{} need confirm by {}').format(self.user.username, reviewers) - - class SSOToken(models.JMSBaseModel): """ 类似腾讯企业邮的 [单点登录](https://exmail.qq.com/qy_mng_logic/doc#10036) diff --git a/apps/authentication/serializers.py b/apps/authentication/serializers.py index b571dea01..548819089 100644 --- a/apps/authentication/serializers.py +++ b/apps/authentication/serializers.py @@ -10,12 +10,11 @@ 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 - +from .models import AccessKey __all__ = [ 'AccessKeySerializer', 'OtpVerifySerializer', 'BearerTokenSerializer', - 'MFAChallengeSerializer', 'LoginConfirmSettingSerializer', 'SSOTokenSerializer', + 'MFAChallengeSerializer', 'SSOTokenSerializer', 'ConnectionTokenSerializer', 'ConnectionTokenSecretSerializer', 'PasswordVerifySerializer', 'MFASelectTypeSerializer', ] @@ -92,13 +91,6 @@ class MFAChallengeSerializer(serializers.Serializer): pass -class LoginConfirmSettingSerializer(serializers.ModelSerializer): - class Meta: - model = LoginConfirmSetting - fields = ['id', 'user', 'reviewers', 'date_created', 'date_updated'] - read_only_fields = ['date_created', 'date_updated'] - - class SSOTokenSerializer(serializers.Serializer): username = serializers.CharField(write_only=True) login_url = serializers.CharField(read_only=True) @@ -201,4 +193,3 @@ class ConnectionTokenSecretSerializer(serializers.Serializer): gateway = ConnectionTokenGatewaySerializer(read_only=True) actions = ActionsField() expired_at = serializers.IntegerField() - diff --git a/apps/authentication/urls/api_urls.py b/apps/authentication/urls/api_urls.py index 2fb66496b..9f9690c74 100644 --- a/apps/authentication/urls/api_urls.py +++ b/apps/authentication/urls/api_urls.py @@ -31,7 +31,6 @@ urlpatterns = [ path('sms/verify-code/send/', api.SendSMSVerifyCodeApi.as_view(), name='sms-verify-code-send'), path('password/verify/', api.UserPasswordVerifyApi.as_view(), name='user-password-verify'), path('login-confirm-ticket/status/', api.TicketStatusApi.as_view(), name='login-confirm-ticket-status'), - path('login-confirm-settings//', api.LoginConfirmSettingUpdateApi.as_view(), name='login-confirm-setting-update') ] urlpatterns += router.urls diff --git a/apps/common/permissions.py b/apps/common/permissions.py index 750a41475..bd9a1e030 100644 --- a/apps/common/permissions.py +++ b/apps/common/permissions.py @@ -14,7 +14,7 @@ class IsValidUser(permissions.IsAuthenticated, permissions.BasePermission): def has_permission(self, request, view): return super(IsValidUser, self).has_permission(request, view) \ - and request.user.is_valid + and request.user.is_valid class IsAppUser(IsValidUser): @@ -22,7 +22,7 @@ class IsAppUser(IsValidUser): def has_permission(self, request, view): return super(IsAppUser, self).has_permission(request, view) \ - and request.user.is_app + and request.user.is_app class IsSuperUser(IsValidUser): @@ -36,7 +36,7 @@ class IsSuperUserOrAppUser(IsSuperUser): if request.user.is_anonymous: return False return super(IsSuperUserOrAppUser, self).has_permission(request, view) \ - or request.user.is_app + or request.user.is_app class IsSuperAuditor(IsValidUser): @@ -60,7 +60,7 @@ class IsOrgAdmin(IsValidUser): if not current_org: return False return super(IsOrgAdmin, self).has_permission(request, view) \ - and current_org.can_admin_by(request.user) + and current_org.can_admin_by(request.user) class IsOrgAdminOrAppUser(IsValidUser): @@ -72,7 +72,7 @@ class IsOrgAdminOrAppUser(IsValidUser): if request.user.is_anonymous: return False return super(IsOrgAdminOrAppUser, self).has_permission(request, view) \ - and (current_org.can_admin_by(request.user) or request.user.is_app) + and (current_org.can_admin_by(request.user) or request.user.is_app) class IsOrgAdminOrAppUserOrUserReadonly(IsOrgAdminOrAppUser): diff --git a/apps/common/utils/time_period.py b/apps/common/utils/time_period.py new file mode 100644 index 000000000..8a009a7f9 --- /dev/null +++ b/apps/common/utils/time_period.py @@ -0,0 +1,18 @@ +from common.utils.timezone import now + + +def contains_time_period(time_periods): + """ + time_periods: [{"id": 1, "value": "00:00~07:30、10:00~13:00"}, {"id": 2, "value": "00:00~00:00"}] + """ + if not time_periods: + return False + + current_time = now().strftime('%H:%M') + today_time_period = next(filter(lambda x: str(x['id']) == now().strftime("%w"), time_periods)) + for time in today_time_period['value'].split('、'): + start, end = time.split('~') + end = "24:00" if end == "00:00" else end + if start <= current_time <= end: + return True + return False diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 2744727a0..9cc3284a2 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -306,7 +306,6 @@ class Config(dict): 'SECURITY_MFA_VERIFY_TTL': 3600, 'SECURITY_SESSION_SHARE': True, 'OLD_PASSWORD_HISTORY_LIMIT_COUNT': 5, - 'LOGIN_CONFIRM_ENABLE': False, # 准备废弃,放到 acl 中 'CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED': True, 'USER_LOGIN_SINGLE_MACHINE_ENABLED': False, 'ONLY_ALLOW_EXIST_USER_AUTH': False, diff --git a/apps/jumpserver/context_processor.py b/apps/jumpserver/context_processor.py index 4245abd48..93c20c6f3 100644 --- a/apps/jumpserver/context_processor.py +++ b/apps/jumpserver/context_processor.py @@ -24,7 +24,6 @@ def jumpserver_processor(request): 'SECURITY_MFA_VERIFY_TTL': settings.SECURITY_MFA_VERIFY_TTL, 'FORCE_SCRIPT_NAME': settings.FORCE_SCRIPT_NAME, 'SECURITY_VIEW_AUTH_NEED_MFA': settings.SECURITY_VIEW_AUTH_NEED_MFA, - 'LOGIN_CONFIRM_ENABLE': settings.LOGIN_CONFIRM_ENABLE, } return context diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index 06eb5e7b6..a43307ab1 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -126,7 +126,6 @@ FEISHU_APP_SECRET = CONFIG.FEISHU_APP_SECRET # Other setting TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION -LOGIN_CONFIRM_ENABLE = CONFIG.LOGIN_CONFIRM_ENABLE OTP_IN_RADIUS = CONFIG.OTP_IN_RADIUS diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo deleted file mode 100644 index b64789e68..000000000 --- a/apps/locale/zh/LC_MESSAGES/django.mo +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:76a9ccd646b5f8a18196e52a277e7af8319fdb155193b310ed6c1769e45a1ffd -size 97641 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 45e169fc9..f154038ef 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -1615,6 +1615,9 @@ msgstr "登录复核 {}" msgid "IP is not allowed" msgstr "来源 IP 不被允许登录" +msgid "Time Period is not allowed" +msgstr "该 时间段 不被允许登录" + #: authentication/errors.py:294 msgid "SSO auth closed" msgstr "SSO 认证关闭了" diff --git a/apps/settings/api/public.py b/apps/settings/api/public.py index 404d44b23..a94ff7e49 100644 --- a/apps/settings/api/public.py +++ b/apps/settings/api/public.py @@ -50,7 +50,6 @@ class PublicSettingApi(generics.RetrieveAPIView): "WINDOWS_SKIP_ALL_MANUAL_PASSWORD": settings.WINDOWS_SKIP_ALL_MANUAL_PASSWORD, "SECURITY_MAX_IDLE_TIME": settings.SECURITY_MAX_IDLE_TIME, "XPACK_ENABLED": settings.XPACK_ENABLED, - "LOGIN_CONFIRM_ENABLE": settings.LOGIN_CONFIRM_ENABLE, "SECURITY_VIEW_AUTH_NEED_MFA": settings.SECURITY_VIEW_AUTH_NEED_MFA, "SECURITY_MFA_VERIFY_TTL": settings.SECURITY_MFA_VERIFY_TTL, "OLD_PASSWORD_HISTORY_LIMIT_COUNT": settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT, diff --git a/apps/settings/serializers/security.py b/apps/settings/serializers/security.py index d04a252a2..1225de0a4 100644 --- a/apps/settings/serializers/security.py +++ b/apps/settings/serializers/security.py @@ -140,7 +140,3 @@ class SecuritySettingSerializer(SecurityPasswordRuleSerializer, SecurityAuthSeri required=True, label=_('Session share'), help_text=_("Enabled, Allows user active session to be shared with other users") ) - LOGIN_CONFIRM_ENABLE = serializers.BooleanField( - required=False, label=_('Login Confirm'), - help_text=_("Enabled, please go to the user detail add approver") - ) diff --git a/apps/templates/_nav.html b/apps/templates/_nav.html index 2222c5fd0..12c59d209 100644 --- a/apps/templates/_nav.html +++ b/apps/templates/_nav.html @@ -130,7 +130,7 @@ {% endif %} -{% if request.user.can_admin_current_org and LOGIN_CONFIRM_ENABLE and LICENSE_VALID %} +{% if request.user.can_admin_current_org and LICENSE_VALID %}
  • diff --git a/apps/tickets/handler/apply_application.py b/apps/tickets/handler/apply_application.py index aa298efa9..988f70855 100644 --- a/apps/tickets/handler/apply_application.py +++ b/apps/tickets/handler/apply_application.py @@ -51,10 +51,10 @@ class Handler(BaseHandler): '''.format( _('Applied category'), apply_category_display, _('Applied type'), apply_type_display, - _('Applied application group'), apply_applications, - _('Applied system user group'), apply_system_users, - _('Applied date start'), apply_date_start, - _('Applied date expired'), apply_date_expired, + _('Applied application group'), ','.join(apply_applications), + _('Applied system user group'), ','.join(apply_system_users), + _('Applied date start'), apply_date_start.strftime('%Y-%m-%d %H:%M:%S'), + _('Applied date expired'), apply_date_expired.strftime('%Y-%m-%d %H:%M:%S'), ) return applied_body diff --git a/apps/tickets/handler/apply_asset.py b/apps/tickets/handler/apply_asset.py index fdc7f2103..3f35998c1 100644 --- a/apps/tickets/handler/apply_asset.py +++ b/apps/tickets/handler/apply_asset.py @@ -44,11 +44,11 @@ class Handler(BaseHandler): {}: {}, {}: {} '''.format( - _("Applied hostname group"), apply_assets, - _("Applied system user group"), apply_system_users, - _("Applied actions"), apply_actions_display, - _('Applied date start'), apply_date_start, - _('Applied date expired'), apply_date_expired, + _("Applied hostname group"), ','.join(apply_assets), + _("Applied system user group"), ','.join(apply_system_users), + _("Applied actions"), ','.join(apply_actions_display), + _('Applied date start'), apply_date_start.strftime('%Y-%m-%d %H:%M:%S'), + _('Applied date expired'), apply_date_expired.strftime('%Y-%m-%d %H:%M:%S'), ) return applied_body diff --git a/apps/users/api/user.py b/apps/users/api/user.py index f55a96964..d72f04f73 100644 --- a/apps/users/api/user.py +++ b/apps/users/api/user.py @@ -19,7 +19,7 @@ from orgs.utils import current_org from orgs.models import ROLE as ORG_ROLE, OrganizationMember from users.utils import LoginBlockUtil, MFABlockUtils from .. import serializers -from ..serializers import UserSerializer, UserRetrieveSerializer, MiniUserSerializer, InviteSerializer +from ..serializers import UserSerializer, MiniUserSerializer, InviteSerializer from .mixins import UserQuerysetMixin from ..models import User from ..signals import post_user_create @@ -38,7 +38,6 @@ class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet): permission_classes = (IsOrgAdmin, CanUpdateDeleteUser) serializer_classes = { 'default': UserSerializer, - 'retrieve': UserRetrieveSerializer, 'suggestion': MiniUserSerializer, 'invite': InviteSerializer, } diff --git a/apps/users/models/user.py b/apps/users/models/user.py index f3b4f8579..90b6b1976 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -17,6 +17,7 @@ from django.utils.translation import ugettext_lazy as _ from django.utils import timezone from django.shortcuts import reverse +from acls.models import LoginACL from orgs.utils import current_org from orgs.models import OrganizationMember, Organization from common.exceptions import JMSException @@ -148,13 +149,6 @@ class AuthMixin: return True return False - def get_login_confirm_setting(self): - if hasattr(self, 'login_confirm_setting'): - s = self.login_confirm_setting - if s.reviewers.all().count() and s.is_active: - return s - return False - @staticmethod def get_public_key_body(key): for i in key.split(): @@ -758,11 +752,6 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): user_default = settings.STATIC_URL + "img/avatar/user.png" return user_default - # def admin_orgs(self): - # from orgs.models import Organization - # orgs = Organization.get_user_admin_or_audit_orgs(self) - # return orgs - def avatar_url(self): admin_default = settings.STATIC_URL + "img/avatar/admin.png" user_default = settings.STATIC_URL + "img/avatar/user.png" diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index 20a94d224..d0357db8a 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- # -from django.core.cache import cache from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers @@ -12,7 +11,7 @@ from ..models import User from ..const import SystemOrOrgRole, PasswordStrategy __all__ = [ - 'UserSerializer', 'UserRetrieveSerializer', 'MiniUserSerializer', + 'UserSerializer', 'MiniUserSerializer', 'InviteSerializer', 'ServiceAccountSerializer', ] @@ -29,7 +28,8 @@ 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', label=_('Can public key authentication')) + can_public_key_auth = serializers.ReadOnlyField( + source='can_use_ssh_key_login', label=_('Can public key authentication')) org_roles = serializers.ListField( label=_('Organization role name'), allow_null=True, required=False, child=serializers.ChoiceField(choices=ORG_ROLE.choices), default=["User"] @@ -184,14 +184,6 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): return super(UserSerializer, self).update(instance, validated_data) -class UserRetrieveSerializer(UserSerializer): - login_confirm_settings = serializers.PrimaryKeyRelatedField(read_only=True, - source='login_confirm_setting.reviewers', many=True) - - class Meta(UserSerializer.Meta): - fields = UserSerializer.Meta.fields + ['login_confirm_settings'] - - class MiniUserSerializer(serializers.ModelSerializer): class Meta: model = User diff --git a/config_example.yml b/config_example.yml index dd509c0aa..c3075a8b6 100644 --- a/config_example.yml +++ b/config_example.yml @@ -124,9 +124,6 @@ REDIS_PORT: 6379 # 启用定时任务 # PERIOD_TASK_ENABLED: True # -# 启用二次复合认证配置 -# LOGIN_CONFIRM_ENABLE: False -# # Windows 登录跳过手动输入密码 # WINDOWS_SKIP_ALL_MANUAL_PASSWORD: False