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