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