mirror of https://github.com/jumpserver/jumpserver
parent
2366f02d10
commit
a5acdb9f60
|
@ -274,6 +274,7 @@ def on_user_auth_success(sender, user, request, login_type=None, **kwargs):
|
|||
logger.debug('User login success: {}'.format(user.username))
|
||||
check_different_city_login_if_need(user, request)
|
||||
data = generate_data(user.username, request, login_type=login_type)
|
||||
request.session['login_time'] = data['datetime'].strftime("%Y-%m-%d %H:%M:%S")
|
||||
data.update({'mfa': int(user.mfa_enabled), 'status': True})
|
||||
write_login_log(**data)
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ from .connection_token import *
|
|||
from .token import *
|
||||
from .mfa import *
|
||||
from .access_key import *
|
||||
from .confirm import *
|
||||
from .login_confirm import *
|
||||
from .sso import *
|
||||
from .wecom import *
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework.generics import ListCreateAPIView
|
||||
from rest_framework.response import Response
|
||||
|
||||
from common.permissions import IsValidUser
|
||||
from ..mfa import MFAOtp
|
||||
from ..const import ConfirmType
|
||||
from ..mixins import authenticate
|
||||
from ..serializers import ConfirmSerializer
|
||||
|
||||
|
||||
class ConfirmViewSet(ListCreateAPIView):
|
||||
permission_classes = (IsValidUser,)
|
||||
serializer_class = ConfirmSerializer
|
||||
|
||||
def check(self, confirm_type: str):
|
||||
if confirm_type == ConfirmType.MFA:
|
||||
return bool(MFAOtp(self.user).is_active())
|
||||
|
||||
if confirm_type == ConfirmType.PASSWORD:
|
||||
return self.user.is_password_authenticate()
|
||||
|
||||
if confirm_type == ConfirmType.RELOGIN:
|
||||
return not self.user.is_password_authenticate()
|
||||
|
||||
def authenticate(self, confirm_type, secret_key):
|
||||
if confirm_type == ConfirmType.MFA:
|
||||
ok, msg = MFAOtp(self.user).check_code(secret_key)
|
||||
return ok, msg
|
||||
|
||||
if confirm_type == ConfirmType.PASSWORD:
|
||||
ok = authenticate(self.request, username=self.user.username, password=secret_key)
|
||||
msg = '' if ok else _('Authentication failed password incorrect')
|
||||
return ok, msg
|
||||
|
||||
if confirm_type == ConfirmType.RELOGIN:
|
||||
now = timezone.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
now = datetime.strptime(now, '%Y-%m-%d %H:%M:%S')
|
||||
login_time = self.request.session.get('login_time')
|
||||
SPECIFIED_TIME = 5
|
||||
msg = _('Login time has exceeded {} minutes, please login again').format(SPECIFIED_TIME)
|
||||
if not login_time:
|
||||
return False, msg
|
||||
login_time = datetime.strptime(login_time, '%Y-%m-%d %H:%M:%S')
|
||||
if (now - login_time).seconds >= SPECIFIED_TIME * 60:
|
||||
return False, msg
|
||||
return True, ''
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
return self.request.user
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
if not settings.SECURITY_VIEW_AUTH_NEED_MFA:
|
||||
return Response('ok')
|
||||
|
||||
mfa_verify_time = request.session.get('MFA_VERIFY_TIME', 0)
|
||||
if time.time() - mfa_verify_time < settings.SECURITY_MFA_VERIFY_TTL:
|
||||
return Response('ok')
|
||||
|
||||
data = []
|
||||
for i, confirm_type in enumerate(ConfirmType.values, 1):
|
||||
if self.check(confirm_type):
|
||||
data.append({'name': confirm_type, 'level': i})
|
||||
msg = _('This action require verify your MFA')
|
||||
return Response({'error': msg, 'backends': data}, status=400)
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
validated_data = serializer.validated_data
|
||||
confirm_type = validated_data.get('confirm_type')
|
||||
secret_key = validated_data.get('secret_key')
|
||||
ok, msg = self.authenticate(confirm_type, secret_key)
|
||||
if ok:
|
||||
request.session["MFA_VERIFY_TIME"] = int(time.time())
|
||||
return Response('ok')
|
||||
return Response({'error': msg}, status=400)
|
|
@ -2,7 +2,7 @@ from rest_framework.views import APIView
|
|||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
||||
from users.permissions import IsAuthPasswdTimeValid
|
||||
from users.permissions import IsAuthConfirmTimeValid
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
from common.mixins.api import RoleUserMixin, RoleAdminMixin
|
||||
|
@ -26,9 +26,8 @@ class DingTalkQRUnBindBase(APIView):
|
|||
|
||||
|
||||
class DingTalkQRUnBindForUserApi(RoleUserMixin, DingTalkQRUnBindBase):
|
||||
permission_classes = (IsAuthPasswdTimeValid,)
|
||||
permission_classes = (IsAuthConfirmTimeValid,)
|
||||
|
||||
|
||||
class DingTalkQRUnBindForAdminApi(RoleAdminMixin, DingTalkQRUnBindBase):
|
||||
user_id_url_kwarg = 'user_id'
|
||||
|
|
@ -2,7 +2,7 @@ from rest_framework.views import APIView
|
|||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
||||
from users.permissions import IsAuthPasswdTimeValid
|
||||
from users.permissions import IsAuthConfirmTimeValid
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
from common.mixins.api import RoleUserMixin, RoleAdminMixin
|
||||
|
@ -26,7 +26,7 @@ class FeiShuQRUnBindBase(APIView):
|
|||
|
||||
|
||||
class FeiShuQRUnBindForUserApi(RoleUserMixin, FeiShuQRUnBindBase):
|
||||
permission_classes = (IsAuthPasswdTimeValid,)
|
||||
permission_classes = (IsAuthConfirmTimeValid,)
|
||||
|
||||
|
||||
class FeiShuQRUnBindForAdminApi(RoleAdminMixin, FeiShuQRUnBindBase):
|
||||
|
|
|
@ -2,7 +2,7 @@ from rest_framework.views import APIView
|
|||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
||||
from users.permissions import IsAuthPasswdTimeValid
|
||||
from users.permissions import IsAuthConfirmTimeValid
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
from common.mixins.api import RoleUserMixin, RoleAdminMixin
|
||||
|
@ -26,9 +26,8 @@ class WeComQRUnBindBase(APIView):
|
|||
|
||||
|
||||
class WeComQRUnBindForUserApi(RoleUserMixin, WeComQRUnBindBase):
|
||||
permission_classes = (IsAuthPasswdTimeValid,)
|
||||
permission_classes = (IsAuthConfirmTimeValid,)
|
||||
|
||||
|
||||
class WeComQRUnBindForAdminApi(RoleAdminMixin, WeComQRUnBindBase):
|
||||
user_id_url_kwarg = 'user_id'
|
||||
|
|
@ -1,2 +1,10 @@
|
|||
from django.db.models import TextChoices
|
||||
|
||||
RSA_PRIVATE_KEY = 'rsa_private_key'
|
||||
RSA_PUBLIC_KEY = 'rsa_public_key'
|
||||
|
||||
|
||||
class ConfirmType(TextChoices):
|
||||
RELOGIN = 'relogin', 'Re-Login'
|
||||
PASSWORD = 'password', 'Password'
|
||||
MFA = 'mfa', 'MFA'
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from .token import *
|
||||
from .connect_token import *
|
||||
from .password_mfa import *
|
||||
from .confirm import *
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import EncryptedField
|
||||
from ..const import ConfirmType
|
||||
|
||||
|
||||
class ConfirmSerializer(serializers.Serializer):
|
||||
confirm_type = serializers.ChoiceField(
|
||||
required=True, choices=ConfirmType.choices
|
||||
)
|
||||
secret_key = EncryptedField()
|
|
@ -26,6 +26,7 @@ urlpatterns = [
|
|||
path('feishu/event/subscription/callback/', api.FeiShuEventSubscriptionCallback.as_view(), name='feishu-event-subscription-callback'),
|
||||
|
||||
path('auth/', api.TokenCreateApi.as_view(), name='user-auth'),
|
||||
path('confirm/', api.ConfirmViewSet.as_view(), name='user-confirm'),
|
||||
path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'),
|
||||
path('mfa/verify/', api.MFAChallengeVerifyApi.as_view(), name='mfa-verify'),
|
||||
path('mfa/challenge/', api.MFAChallengeVerifyApi.as_view(), name='mfa-challenge'),
|
||||
|
|
|
@ -9,8 +9,9 @@ from rest_framework.permissions import IsAuthenticated, AllowAny
|
|||
from rest_framework.exceptions import APIException
|
||||
|
||||
from users.views import UserVerifyPasswordView
|
||||
from users.utils import is_auth_password_time_valid
|
||||
from users.utils import is_auth_confirm_time_valid
|
||||
from users.models import User
|
||||
from users.permissions import IsAuthConfirmTimeValid
|
||||
from common.utils import get_logger, FlashMessageUtil
|
||||
from common.utils.random import random_string
|
||||
from common.utils.django import reverse, get_object_or_none
|
||||
|
@ -118,17 +119,12 @@ class DingTalkOAuthMixin(DingTalkBaseMixin, View):
|
|||
|
||||
|
||||
class DingTalkQRBindView(DingTalkQRMixin, View):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
permission_classes = (IsAuthenticated, IsAuthConfirmTimeValid)
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
user = request.user
|
||||
redirect_url = request.GET.get('redirect_url')
|
||||
|
||||
if not is_auth_password_time_valid(request.session):
|
||||
msg = _('Please verify your password first')
|
||||
response = self.get_failed_response(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
redirect_uri = reverse('authentication:dingtalk-qr-bind-callback', kwargs={'user_id': user.id}, external=True)
|
||||
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ from django.db.utils import IntegrityError
|
|||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
from users.utils import is_auth_password_time_valid
|
||||
from users.permissions import IsAuthConfirmTimeValid
|
||||
from users.views import UserVerifyPasswordView
|
||||
from users.models import User
|
||||
from common.utils import get_logger, FlashMessageUtil
|
||||
|
@ -89,17 +89,12 @@ class FeiShuQRMixin(PermissionsMixin, View):
|
|||
|
||||
|
||||
class FeiShuQRBindView(FeiShuQRMixin, View):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
permission_classes = (IsAuthenticated, IsAuthConfirmTimeValid)
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
user = request.user
|
||||
redirect_url = request.GET.get('redirect_url')
|
||||
|
||||
if not is_auth_password_time_valid(request.session):
|
||||
msg = _('Please verify your password first')
|
||||
response = self.get_failed_response(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
redirect_uri = reverse('authentication:feishu-qr-bind-callback', external=True)
|
||||
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
|
||||
|
||||
|
|
|
@ -9,8 +9,8 @@ from rest_framework.permissions import IsAuthenticated, AllowAny
|
|||
from rest_framework.exceptions import APIException
|
||||
|
||||
from users.views import UserVerifyPasswordView
|
||||
from users.utils import is_auth_password_time_valid
|
||||
from users.models import User
|
||||
from users.permissions import IsAuthConfirmTimeValid
|
||||
from common.utils import get_logger, FlashMessageUtil
|
||||
from common.utils.random import random_string
|
||||
from common.utils.django import reverse, get_object_or_none
|
||||
|
@ -118,17 +118,12 @@ class WeComOAuthMixin(WeComBaseMixin, View):
|
|||
|
||||
|
||||
class WeComQRBindView(WeComQRMixin, View):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
permission_classes = (IsAuthenticated, IsAuthConfirmTimeValid)
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
user = request.user
|
||||
redirect_url = request.GET.get('redirect_url')
|
||||
|
||||
if not is_auth_password_time_valid(request.session):
|
||||
msg = _('Please verify your password first')
|
||||
response = self.get_failed_response(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
redirect_uri = reverse('authentication:wecom-qr-bind-callback', kwargs={'user_id': user.id}, external=True)
|
||||
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
|
||||
|
||||
|
|
|
@ -791,6 +791,11 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
|
|||
def is_local(self):
|
||||
return self.source == self.Source.local.value
|
||||
|
||||
def is_password_authenticate(self):
|
||||
cas = self.Source.cas
|
||||
saml2 = self.Source.saml2
|
||||
return self.source not in [cas, saml2]
|
||||
|
||||
def set_unprovide_attr_if_need(self):
|
||||
if not self.name:
|
||||
self.name = self.username
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
from rest_framework import permissions
|
||||
|
||||
from .utils import is_auth_password_time_valid
|
||||
from .utils import is_auth_password_time_valid, is_auth_confirm_time_valid
|
||||
|
||||
|
||||
class IsAuthPasswdTimeValid(permissions.IsAuthenticated):
|
||||
|
||||
def has_permission(self, request, view):
|
||||
return super().has_permission(request, view) \
|
||||
and is_auth_password_time_valid(request.session)
|
||||
and is_auth_password_time_valid(request.session)
|
||||
|
||||
|
||||
class IsAuthConfirmTimeValid(permissions.IsAuthenticated):
|
||||
|
||||
def has_permission(self, request, view):
|
||||
return super().has_permission(request, view) \
|
||||
and is_auth_confirm_time_valid(request.session)
|
||||
|
|
|
@ -255,3 +255,16 @@ def is_auth_password_time_valid(session):
|
|||
|
||||
def is_auth_otp_time_valid(session):
|
||||
return is_auth_time_valid(session, 'auth_otp_expired_at')
|
||||
|
||||
|
||||
def is_confirm_time_valid(session, key):
|
||||
if not settings.SECURITY_VIEW_AUTH_NEED_MFA:
|
||||
return True
|
||||
mfa_verify_time = session.get(key, 0)
|
||||
if time.time() - mfa_verify_time < settings.SECURITY_MFA_VERIFY_TTL:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_auth_confirm_time_valid(session):
|
||||
return is_confirm_time_valid(session, 'MFA_VERIFY_TIME')
|
||||
|
|
Loading…
Reference in New Issue