Commit 6ce9815d authored by ibuler's avatar ibuler

[Update] 基本完成登录逻辑

parent 9d201bbf
...@@ -110,5 +110,14 @@ class UserLoginLog(models.Model): ...@@ -110,5 +110,14 @@ class UserLoginLog(models.Model):
login_logs = login_logs.filter(username__in=username_list) login_logs = login_logs.filter(username__in=username_list)
return login_logs return login_logs
@property
def reason_display(self):
from authentication.errors import reason_choices, old_reason_choices
reason = reason_choices.get(self.reason)
if reason:
return reason
reason = old_reason_choices.get(self.reason, self.reason)
return reason
class Meta: class Meta:
ordering = ['-datetime', 'username'] ordering = ['-datetime', 'username']
...@@ -4,15 +4,18 @@ ...@@ -4,15 +4,18 @@
from django.db.models.signals import post_save, post_delete from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver from django.dispatch import receiver
from django.db import transaction from django.db import transaction
from django.utils import timezone
from rest_framework.renderers import JSONRenderer from rest_framework.renderers import JSONRenderer
from rest_framework.request import Request
from jumpserver.utils import current_request from jumpserver.utils import current_request
from common.utils import get_request_ip, get_logger, get_syslogger from common.utils import get_request_ip, get_logger, get_syslogger
from users.models import User from users.models import User
from authentication.signals import post_auth_failed, post_auth_success
from terminal.models import Session, Command from terminal.models import Session, Command
from terminal.backends.command.serializers import SessionCommandSerializer from terminal.backends.command.serializers import SessionCommandSerializer
from . import models from . import models, serializers
from . import serializers from .tasks import write_login_log_async
logger = get_logger(__name__) logger = get_logger(__name__)
sys_logger = get_syslogger("audits") sys_logger = get_syslogger("audits")
...@@ -99,3 +102,39 @@ def on_audits_log_create(sender, instance=None, **kwargs): ...@@ -99,3 +102,39 @@ def on_audits_log_create(sender, instance=None, **kwargs):
data = json_render.render(s.data).decode(errors='ignore') data = json_render.render(s.data).decode(errors='ignore')
msg = "{} - {}".format(category, data) msg = "{} - {}".format(category, data)
sys_logger.info(msg) sys_logger.info(msg)
def generate_data(username, request):
user_agent = request.META.get('HTTP_USER_AGENT', '')
if isinstance(request, Request):
login_ip = request.data.get('remote_addr', None)
login_type = request.data.get('login_type', '')
else:
login_ip = get_request_ip(request)
login_type = 'W'
data = {
'username': username,
'ip': login_ip,
'type': login_type,
'user_agent': user_agent,
'datetime': timezone.now()
}
return data
@receiver(post_auth_success)
def on_user_auth_success(sender, user, request, **kwargs):
logger.debug('User login success: {}'.format(user.username))
data = generate_data(user.username, request)
data.update({'mfa': int(user.otp_enabled), 'status': True})
write_login_log_async.delay(**data)
@receiver(post_auth_failed)
def on_user_auth_failed(sender, username, request, reason, **kwargs):
logger.debug('User login failed: {}'.format(username))
data = generate_data(username, request)
data.update({'reason': reason, 'status': False})
write_login_log_async.delay(**data)
...@@ -7,6 +7,7 @@ from celery import shared_task ...@@ -7,6 +7,7 @@ from celery import shared_task
from ops.celery.decorator import register_as_period_task from ops.celery.decorator import register_as_period_task
from .models import UserLoginLog from .models import UserLoginLog
from .utils import write_login_log
@register_as_period_task(interval=3600*24) @register_as_period_task(interval=3600*24)
...@@ -19,3 +20,8 @@ def clean_login_log_period(): ...@@ -19,3 +20,8 @@ def clean_login_log_period():
days = 90 days = 90
expired_day = now - datetime.timedelta(days=days) expired_day = now - datetime.timedelta(days=days)
UserLoginLog.objects.filter(datetime__lt=expired_day).delete() UserLoginLog.objects.filter(datetime__lt=expired_day).delete()
@shared_task
def write_login_log_async(*args, **kwargs):
write_login_log(*args, **kwargs)
...@@ -78,7 +78,7 @@ ...@@ -78,7 +78,7 @@
<td class="text-center">{{ login_log.ip }}</td> <td class="text-center">{{ login_log.ip }}</td>
<td class="text-center">{{ login_log.city }}</td> <td class="text-center">{{ login_log.city }}</td>
<td class="text-center">{{ login_log.get_mfa_display }}</td> <td class="text-center">{{ login_log.get_mfa_display }}</td>
<td class="text-center">{% trans login_log.reason %}</td> <td class="text-center">{{ login_log.reason_display }}</td>
<td class="text-center">{{ login_log.get_status_display }}</td> <td class="text-center">{{ login_log.get_status_display }}</td>
<td class="text-center">{{ login_log.datetime }}</td> <td class="text-center">{{ login_log.datetime }}</td>
</tr> </tr>
......
import csv import csv
import codecs import codecs
from django.http import HttpResponse from django.http import HttpResponse
from django.utils.translation import ugettext as _
from common.utils import validate_ip, get_ip_city
def get_excel_response(filename): def get_excel_response(filename):
...@@ -20,3 +23,16 @@ def write_content_to_excel(response, header=None, login_logs=None, fields=None): ...@@ -20,3 +23,16 @@ def write_content_to_excel(response, header=None, login_logs=None, fields=None):
data = [getattr(log, field.name) for field in fields] data = [getattr(log, field.name) for field in fields]
writer.writerow(data) writer.writerow(data)
return response return response
def write_login_log(*args, **kwargs):
from audits.models import UserLoginLog
default_city = _("Unknown")
ip = kwargs.get('ip') or ''
if not (ip and validate_ip(ip)):
ip = ip[:15]
city = default_city
else:
city = get_ip_city(ip) or default_city
kwargs.update({'ip': ip, 'city': city})
UserLoginLog.objects.create(**kwargs)
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import uuid import uuid
import time
from django.core.cache import cache from django.core.cache import cache
from django.urls import reverse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext as _
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.generics import CreateAPIView
from rest_framework.views import APIView from rest_framework.views import APIView
from common.utils import get_logger, get_request_ip, get_object_or_none from common.utils import get_logger
from common.permissions import IsOrgAdminOrAppUser, IsValidUser from common.permissions import IsOrgAdminOrAppUser
from orgs.mixins.api import RootOrgViewMixin from orgs.mixins.api import RootOrgViewMixin
from users.serializers import UserSerializer
from users.models import User from users.models import User
from assets.models import Asset, SystemUser from assets.models import Asset, SystemUser
from users.utils import (
check_otp_code, increase_login_failed_count,
is_block_login, clean_failed_count
)
from .. import errors
from ..utils import check_user_valid
from ..serializers import OtpVerifySerializer
from ..signals import post_auth_success, post_auth_failed
logger = get_logger(__name__) logger = get_logger(__name__)
__all__ = [ __all__ = [
'UserAuthApi', 'UserConnectionTokenApi', 'UserOtpAuthApi', 'UserConnectionTokenApi',
'UserOtpVerifyApi', 'UserOrderAcceptAuthApi',
] ]
class UserAuthApi(RootOrgViewMixin, APIView):
permission_classes = (AllowAny,)
serializer_class = UserSerializer
def get_serializer_context(self):
return {
'request': self.request,
'view': self
}
def get_serializer(self, *args, **kwargs):
kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs)
def post(self, request):
# limit login
username = request.data.get('username')
ip = request.data.get('remote_addr', None)
ip = ip or get_request_ip(request)
if is_block_login(username, ip):
msg = _("Log in frequently and try again later")
logger.warn(msg + ': ' + username + ':' + ip)
return Response({'msg': msg}, status=401)
user, msg = self.check_user_valid(request)
if not user:
username = request.data.get('username', '')
self.send_auth_signal(success=False, username=username, reason=msg)
increase_login_failed_count(username, ip)
return Response({'msg': msg}, status=401)
if not user.otp_enabled:
self.send_auth_signal(success=True, user=user)
# 登陆成功,清除原来的缓存计数
clean_failed_count(username, ip)
token, expired_at = user.create_bearer_token(request)
return Response(
{'token': token, 'user': self.get_serializer(user).data}
)
seed = uuid.uuid4().hex
cache.set(seed, user, 300)
return Response(
{
'code': 101,
'msg': _('Please carry seed value and '
'conduct MFA secondary certification'),
'otp_url': reverse('api-auth:user-otp-auth'),
'seed': seed,
'user': self.get_serializer(user).data
}, status=300
)
@staticmethod
def check_user_valid(request):
username = request.data.get('username', '')
password = request.data.get('password', '')
public_key = request.data.get('public_key', '')
user, msg = check_user_valid(
username=username, password=password,
public_key=public_key
)
return user, msg
def send_auth_signal(self, success=True, user=None, username='', reason=''):
if success:
post_auth_success.send(sender=self.__class__, user=user, request=self.request)
else:
post_auth_failed.send(
sender=self.__class__, username=username,
request=self.request, reason=reason
)
class UserConnectionTokenApi(RootOrgViewMixin, APIView): class UserConnectionTokenApi(RootOrgViewMixin, APIView):
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
...@@ -150,82 +61,5 @@ class UserConnectionTokenApi(RootOrgViewMixin, APIView): ...@@ -150,82 +61,5 @@ class UserConnectionTokenApi(RootOrgViewMixin, APIView):
return super().get_permissions() return super().get_permissions()
class UserOtpAuthApi(RootOrgViewMixin, APIView):
permission_classes = (AllowAny,)
serializer_class = UserSerializer
def get_serializer_context(self):
return {
'request': self.request,
'view': self
}
def get_serializer(self, *args, **kwargs):
kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs)
def post(self, request):
otp_code = request.data.get('otp_code', '')
seed = request.data.get('seed', '')
user = cache.get(seed, None)
if not user:
return Response(
{'msg': _('Please verify the user name and password first')},
status=401
)
if not check_otp_code(user.otp_secret_key, otp_code):
self.send_auth_signal(success=False, username=user.username, reason=errors.mfa_failed)
return Response({'msg': _('MFA certification failed')}, status=401)
self.send_auth_signal(success=True, user=user)
token, expired_at = user.create_bearer_token(request)
data = {'token': token, 'user': self.get_serializer(user).data}
return Response(data)
def send_auth_signal(self, success=True, user=None, username='', reason=''):
if success:
post_auth_success.send(sender=self.__class__, user=user, request=self.request)
else:
post_auth_failed.send(
sender=self.__class__, username=username,
request=self.request, reason=reason
)
class UserOtpVerifyApi(CreateAPIView):
permission_classes = (IsValidUser,)
serializer_class = OtpVerifySerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
code = serializer.validated_data["code"]
if request.user.check_otp(code):
request.session["MFA_VERIFY_TIME"] = int(time.time())
return Response({"ok": "1"})
else:
return Response({"error": "Code not valid"}, status=400)
class UserOrderAcceptAuthApi(APIView):
permission_classes = ()
def get(self, request, *args, **kwargs):
from orders.models import LoginConfirmOrder
order_id = self.request.session.get("auth_order_id")
logger.debug('Login confirm order id: {}'.format(order_id))
if not order_id:
order = None
else:
order = get_object_or_none(LoginConfirmOrder, pk=order_id)
if not order:
error = _("No order found or order expired")
return Response({"error": error, "status": "not found"}, status=404)
if order.status == order.STATUS_ACCEPTED:
self.request.session["auth_confirm"] = "1"
return Response({"msg": "ok"})
elif order.status == order.STATUS_REJECTED:
error = _("Order was rejected by {}").format(order.assignee_display)
else:
error = "Order status: {}".format(order.status)
return Response({"error": error, "status": order.status}, status=400)
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from rest_framework.generics import UpdateAPIView from rest_framework.generics import UpdateAPIView
from rest_framework.response import Response
from rest_framework.views import APIView
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from common.utils import get_logger, get_object_or_none
from common.permissions import IsOrgAdmin from common.permissions import IsOrgAdmin
from ..models import LoginConfirmSetting from ..models import LoginConfirmSetting
from ..serializers import LoginConfirmSettingSerializer from ..serializers import LoginConfirmSettingSerializer
from .. import errors
__all__ = ['LoginConfirmSettingUpdateApi'] __all__ = ['LoginConfirmSettingUpdateApi', 'UserOrderAcceptAuthApi']
logger = get_logger(__name__)
class LoginConfirmSettingUpdateApi(UpdateAPIView): class LoginConfirmSettingUpdateApi(UpdateAPIView):
...@@ -23,3 +28,29 @@ class LoginConfirmSettingUpdateApi(UpdateAPIView): ...@@ -23,3 +28,29 @@ class LoginConfirmSettingUpdateApi(UpdateAPIView):
defaults, user=user, defaults, user=user,
) )
return s return s
class UserOrderAcceptAuthApi(APIView):
permission_classes = ()
def get(self, request, *args, **kwargs):
from orders.models import LoginConfirmOrder
order_id = self.request.session.get("auth_order_id")
logger.debug('Login confirm order id: {}'.format(order_id))
if not order_id:
order = None
else:
order = get_object_or_none(LoginConfirmOrder, pk=order_id)
try:
if not order:
raise errors.LoginConfirmOrderNotFound(order_id)
if order.status == order.STATUS_ACCEPTED:
self.request.session["auth_confirm"] = "1"
return Response({"msg": "ok"})
elif order.status == order.STATUS_REJECTED:
raise errors.LoginConfirmRejectedError(order_id)
else:
return errors.LoginConfirmWaitError(order_id)
except errors.AuthFailedError as e:
data = e.as_data()
return Response(data, status=400)
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import time
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
from rest_framework.generics import CreateAPIView from rest_framework.generics import CreateAPIView
from rest_framework.serializers import ValidationError
from rest_framework.response import Response
from common.permissions import IsValidUser
from ..serializers import OtpVerifySerializer
from .. import serializers from .. import serializers
from .. import errors
from ..mixins import AuthMixin
class MFAChallengeApi(CreateAPIView): __all__ = ['MFAChallengeApi', 'UserOtpVerifyApi']
class MFAChallengeApi(AuthMixin, CreateAPIView):
permission_classes = (AllowAny,) permission_classes = (AllowAny,)
serializer_class = serializers.MFAChallengeSerializer serializer_class = serializers.MFAChallengeSerializer
def perform_create(self, serializer):
try:
user = self.get_user_from_session()
code = serializer.validated_data.get('code')
valid = user.check_otp(code)
if not valid:
self.request.session['auth_mfa'] = ''
raise errors.MFAFailedError(
username=user.username, request=self.request
)
except errors.AuthFailedError as e:
data = {"error": e.error, "msg": e.reason}
raise ValidationError(data)
def create(self, request, *args, **kwargs):
super().create(request, *args, **kwargs)
return Response({'msg': 'ok'})
class UserOtpVerifyApi(CreateAPIView):
permission_classes = (IsValidUser,)
serializer_class = OtpVerifySerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
code = serializer.validated_data["code"]
if request.user.check_otp(code):
request.session["MFA_VERIFY_TIME"] = int(time.time())
return Response({"ok": "1"})
else:
return Response({"error": "Code not valid"}, status=400)
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.utils.translation import ugettext as _
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.generics import CreateAPIView from rest_framework.generics import CreateAPIView
from common.utils import get_request_ip, get_logger, get_object_or_none from common.utils import get_logger
from users.utils import (
check_otp_code, increase_login_failed_count,
is_block_login, clean_failed_count
)
from users.models import User
from ..utils import check_user_valid
from ..signals import post_auth_success, post_auth_failed
from .. import serializers, errors from .. import serializers, errors
from ..mixins import AuthMixin
logger = get_logger(__name__) logger = get_logger(__name__)
...@@ -22,126 +16,24 @@ logger = get_logger(__name__) ...@@ -22,126 +16,24 @@ logger = get_logger(__name__)
__all__ = ['TokenCreateApi'] __all__ = ['TokenCreateApi']
class TokenCreateApi(CreateAPIView): class TokenCreateApi(AuthMixin, CreateAPIView):
permission_classes = (AllowAny,) permission_classes = (AllowAny,)
serializer_class = serializers.BearerTokenSerializer serializer_class = serializers.BearerTokenSerializer
def check_session(self): def create_session_if_need(self):
pass if self.request.session.is_empty():
self.request.session.create()
def get_request_ip(self):
ip = self.request.data.get('remote_addr', None)
ip = ip or get_request_ip(self.request)
return ip
def check_is_block(self):
username = self.request.data.get("username")
ip = self.get_request_ip()
if is_block_login(username, ip):
msg = errors.ip_blocked
logger.warn(msg + ': ' + username + ':' + ip)
raise errors.AuthFailedError(msg, 'blocked')
def get_user_from_session(self):
user_id = self.request.session["user_id"]
user = get_object_or_none(User, pk=user_id)
if not user:
error = "Not user in session: {}".format(user_id)
raise errors.AuthFailedError(error, 'session_error')
return user
def check_user_auth(self):
request = self.request
if request.session.get("auth_password") and \
request.session.get('user_id'):
user = self.get_user_from_session()
return user
self.check_is_block()
username = request.data.get('username', '')
password = request.data.get('password', '')
public_key = request.data.get('public_key', '')
user, msg = check_user_valid(
username=username, password=password,
public_key=public_key
)
ip = self.get_request_ip()
if not user:
raise errors.AuthFailedError(msg, error='auth_failed', username=username)
clean_failed_count(username, ip)
request.session['auth_password'] = 1
request.session['user_id'] = str(user.id)
return user
def check_user_mfa_if_need(self, user):
if self.request.session.get('auth_mfa'):
return True
if not user.otp_enabled or not user.otp_secret_key:
return True
otp_code = self.request.data.get("otp_code")
if not otp_code:
raise errors.MFARequiredError()
if not check_otp_code(user.otp_secret_key, otp_code):
raise errors.AuthFailedError(
errors.mfa_failed, error='mfa_failed',
username=user.username,
)
return True
def check_user_login_confirm_if_need(self, user):
from orders.models import LoginConfirmOrder
confirm_setting = user.get_login_confirm_setting()
if self.request.session.get('auth_confirm') or not confirm_setting:
return
order = None
if self.request.session.get('auth_order_id'):
order_id = self.request.session['auth_order_id']
order = get_object_or_none(LoginConfirmOrder, pk=order_id)
if not order:
order = confirm_setting.create_confirm_order(self.request)
self.request.session['auth_order_id'] = str(order.id)
if order.status == "accepted":
return
elif order.status == "rejected":
raise errors.LoginConfirmRejectedError()
else:
raise errors.LoginConfirmWaitError()
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
self.check_session() self.create_session_if_need()
# 如果认证没有过,检查账号密码 # 如果认证没有过,检查账号密码
try: try:
user = self.check_user_auth() user = self.check_user_auth()
self.check_user_mfa_if_need(user) self.check_user_mfa_if_need(user)
self.check_user_login_confirm_if_need(user) self.check_user_login_confirm_if_need(user)
self.send_auth_signal(success=True, user=user) self.send_auth_signal(success=True, user=user)
self.clear_auth_mark()
resp = super().create(request, *args, **kwargs) resp = super().create(request, *args, **kwargs)
return resp return resp
except errors.AuthFailedError as e: except errors.AuthFailedError as e:
if e.username: return Response(e.as_data(), status=401)
increase_login_failed_count(e.username, self.get_request_ip())
self.send_auth_signal(
success=False, username=e.username, reason=e.reason
)
return Response({'msg': e.reason, 'error': e.error}, status=401)
except errors.MFARequiredError:
msg = _("MFA required")
data = {'msg': msg, "choices": ["otp"], "error": 'mfa_required'}
return Response(data, status=300)
except errors.LoginConfirmRejectedError as e:
pass
except errors.LoginConfirmWaitError as e:
pass
except errors.LoginConfirmRequiredError as e:
pass
def send_auth_signal(self, success=True, user=None, username='', reason=''):
if success:
post_auth_success.send(
sender=self.__class__, user=user, request=self.request
)
else:
post_auth_failed.send(
sender=self.__class__, username=username,
request=self.request, reason=reason
)
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.urls import reverse
from django.conf import settings
password_failed = _('Username/password check failed') from .signals import post_auth_failed
mfa_failed = _('MFA authentication failed') from users.utils import (
user_not_exist = _("Username does not exist") increase_login_failed_count, get_login_failed_count
password_expired = _("Password expired") )
user_invalid = _('Disabled or expired')
ip_blocked = _("Log in frequently and try again later")
mfa_required = _("MFA required") reason_password_failed = 'password_failed'
login_confirm_required = _("Login confirm required") reason_mfa_failed = 'mfa_failed'
login_confirm_wait = _("Wait login confirm") reason_user_not_exist = 'user_not_exist'
reason_password_expired = 'password_expired'
reason_user_invalid = 'user_invalid'
reason_user_inactive = 'user_inactive'
reason_choices = {
reason_password_failed: _('Username/password check failed'),
reason_mfa_failed: _('MFA authentication failed'),
reason_user_not_exist: _("Username does not exist"),
reason_password_expired: _("Password expired"),
reason_user_invalid: _('Disabled or expired'),
reason_user_inactive: _("This account is inactive.")
}
old_reason_choices = {
'0': '-',
'1': reason_choices[reason_password_failed],
'2': reason_choices[reason_mfa_failed],
'3': reason_choices[reason_user_not_exist],
'4': reason_choices[reason_password_expired],
}
session_empty_msg = _("No session found, check your cookie")
invalid_login_msg = _(
"The username or password you entered is incorrect, "
"please enter it again. "
"You can also try {times_try} times "
"(The account will be temporarily locked for {block_time} minutes)"
)
block_login_msg = _(
"The account has been locked "
"(please contact admin to unlock it or try again after {} minutes)"
)
mfa_failed_msg = _("MFA code invalid, or ntp sync server time")
mfa_required_msg = _("MFA required")
login_confirm_required_msg = _("Login confirm required")
login_confirm_wait_msg = _("Wait login confirm order for accept")
login_confirm_rejected_msg = _("Login confirm order was rejected")
login_confirm_order_not_found_msg = _("Order not found")
class AuthFailedNeedLogMixin:
username = ''
request = None
error = ''
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
post_auth_failed.send(
sender=self.__class__, username=self.username,
request=self.request, reason=self.error
)
class AuthFailedNeedBlockMixin:
username = ''
ip = ''
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
increase_login_failed_count(self.username, self.ip)
class AuthFailedError(Exception): class AuthFailedError(Exception):
def __init__(self, reason, error=None, username=None): username = ''
self.reason = reason msg = ''
self.error = error error = ''
self.username = username request = None
ip = ''
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
def as_data(self):
return {
'error': self.error,
'msg': self.msg,
}
class CredentialError(AuthFailedNeedLogMixin, AuthFailedNeedBlockMixin, AuthFailedError):
def __init__(self, error, username, ip, request):
super().__init__(error=error, username=username, ip=ip, request=request)
times_up = settings.SECURITY_LOGIN_LIMIT_COUNT
times_failed = get_login_failed_count(username, ip)
times_try = int(times_up) - int(times_failed)
block_time = settings.SECURITY_LOGIN_LIMIT_TIME
default_msg = invalid_login_msg.format(
times_try=times_try, block_time=block_time
)
if error == reason_password_failed:
self.msg = default_msg
else:
self.msg = reason_choices.get(error, default_msg)
class MFAFailedError(AuthFailedNeedLogMixin, AuthFailedError):
reason = reason_mfa_failed
error = 'mfa_failed'
msg = mfa_failed_msg
def __init__(self, username, request):
super().__init__(username=username, request=request)
class BlockLoginError(AuthFailedNeedBlockMixin, AuthFailedError):
error = 'block_login'
msg = block_login_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
def __init__(self, username, ip):
super().__init__(username=username, ip=ip)
class SessionEmptyError(AuthFailedError):
msg = session_empty_msg
error = 'session_empty_msg'
class MFARequiredError(AuthFailedError):
msg = mfa_required_msg
error = 'mfa_required_msg'
def as_data(self):
return {
'error': self.error,
'msg': self.msg,
'choices': ['otp'],
'url': reverse('api-auth:mfa-challenge')
}
class LoginConfirmRequiredError(AuthFailedError):
msg = login_confirm_required_msg
error = 'login_confirm_required_msg'
class LoginConfirmError(AuthFailedError):
msg = login_confirm_wait_msg
error = 'login_confirm_wait_msg'
def __init__(self, order_id, **kwargs):
self.order_id = order_id
super().__init__(**kwargs)
class MFARequiredError(Exception): def as_data(self):
reason = mfa_required return {
error = 'mfa_required' "error": self.error,
"msg": self.msg,
"order_id": self.order_id
}
class LoginConfirmRequiredError(Exception): class LoginConfirmWaitError(LoginConfirmError):
reason = login_confirm_required msg = login_confirm_wait_msg
error = 'login_confirm_required' error = 'login_confirm_wait_msg'
class LoginConfirmWaitError(Exception): class LoginConfirmRejectedError(LoginConfirmError):
reason = login_confirm_wait msg = login_confirm_rejected_msg
error = 'login_confirm_wait' error = 'login_confirm_rejected_msg'
class LoginConfirmRejectedError(Exception): class LoginConfirmOrderNotFound(LoginConfirmError):
reason = login_confirm_wait msg = login_confirm_order_not_found_msg
error = 'login_confirm_rejected' error = 'login_confirm_order_not_found_msg'
...@@ -9,53 +9,19 @@ from django.conf import settings ...@@ -9,53 +9,19 @@ from django.conf import settings
from users.utils import get_login_failed_count from users.utils import get_login_failed_count
class UserLoginForm(AuthenticationForm): class UserLoginForm(forms.Form):
username = forms.CharField(label=_('Username'), max_length=100) username = forms.CharField(label=_('Username'), max_length=100)
password = forms.CharField( password = forms.CharField(
label=_('Password'), widget=forms.PasswordInput, label=_('Password'), widget=forms.PasswordInput,
max_length=128, strip=False max_length=128, strip=False
) )
error_messages = {
'invalid_login': _(
"The username or password you entered is incorrect, "
"please enter it again."
),
'inactive': _("This account is inactive."),
'limit_login': _(
"You can also try {times_try} times "
"(The account will be temporarily locked for {block_time} minutes)"
),
'block_login': _(
"The account has been locked "
"(please contact admin to unlock it or try again after {} minutes)"
)
}
def confirm_login_allowed(self, user): def confirm_login_allowed(self, user):
if not user.is_staff: if not user.is_staff:
raise forms.ValidationError( raise forms.ValidationError(
self.error_messages['inactive'], self.error_messages['inactive'],
code='inactive',) code='inactive',
def get_limit_login_error_message(self, username, ip):
times_up = settings.SECURITY_LOGIN_LIMIT_COUNT
times_failed = get_login_failed_count(username, ip)
times_try = int(times_up) - int(times_failed)
block_time = settings.SECURITY_LOGIN_LIMIT_TIME
if times_try <= 0:
error_message = self.error_messages['block_login']
error_message = error_message.format(block_time)
else:
error_message = self.error_messages['limit_login']
error_message = error_message.format(
times_try=times_try, block_time=block_time,
) )
return error_message
def add_limit_login_error(self, username, ip):
error = self.get_limit_login_error_message(username, ip)
self.add_error('password', error)
class UserLoginCaptchaForm(UserLoginForm): class UserLoginCaptchaForm(UserLoginForm):
......
# -*- coding: utf-8 -*-
#
import time
from common.utils import get_object_or_none, get_request_ip, get_logger
from users.models import User
from users.utils import (
is_block_login, clean_failed_count, increase_login_failed_count
)
from . import errors
from .utils import check_user_valid
from .signals import post_auth_success, post_auth_failed
logger = get_logger(__name__)
class AuthMixin:
request = None
def get_user_from_session(self):
if self.request.session.is_empty():
raise errors.SessionEmptyError()
if self.request.user and not self.request.user.is_anonymous:
return self.request.user
user_id = self.request.session.get('user_id')
if not user_id:
user = None
else:
user = get_object_or_none(User, pk=user_id)
if not user:
raise errors.SessionEmptyError()
return user
def get_request_ip(self):
ip = ''
if hasattr(self.request, 'data'):
ip = self.request.data.get('remote_addr', '')
ip = ip or get_request_ip(self.request)
return ip
def check_is_block(self):
if hasattr(self.request, 'data'):
username = self.request.data.get("username")
else:
username = self.request.POST.get("username")
ip = self.get_request_ip()
if is_block_login(username, ip):
logger.warn('Ip was blocked' + ': ' + username + ':' + ip)
raise errors.BlockLoginError(username=username, ip=ip)
def check_user_auth(self):
request = self.request
self.check_is_block()
if hasattr(request, 'data'):
username = request.data.get('username', '')
password = request.data.get('password', '')
public_key = request.data.get('public_key', '')
else:
username = request.POST.get('username', '')
password = request.POST.get('password', '')
public_key = request.POST.get('public_key', '')
user, error = check_user_valid(
username=username, password=password,
public_key=public_key
)
ip = self.get_request_ip()
if not user:
raise errors.CredentialError(
username=username, error=error, ip=ip, request=request
)
clean_failed_count(username, ip)
request.session['auth_password'] = 1
request.session['user_id'] = str(user.id)
return user
def check_user_mfa_if_need(self, user):
if self.request.session.get('auth_mfa'):
return True
if not user.otp_enabled or not user.otp_secret_key:
return True
raise errors.MFARequiredError()
def check_user_mfa(self, code):
user = self.get_user_from_session()
ok = user.check_otp(code)
if ok:
self.request.session['auth_mfa'] = 1
self.request.session['auth_mfa_time'] = time.time()
self.request.session['auth_mfa_type'] = 'otp'
return
raise errors.MFAFailedError(username=user.username, request=self.request)
def check_user_login_confirm_if_need(self, user):
from orders.models import LoginConfirmOrder
confirm_setting = user.get_login_confirm_setting()
if self.request.session.get('auth_confirm') or not confirm_setting:
return
order = None
if self.request.session.get('auth_order_id'):
order_id = self.request.session['auth_order_id']
order = get_object_or_none(LoginConfirmOrder, pk=order_id)
if not order:
order = confirm_setting.create_confirm_order(self.request)
self.request.session['auth_order_id'] = str(order.id)
if order.status == "accepted":
return
elif order.status == "rejected":
raise errors.LoginConfirmRejectedError(order.id)
else:
raise errors.LoginConfirmWaitError(order.id)
def clear_auth_mark(self):
self.request.session['auth_password'] = ''
self.request.session['auth_mfa'] = ''
self.request.session['auth_confirm'] = ''
def send_auth_signal(self, success=True, user=None, username='', reason=''):
if success:
post_auth_success.send(
sender=self.__class__, user=user, request=self.request
)
else:
post_auth_failed.send(
sender=self.__class__, username=username,
request=self.request, reason=reason
)
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
from django.core.cache import cache from django.core.cache import cache
from rest_framework import serializers from rest_framework import serializers
from common.utils import get_object_or_none
from users.models import User from users.models import User
from .models import AccessKey, LoginConfirmSetting from .models import AccessKey, LoginConfirmSetting
...@@ -24,7 +25,12 @@ class OtpVerifySerializer(serializers.Serializer): ...@@ -24,7 +25,12 @@ class OtpVerifySerializer(serializers.Serializer):
code = serializers.CharField(max_length=6, min_length=6) code = serializers.CharField(max_length=6, min_length=6)
class BearerTokenMixin(serializers.Serializer): class BearerTokenSerializer(serializers.Serializer):
username = serializers.CharField(allow_null=True, required=False)
password = serializers.CharField(write_only=True, allow_null=True,
required=False)
public_key = serializers.CharField(write_only=True, allow_null=True,
required=False)
token = serializers.CharField(read_only=True) token = serializers.CharField(read_only=True)
keyword = serializers.SerializerMethodField() keyword = serializers.SerializerMethodField()
date_expired = serializers.DateTimeField(read_only=True) date_expired = serializers.DateTimeField(read_only=True)
...@@ -33,58 +39,35 @@ class BearerTokenMixin(serializers.Serializer): ...@@ -33,58 +39,35 @@ class BearerTokenMixin(serializers.Serializer):
def get_keyword(obj): def get_keyword(obj):
return 'Bearer' return 'Bearer'
def create_response(self, username): def create(self, validated_data):
request = self.context.get("request") request = self.context.get('request')
try: if request.user and not request.user.is_anonymous:
user = User.objects.get(username=username) user = request.user
except User.DoesNotExist: else:
raise serializers.ValidationError("username %s not exist" % username) user_id = request.session.get('user_id')
user = get_object_or_none(User, pk=user_id)
if not user:
raise serializers.ValidationError(
"user id {} not exist".format(user_id)
)
token, date_expired = user.create_bearer_token(request) token, date_expired = user.create_bearer_token(request)
instance = { instance = {
"username": username, "username": user.username,
"token": token, "token": token,
"date_expired": date_expired, "date_expired": date_expired,
} }
return instance return instance
def update(self, instance, validated_data):
pass
class BearerTokenSerializer(BearerTokenMixin, serializers.Serializer):
username = serializers.CharField()
password = serializers.CharField(write_only=True, allow_null=True,
required=False)
public_key = serializers.CharField(write_only=True, allow_null=True,
required=False)
def create(self, validated_data):
username = validated_data.get("username")
return self.create_response(username)
class MFAChallengeSerializer(BearerTokenMixin, serializers.Serializer): class MFAChallengeSerializer(serializers.Serializer):
req = serializers.CharField(write_only=True) auth_type = serializers.CharField(write_only=True, required=False, allow_blank=True)
auth_type = serializers.CharField(write_only=True)
code = serializers.CharField(write_only=True) code = serializers.CharField(write_only=True)
def validate_req(self, attr):
username = cache.get(attr)
if not username:
raise serializers.ValidationError("Not valid, may be expired")
self.context["username"] = username
def validate_code(self, code):
username = self.context["username"]
user = User.objects.get(username=username)
ok = user.check_otp(code)
if not ok:
msg = "Otp code not valid, may be expired"
raise serializers.ValidationError(msg)
def create(self, validated_data): def create(self, validated_data):
username = self.context["username"] pass
return self.create_response(username)
def update(self, instance, validated_data):
pass
class LoginConfirmSettingSerializer(serializers.ModelSerializer): class LoginConfirmSettingSerializer(serializers.ModelSerializer):
......
from rest_framework.request import Request
from django.http.request import QueryDict from django.http.request import QueryDict
from django.conf import settings from django.conf import settings
from django.dispatch import receiver from django.dispatch import receiver
from django.contrib.auth.signals import user_logged_out from django.contrib.auth.signals import user_logged_out
from django.utils import timezone
from django_auth_ldap.backend import populate_user from django_auth_ldap.backend import populate_user
from common.utils import get_request_ip
from .backends.openid import new_client from .backends.openid import new_client
from .backends.openid.signals import ( from .backends.openid.signals import (
post_create_openid_user, post_openid_login_success post_create_openid_user, post_openid_login_success
) )
from .tasks import write_login_log_async from .signals import post_auth_success
from .signals import post_auth_success, post_auth_failed
@receiver(user_logged_out) @receiver(user_logged_out)
...@@ -52,35 +48,4 @@ def on_ldap_create_user(sender, user, ldap_user, **kwargs): ...@@ -52,35 +48,4 @@ def on_ldap_create_user(sender, user, ldap_user, **kwargs):
user.save() user.save()
def generate_data(username, request):
user_agent = request.META.get('HTTP_USER_AGENT', '')
if isinstance(request, Request):
login_ip = request.data.get('remote_addr', None)
login_type = request.data.get('login_type', '')
else:
login_ip = get_request_ip(request)
login_type = 'W'
data = {
'username': username,
'ip': login_ip,
'type': login_type,
'user_agent': user_agent,
'datetime': timezone.now()
}
return data
@receiver(post_auth_success)
def on_user_auth_success(sender, user, request, **kwargs):
data = generate_data(user.username, request)
data.update({'mfa': int(user.otp_enabled), 'status': True})
write_login_log_async.delay(**data)
@receiver(post_auth_failed)
def on_user_auth_failed(sender, username, request, reason, **kwargs):
data = generate_data(username, request)
data.update({'reason': reason, 'status': False})
write_login_log_async.delay(**data)
...@@ -6,17 +6,8 @@ from ops.celery.decorator import register_as_period_task ...@@ -6,17 +6,8 @@ from ops.celery.decorator import register_as_period_task
from django.contrib.sessions.models import Session from django.contrib.sessions.models import Session
from django.utils import timezone from django.utils import timezone
from .utils import write_login_log
@shared_task
def write_login_log_async(*args, **kwargs):
write_login_log(*args, **kwargs)
@register_as_period_task(interval=3600*24) @register_as_period_task(interval=3600*24)
@shared_task @shared_task
def clean_django_sessions(): def clean_django_sessions():
Session.objects.filter(expire_date__lt=timezone.now()).delete() Session.objects.filter(expire_date__lt=timezone.now()).delete()
...@@ -37,7 +37,6 @@ ...@@ -37,7 +37,6 @@
<p> <p>
{% trans "Changes the world, starting with a little bit." %} {% trans "Changes the world, starting with a little bit." %}
</p> </p>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="ibox-content"> <div class="ibox-content">
...@@ -47,25 +46,29 @@ ...@@ -47,25 +46,29 @@
</div> </div>
<form class="m-t" role="form" method="post" action=""> <form class="m-t" role="form" method="post" action="">
{% csrf_token %} {% csrf_token %}
{% if form.non_field_errors %}
{% if block_login %} <div style="line-height: 17px;">
<p class="red-fonts">{% trans 'Log in frequently and try again later' %}</p>
{% elif password_expired %}
<p class="red-fonts">{% trans 'The user password has expired' %}</p>
{% elif form.errors %}
{% if 'captcha' in form.errors %}
<p class="red-fonts">{% trans 'Captcha invalid' %}</p>
{% else %}
<p class="red-fonts">{{ form.non_field_errors.as_text }}</p> <p class="red-fonts">{{ form.non_field_errors.as_text }}</p>
{% endif %} </div>
<p class="red-fonts">{{ form.errors.password.as_text }}</p> {% elif form.errors.captcha %}
<p class="red-fonts">{% trans 'Captcha invalid' %}</p>
{% endif %} {% endif %}
<div class="form-group"> <div class="form-group">
<input type="text" class="form-control" name="{{ form.username.html_name }}" placeholder="{% trans 'Username' %}" required="" value="{% if form.username.value %}{{ form.username.value }}{% endif %}"> <input type="text" class="form-control" name="{{ form.username.html_name }}" placeholder="{% trans 'Username' %}" required="" value="{% if form.username.value %}{{ form.username.value }}{% endif %}">
{% if form.errors.username %}
<div class="help-block field-error">
<p class="red-fonts">{{ form.errors.username.as_text }}</p>
</div>
{% endif %}
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="password" class="form-control" name="{{ form.password.html_name }}" placeholder="{% trans 'Password' %}" required=""> <input type="password" class="form-control" name="{{ form.password.html_name }}" placeholder="{% trans 'Password' %}" required="">
{% if form.errors.password %}
<div class="help-block field-error">
<p class="red-fonts">{{ form.errors.password.as_text }}</p>
</div>
{% endif %}
</div> </div>
<div> <div>
{{ form.captcha }} {{ form.captcha }}
......
...@@ -86,7 +86,7 @@ function doRequestAuth() { ...@@ -86,7 +86,7 @@ function doRequestAuth() {
window.location = successUrl; window.location = successUrl;
}, },
error: function (text, data) { error: function (text, data) {
if (data.status !== "pending") { if (data.error !== "login_confirm_wait") {
if (!errorMsgShow) { if (!errorMsgShow) {
infoMsgRef.hide(); infoMsgRef.hide();
errorMsgRef.show(); errorMsgRef.show();
...@@ -97,7 +97,7 @@ function doRequestAuth() { ...@@ -97,7 +97,7 @@ function doRequestAuth() {
clearInterval(checkInterval); clearInterval(checkInterval);
$(".copy-btn").attr('disabled', 'disabled') $(".copy-btn").attr('disabled', 'disabled')
} }
errorMsgRef.html(data.error) errorMsgRef.html(data.msg)
}, },
flash_message: false flash_message: false
}) })
......
...@@ -48,6 +48,13 @@ ...@@ -48,6 +48,13 @@
float: right; float: right;
} }
.red-fonts {
color: red;
}
.field-error {
text-align: left;
}
</style> </style>
</head> </head>
...@@ -69,30 +76,32 @@ ...@@ -69,30 +76,32 @@
<div style="margin-bottom: 10px"> <div style="margin-bottom: 10px">
<div> <div>
<div class="col-md-1"></div> <div class="col-md-1"></div>
<div class="contact-form col-md-10" style="margin-top: 10px;height: 35px"> <div class="contact-form col-md-10" style="margin-top: 20px;height: 35px">
<form id="contact-form" action="" method="post" role="form" novalidate="novalidate"> <form id="contact-form" action="" method="post" role="form" novalidate="novalidate">
{% csrf_token %} {% csrf_token %}
{% if form.non_field_errors %}
<div style="height: 70px;color: red;line-height: 17px;"> <div style="height: 70px;color: red;line-height: 17px;">
{% if block_login %}
<p class="red-fonts">{% trans 'Log in frequently and try again later' %}</p>
<p class="red-fonts">{{ form.errors.password.as_text }}</p>
{% elif password_expired %}
<p class="red-fonts">{% trans 'The user password has expired' %}</p>
{% elif form.errors %}
{% if 'captcha' in form.errors %}
<p class="red-fonts">{% trans 'Captcha invalid' %}</p>
{% else %}
<p class="red-fonts">{{ form.non_field_errors.as_text }}</p> <p class="red-fonts">{{ form.non_field_errors.as_text }}</p>
{% endif %}
<p class="red-fonts">{{ form.errors.password.as_text }}</p>
{% endif %}
</div> </div>
{% elif form.errors.captcha %}
<p class="red-fonts">{% trans 'Captcha invalid' %}</p>
{% endif %}
<div class="form-group"> <div class="form-group">
<input type="text" class="form-control" name="{{ form.username.html_name }}" placeholder="{% trans 'Username' %}" required="" value="{% if form.username.value %}{{ form.username.value }}{% endif %}" style="height: 35px"> <input type="text" class="form-control" name="{{ form.username.html_name }}" placeholder="{% trans 'Username' %}" required="" value="{% if form.username.value %}{{ form.username.value }}{% endif %}" style="height: 35px">
{% if form.errors.username %}
<div class="help-block field-error">
<p class="red-fonts">{{ form.errors.username.as_text }}</p>
</div>
{% endif %}
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="password" class="form-control" name="{{ form.password.html_name }}" placeholder="{% trans 'Password' %}" required=""> <input type="password" class="form-control" name="{{ form.password.html_name }}" placeholder="{% trans 'Password' %}" required="">
{% if form.errors.password %}
<div class="help-block field-error">
<p class="red-fonts">{{ form.errors.password.as_text }}</p>
</div>
{% endif %}
</div> </div>
<div class="form-group" style="height: 50px;margin-bottom: 0;font-size: 13px"> <div class="form-group" style="height: 50px;margin-bottom: 0;font-size: 13px">
{{ form.captcha }} {{ form.captcha }}
......
...@@ -12,12 +12,11 @@ router.register('access-keys', api.AccessKeyViewSet, 'access-key') ...@@ -12,12 +12,11 @@ router.register('access-keys', api.AccessKeyViewSet, 'access-key')
urlpatterns = [ urlpatterns = [
# path('token/', api.UserToken.as_view(), name='user-token'), # path('token/', api.UserToken.as_view(), name='user-token'),
path('auth/', api.UserAuthApi.as_view(), name='user-auth'), path('auth/', api.TokenCreateApi.as_view(), name='user-auth'),
path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'), path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'),
path('mfa/challenge/', api.MFAChallengeApi.as_view(), name='mfa-challenge'), path('mfa/challenge/', api.MFAChallengeApi.as_view(), name='mfa-challenge'),
path('connection-token/', path('connection-token/',
api.UserConnectionTokenApi.as_view(), name='connection-token'), api.UserConnectionTokenApi.as_view(), name='connection-token'),
path('otp/auth/', api.UserOtpAuthApi.as_view(), name='user-otp-auth'),
path('otp/verify/', api.UserOtpVerifyApi.as_view(), name='user-otp-verify'), path('otp/verify/', api.UserOtpVerifyApi.as_view(), name='user-otp-verify'),
path('order/auth/', api.UserOrderAcceptAuthApi.as_view(), name='user-order-auth'), path('order/auth/', api.UserOrderAcceptAuthApi.as_view(), name='user-order-auth'),
path('login-confirm-settings/<uuid:user_id>/', api.LoginConfirmSettingUpdateApi.as_view(), name='login-confirm-setting-update') path('login-confirm-settings/<uuid:user_id>/', api.LoginConfirmSettingUpdateApi.as_view(), name='login-confirm-setting-update')
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.utils.translation import ugettext as _, ugettext_lazy as __ from django.utils.translation import ugettext as _
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from django.utils import timezone
from common.utils import ( from common.utils import (
get_ip_city, get_object_or_none, validate_ip, get_request_ip get_ip_city, get_object_or_none, validate_ip
) )
from users.models import User from users.models import User
from . import errors from . import errors
def write_login_log(*args, **kwargs):
from audits.models import UserLoginLog
default_city = _("Unknown")
ip = kwargs.get('ip') or ''
if not (ip and validate_ip(ip)):
ip = ip[:15]
city = default_city
else:
city = get_ip_city(ip) or default_city
kwargs.update({'ip': ip, 'city': city})
UserLoginLog.objects.create(**kwargs)
def check_user_valid(**kwargs): def check_user_valid(**kwargs):
password = kwargs.pop('password', None) password = kwargs.pop('password', None)
public_key = kwargs.pop('public_key', None) public_key = kwargs.pop('public_key', None)
email = kwargs.pop('email', None) email = kwargs.pop('email', None)
username = kwargs.pop('username', None) username = kwargs.pop('username', None)
request = kwargs.get('request')
if username: if username:
user = get_object_or_none(User, username=username) user = get_object_or_none(User, username=username)
...@@ -38,21 +25,25 @@ def check_user_valid(**kwargs): ...@@ -38,21 +25,25 @@ def check_user_valid(**kwargs):
user = None user = None
if user is None: if user is None:
return None, errors.user_not_exist return None, errors.reason_user_not_exist
elif not user.is_valid: elif user.is_expired:
return None, errors.user_invalid return None, errors.reason_password_expired
elif not user.is_active:
return None, errors.reason_user_inactive
elif user.password_has_expired: elif user.password_has_expired:
return None, errors.password_expired return None, errors.reason_password_expired
if password and authenticate(username=username, password=password): if password:
user = authenticate(request, username=username, password=password)
if user:
return user, '' return user, ''
if public_key and user.public_key: if public_key and user.public_key:
public_key_saved = user.public_key.split() public_key_saved = user.public_key.split()
if len(public_key_saved) == 1: if len(public_key_saved) == 1:
if public_key == public_key_saved[0]: public_key_saved = public_key_saved[0]
return user, '' else:
elif len(public_key_saved) > 1: public_key_saved = public_key_saved[1]
if public_key == public_key_saved[1]: if public_key == public_key_saved:
return user, '' return user, ''
return None, errors.password_failed return None, errors.reason_password_failed
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from .login import * from .login import *
from .mfa import *
...@@ -16,22 +16,20 @@ from django.views.decorators.debug import sensitive_post_parameters ...@@ -16,22 +16,20 @@ from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic.base import TemplateView, RedirectView from django.views.generic.base import TemplateView, RedirectView
from django.views.generic.edit import FormView from django.views.generic.edit import FormView
from django.conf import settings from django.conf import settings
from django.urls import reverse_lazy
from common.utils import get_request_ip, get_object_or_none from common.utils import get_request_ip, get_object_or_none
from users.models import User from users.models import User
from users.utils import ( from users.utils import (
check_otp_code, is_block_login, clean_failed_count, get_user_or_tmp_user, get_user_or_tmp_user, increase_login_failed_count,
set_tmp_user_to_cache, increase_login_failed_count,
redirect_user_first_login_or_index redirect_user_first_login_or_index
) )
from ..models import LoginConfirmSetting
from ..signals import post_auth_success, post_auth_failed from ..signals import post_auth_success, post_auth_failed
from .. import forms from .. import forms, mixins, errors
from .. import errors
__all__ = [ __all__ = [
'UserLoginView', 'UserLoginOtpView', 'UserLogoutView', 'UserLoginView', 'UserLogoutView',
'UserLoginGuardView', 'UserLoginWaitConfirmView', 'UserLoginGuardView', 'UserLoginWaitConfirmView',
] ]
...@@ -39,10 +37,11 @@ __all__ = [ ...@@ -39,10 +37,11 @@ __all__ = [
@method_decorator(sensitive_post_parameters(), name='dispatch') @method_decorator(sensitive_post_parameters(), name='dispatch')
@method_decorator(csrf_protect, name='dispatch') @method_decorator(csrf_protect, name='dispatch')
@method_decorator(never_cache, name='dispatch') @method_decorator(never_cache, name='dispatch')
class UserLoginView(FormView): class UserLoginView(mixins.AuthMixin, FormView):
form_class = forms.UserLoginForm form_class = forms.UserLoginForm
form_class_captcha = forms.UserLoginCaptchaForm form_class_captcha = forms.UserLoginCaptchaForm
key_prefix_captcha = "_LOGIN_INVALID_{}" key_prefix_captcha = "_LOGIN_INVALID_{}"
redirect_field_name = 'next'
def get_template_names(self): def get_template_names(self):
template_name = 'authentication/login.html' template_name = 'authentication/login.html'
...@@ -69,54 +68,25 @@ class UserLoginView(FormView): ...@@ -69,54 +68,25 @@ class UserLoginView(FormView):
request.session.set_test_cookie() request.session.set_test_cookie()
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
# limit login authentication
ip = get_request_ip(request)
username = self.request.POST.get('username')
if is_block_login(username, ip):
return self.render_to_response(self.get_context_data(block_login=True))
return super().post(request, *args, **kwargs)
def form_valid(self, form): def form_valid(self, form):
if not self.request.session.test_cookie_worked(): if not self.request.session.test_cookie_worked():
return HttpResponse(_("Please enable cookies and try again.")) return HttpResponse(_("Please enable cookies and try again."))
user = form.get_user() try:
# user password expired self.check_user_auth()
if user.password_has_expired: except errors.AuthFailedError as e:
reason = errors.password_expired form.add_error(None, e.msg)
self.send_auth_signal(success=False, username=user.username, reason=reason) ip = self.get_request_ip()
return self.render_to_response(self.get_context_data(password_expired=True))
set_tmp_user_to_cache(self.request, user)
username = form.cleaned_data.get('username')
ip = get_request_ip(self.request)
# 登陆成功,清除缓存计数
clean_failed_count(username, ip)
self.request.session['auth_password'] = '1'
return self.redirect_to_guard_view()
def form_invalid(self, form):
# write login failed log
username = form.cleaned_data.get('username')
exist = User.objects.filter(username=username).first()
reason = errors.password_failed if exist else errors.user_not_exist
# limit user login failed count
ip = get_request_ip(self.request)
increase_login_failed_count(username, ip)
form.add_limit_login_error(username, ip)
# show captcha
cache.set(self.key_prefix_captcha.format(ip), 1, 3600) cache.set(self.key_prefix_captcha.format(ip), 1, 3600)
self.send_auth_signal(success=False, username=username, reason=reason) context = self.get_context_data(form=form)
return self.render_to_response(context)
old_form = form return self.redirect_to_guard_view()
form = self.form_class_captcha(data=form.data)
form._errors = old_form.errors
return super().form_invalid(form)
@staticmethod def redirect_to_guard_view(self):
def redirect_to_guard_view(): guard_url = reverse('authentication:login-guard')
continue_url = reverse('authentication:login-guard') args = self.request.META.get('QUERY_STRING', '')
return redirect(continue_url) if args and self.query_string:
guard_url = "%s?%s" % (guard_url, args)
return redirect(guard_url)
def get_form_class(self): def get_form_class(self):
ip = get_request_ip(self.request) ip = get_request_ip(self.request)
...@@ -134,58 +104,34 @@ class UserLoginView(FormView): ...@@ -134,58 +104,34 @@ class UserLoginView(FormView):
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
class UserLoginOtpView(FormView): class UserLoginGuardView(mixins.AuthMixin, RedirectView):
template_name = 'authentication/login_otp.html'
form_class = forms.UserCheckOtpCodeForm
redirect_field_name = 'next'
def form_valid(self, form):
user = get_user_or_tmp_user(self.request)
otp_code = form.cleaned_data.get('otp_code')
otp_secret_key = user.otp_secret_key
if check_otp_code(otp_secret_key, otp_code):
self.request.session['auth_otp'] = '1'
return UserLoginView.redirect_to_guard_view()
else:
self.send_auth_signal(
success=False, username=user.username,
reason=errors.mfa_failed
)
form.add_error(
'otp_code', _('MFA code invalid, or ntp sync server time')
)
return super().form_invalid(form)
def send_auth_signal(self, success=True, user=None, username='', reason=''):
if success:
post_auth_success.send(sender=self.__class__, user=user, request=self.request)
else:
post_auth_failed.send(
sender=self.__class__, username=username,
request=self.request, reason=reason
)
class UserLoginGuardView(RedirectView):
redirect_field_name = 'next' redirect_field_name = 'next'
login_url = reverse_lazy('authentication:login')
login_otp_url = reverse_lazy('authentication:login-otp')
login_confirm_url = reverse_lazy('authentication:login-wait-confirm')
def format_redirect_url(self, url):
args = self.request.META.get('QUERY_STRING', '')
if args and self.query_string:
url = "%s?%s" % (url, args)
return url
def get_redirect_url(self, *args, **kwargs): def get_redirect_url(self, *args, **kwargs):
if not self.request.session.get('auth_password'): if not self.request.session.get('auth_password'):
return reverse('authentication:login') return self.format_redirect_url(self.login_url)
user = self.get_user_from_session()
user = get_user_or_tmp_user(self.request)
# 启用并设置了otp # 启用并设置了otp
if user.otp_enabled and user.otp_secret_key and \ if user.otp_enabled and user.otp_secret_key and \
not self.request.session.get('auth_otp'): not self.request.session.get('auth_mfa'):
return reverse('authentication:login-otp') return self.format_redirect_url(self.login_otp_url)
confirm_setting = user.get_login_confirm_setting() confirm_setting = user.get_login_confirm_setting()
if confirm_setting and not self.request.session.get('auth_confirm'): if confirm_setting and not self.request.session.get('auth_confirm'):
order = confirm_setting.create_confirm_order(self.request) order = confirm_setting.create_confirm_order(self.request)
self.request.session['auth_order_id'] = str(order.id) self.request.session['auth_order_id'] = str(order.id)
url = reverse('authentication:login-wait-confirm') url = self.format_redirect_url(self.login_confirm_url)
return url return url
self.login_success(user) self.login_success(user)
self.clear_auth_mark()
# 启用但是没有设置otp # 启用但是没有设置otp
if user.otp_enabled and not user.otp_secret_key: if user.otp_enabled and not user.otp_secret_key:
# 1,2,mfa_setting & F # 1,2,mfa_setting & F
......
# -*- coding: utf-8 -*-
#
from __future__ import unicode_literals
from django.views.generic.edit import FormView
from .. import forms, errors, mixins
from .utils import redirect_to_guard_view
__all__ = ['UserLoginOtpView']
class UserLoginOtpView(mixins.AuthMixin, FormView):
template_name = 'authentication/login_otp.html'
form_class = forms.UserCheckOtpCodeForm
redirect_field_name = 'next'
def form_valid(self, form):
otp_code = form.cleaned_data.get('otp_code')
try:
self.check_user_mfa(otp_code)
return redirect_to_guard_view()
except errors.MFAFailedError as e:
form.add_error('otp_code', e.reason)
return super().form_invalid(form)
# -*- coding: utf-8 -*-
#
from django.shortcuts import reverse, redirect
def redirect_to_guard_view():
continue_url = reverse('authentication:login-guard')
return redirect(continue_url)
...@@ -153,6 +153,14 @@ def get_request_ip(request): ...@@ -153,6 +153,14 @@ def get_request_ip(request):
return login_ip return login_ip
def get_request_ip_or_data(request):
ip = ''
if hasattr(request, 'data'):
ip = request.data.get('remote_addr', '')
ip = ip or get_request_ip(request)
return ip
def validate_ip(ip): def validate_ip(ip):
try: try:
ipaddress.ip_address(ip) ipaddress.ip_address(ip)
......
This diff is collapsed.
...@@ -100,16 +100,6 @@ ...@@ -100,16 +100,6 @@
<link rel="stylesheet" type="text/css" href={% static "css/plugins/daterangepicker/daterangepicker.css" %} /> <link rel="stylesheet" type="text/css" href={% static "css/plugins/daterangepicker/daterangepicker.css" %} />
<script> <script>
var dateOptions = {
singleDatePicker: true,
showDropdowns: true,
timePicker: true,
timePicker24Hour: true,
autoApply: true,
locale: {
format: 'YYYY-MM-DD HH:mm'
}
};
var api_action = "{{ api_action }}"; var api_action = "{{ api_action }}";
$(document).ready(function () { $(document).ready(function () {
...@@ -119,8 +109,8 @@ $(document).ready(function () { ...@@ -119,8 +109,8 @@ $(document).ready(function () {
nodesSelect2Init(".nodes-select2"); nodesSelect2Init(".nodes-select2");
usersSelect2Init(".users-select2"); usersSelect2Init(".users-select2");
$('#date_start').daterangepicker(dateOptions); initDateRangePicker('#date_start');
$('#date_expired').daterangepicker(dateOptions); initDateRangePicker('#date_expired');
$("#id_assets").parent().find(".select2-selection").on('click', function (e) { $("#id_assets").parent().find(".select2-selection").on('click', function (e) {
if ($(e.target).attr('class') !== 'select2-selection__choice__remove'){ if ($(e.target).attr('class') !== 'select2-selection__choice__remove'){
......
...@@ -115,8 +115,8 @@ $(document).ready(function () { ...@@ -115,8 +115,8 @@ $(document).ready(function () {
closeOnSelect: false closeOnSelect: false
}); });
usersSelect2Init('.users-select2'); usersSelect2Init('.users-select2');
$('#date_start').daterangepicker(dateOptions); initDateRangePicker('#date_start');
$('#date_expired').daterangepicker(dateOptions); initDateRangePicker('#date_expired');
}) })
.on("submit", "form", function (evt) { .on("submit", "form", function (evt) {
evt.preventDefault(); evt.preventDefault();
......
...@@ -1289,3 +1289,31 @@ function showCeleryTaskLog(taskId) { ...@@ -1289,3 +1289,31 @@ function showCeleryTaskLog(taskId) {
var url = '/ops/celery/task/taskId/log/'.replace('taskId', taskId); var url = '/ops/celery/task/taskId/log/'.replace('taskId', taskId);
window.open(url, '', 'width=900,height=600') window.open(url, '', 'width=900,height=600')
} }
function initDateRangePicker(selector, options) {
if (!options) {
options = {}
}
var zhLocale = {
format: 'YYYY-MM-DD HH:mm',
separator: ' ~ ',
applyLabel: "应用",
cancelLabel: "取消",
resetLabel: "重置",
daysOfWeek: ["日", "一", "二", "三", "四", "五", "六"],//汉化处理
monthNames: ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"],
};
var defaultOption = {
singleDatePicker: true,
showDropdowns: true,
timePicker: true,
timePicker24Hour: true,
autoApply: true,
};
var userLang = navigator.language || navigator.userLanguage;;
if (userLang.indexOf('zh') !== -1) {
defaultOption.locale = zhLocale;
}
options = Object.assign(defaultOption, options);
return $(selector).daterangepicker(options);
}
...@@ -62,10 +62,6 @@ class AuthMixin: ...@@ -62,10 +62,6 @@ class AuthMixin:
def can_use_ssh_key_login(self): def can_use_ssh_key_login(self):
return settings.TERMINAL_PUBLIC_KEY_AUTH return settings.TERMINAL_PUBLIC_KEY_AUTH
def check_otp(self, code):
from ..utils import check_otp_code
return check_otp_code(self.otp_secret_key, code)
def is_public_key_valid(self): def is_public_key_valid(self):
""" """
Check if the user's ssh public key is valid. Check if the user's ssh public key is valid.
...@@ -362,6 +358,10 @@ class MFAMixin: ...@@ -362,6 +358,10 @@ class MFAMixin:
self.otp_level = 0 self.otp_level = 0
self.otp_secret_key = None self.otp_secret_key = None
def check_otp(self, code):
from ..utils import check_otp_code
return check_otp_code(self.otp_secret_key, code)
class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
SOURCE_LOCAL = 'local' SOURCE_LOCAL = 'local'
......
...@@ -56,6 +56,7 @@ ...@@ -56,6 +56,7 @@
{% endblock %} {% endblock %}
{% block custom_foot_js %} {% block custom_foot_js %}
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script> <script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script>
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.zh-CN.min.js' %}"></script>
<script type="text/javascript" src='{% static "js/plugins/daterangepicker/moment.min.js" %}'></script> <script type="text/javascript" src='{% static "js/plugins/daterangepicker/moment.min.js" %}'></script>
<script type="text/javascript" src='{% static "js/plugins/daterangepicker/daterangepicker.min.js" %}'></script> <script type="text/javascript" src='{% static "js/plugins/daterangepicker/daterangepicker.min.js" %}'></script>
<link rel="stylesheet" type="text/css" href={% static "css/plugins/daterangepicker/daterangepicker.css" %} /> <link rel="stylesheet" type="text/css" href={% static "css/plugins/daterangepicker/daterangepicker.css" %} />
...@@ -72,19 +73,9 @@ ...@@ -72,19 +73,9 @@
$(groups_id).closest('.form-group').removeClass('hidden'); $(groups_id).closest('.form-group').removeClass('hidden');
}} }}
var dateOptions = {
singleDatePicker: true,
showDropdowns: true,
timePicker: true,
timePicker24Hour: true,
autoApply: true,
locale: {
format: 'YYYY-MM-DD HH:mm'
}
};
$(document).ready(function () { $(document).ready(function () {
$('.select2').select2(); $('.select2').select2();
$('#id_date_expired').daterangepicker(dateOptions); initDateRangePicker('#id_date_expired');
var mfa_radio = $('#id_otp_level'); var mfa_radio = $('#id_otp_level');
mfa_radio.addClass("form-inline"); mfa_radio.addClass("form-inline");
mfa_radio.children().css("margin-right","15px"); mfa_radio.children().css("margin-right","15px");
......
...@@ -212,7 +212,7 @@ ...@@ -212,7 +212,7 @@
</table> </table>
</div> </div>
</div> </div>
{% if user_object.is_current_org_admin or user_object.is_superuser %} {% if user.is_current_org_admin or user.is_superuser %}
<div class="panel panel-info"> <div class="panel panel-info">
<div class="panel-heading"> <div class="panel-heading">
<i class="fa fa-info-circle"></i> {% trans 'User group' %} <i class="fa fa-info-circle"></i> {% trans 'User group' %}
......
...@@ -20,9 +20,6 @@ router.register(r'users-groups-relations', api.UserUserGroupRelationViewSet, 'us ...@@ -20,9 +20,6 @@ router.register(r'users-groups-relations', api.UserUserGroupRelationViewSet, 'us
urlpatterns = [ urlpatterns = [
path('connection-token/', auth_api.UserConnectionTokenApi.as_view(), path('connection-token/', auth_api.UserConnectionTokenApi.as_view(),
name='connection-token'), name='connection-token'),
path('auth/', auth_api.UserAuthApi.as_view(), name='user-auth'),
path('otp/auth/', auth_api.UserOtpAuthApi.as_view(), name='user-otp-auth'),
path('profile/', api.UserProfileApi.as_view(), name='user-profile'), path('profile/', api.UserProfileApi.as_view(), name='user-profile'),
path('otp/reset/', api.UserResetOTPApi.as_view(), name='my-otp-reset'), path('otp/reset/', api.UserResetOTPApi.as_view(), name='my-otp-reset'),
path('users/<uuid:pk>/otp/reset/', api.UserResetOTPApi.as_view(), name='user-reset-otp'), path('users/<uuid:pk>/otp/reset/', api.UserResetOTPApi.as_view(), name='user-reset-otp'),
......
...@@ -218,9 +218,11 @@ def set_tmp_user_to_cache(request, user, ttl=3600): ...@@ -218,9 +218,11 @@ def set_tmp_user_to_cache(request, user, ttl=3600):
def redirect_user_first_login_or_index(request, redirect_field_name): def redirect_user_first_login_or_index(request, redirect_field_name):
if request.user.is_first_login: if request.user.is_first_login:
return reverse('users:user-first-login') return reverse('users:user-first-login')
return request.POST.get( url_in_post = request.POST.get(redirect_field_name)
redirect_field_name, if url_in_post:
request.GET.get(redirect_field_name, reverse('index'))) return url_in_post
url_in_get = request.GET.get(redirect_field_name, reverse('index'))
return url_in_get
def generate_otp_uri(request, issuer="Jumpserver"): def generate_otp_uri(request, issuer="Jumpserver"):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment