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
fit2bot 2021-10-20 17:56:59 +08:00 committed by GitHub
parent 9424929dde
commit 9acfd461b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 325 additions and 206 deletions

View File

@ -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):

View File

@ -1,4 +1,3 @@
from orgs.mixins.api import OrgBulkModelViewSet
from common.permissions import IsOrgAdmin
from .. import models, serializers

View File

@ -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

15
apps/acls/filters.py Normal file
View File

@ -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'
)

View File

@ -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',
),
]

View File

@ -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

View File

@ -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()])

View File

@ -0,0 +1 @@
from .rules import *

View File

@ -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'))

View File

@ -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,)

View File

@ -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

View File

@ -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',
),
]

View File

@ -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()

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:76a9ccd646b5f8a18196e52a277e7af8319fdb155193b310ed6c1769e45a1ffd
size 97641

View File

@ -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 认证关闭了"

View File

@ -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,

View File

@ -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")
)

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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,
}

View File

@ -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"

View File

@ -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

View File

@ -124,9 +124,6 @@ REDIS_PORT: 6379
# 启用定时任务
# PERIOD_TASK_ENABLED: True
#
# 启用二次复合认证配置
# LOGIN_CONFIRM_ENABLE: False
#
# Windows 登录跳过手动输入密码
# WINDOWS_SKIP_ALL_MANUAL_PASSWORD: False