mirror of https://github.com/jumpserver/jumpserver
				
				
				
			feat: user login acl (#6963)
* feat: user login acl * 添加分时登陆 * acl 部分还原 * 简化acl判断逻辑 Co-authored-by: feng626 <1304903146@qq.com> Co-authored-by: feng626 <57284900+feng626@users.noreply.github.com>pull/7024/head
							parent
							
								
									9424929dde
								
							
						
					
					
						commit
						9acfd461b4
					
				|  | @ -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): | ||||
|  |  | |||
|  | @ -1,4 +1,3 @@ | |||
| 
 | ||||
| from orgs.mixins.api import OrgBulkModelViewSet | ||||
| from common.permissions import IsOrgAdmin | ||||
| from .. import models, serializers | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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' | ||||
|         ) | ||||
|  | @ -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', | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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()]) | ||||
|  |  | |||
|  | @ -0,0 +1 @@ | |||
| from .rules import * | ||||
|  | @ -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')) | ||||
|  | @ -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,) | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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', | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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() | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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() | ||||
| 
 | ||||
|  |  | |||
|  | @ -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/<uuid:user_id>/', api.LoginConfirmSettingUpdateApi.as_view(), name='login-confirm-setting-update') | ||||
| ] | ||||
| 
 | ||||
| urlpatterns += router.urls | ||||
|  |  | |||
|  | @ -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): | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,3 +0,0 @@ | |||
| version https://git-lfs.github.com/spec/v1 | ||||
| oid sha256:76a9ccd646b5f8a18196e52a277e7af8319fdb155193b310ed6c1769e45a1ffd | ||||
| size 97641 | ||||
|  | @ -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 认证关闭了" | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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") | ||||
|     ) | ||||
|  |  | |||
|  | @ -130,7 +130,7 @@ | |||
|     </li> | ||||
| {% 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 %} | ||||
|     <li id="tickets"> | ||||
|         <a href="{% url 'tickets:ticket-list' %}"> | ||||
|             <i class="fa fa-check-square-o" style="width: 14px"></i> | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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, | ||||
|     } | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -124,9 +124,6 @@ REDIS_PORT: 6379 | |||
| # 启用定时任务 | ||||
| # PERIOD_TASK_ENABLED: True | ||||
| # | ||||
| # 启用二次复合认证配置 | ||||
| # LOGIN_CONFIRM_ENABLE: False | ||||
| # | ||||
| # Windows 登录跳过手动输入密码 | ||||
| # WINDOWS_SKIP_ALL_MANUAL_PASSWORD: False | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 fit2bot
						fit2bot