diff --git a/apps/authentication/api/__init__.py b/apps/authentication/api/__init__.py new file mode 100644 index 000000000..0e8a44178 --- /dev/null +++ b/apps/authentication/api/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# + +from .auth import * diff --git a/apps/authentication/api/auth.py b/apps/authentication/api/auth.py new file mode 100644 index 000000000..0c7c1a16e --- /dev/null +++ b/apps/authentication/api/auth.py @@ -0,0 +1,200 @@ +# -*- coding: utf-8 -*- +# + +import uuid + +from django.core.cache import cache +from django.urls import reverse +from django.shortcuts import get_object_or_404 +from django.utils.translation import ugettext as _ + +from rest_framework.permissions import AllowAny +from rest_framework.response import Response +from rest_framework.views import APIView + +from common.utils import get_logger, get_request_ip +from common.permissions import IsOrgAdminOrAppUser +from orgs.mixins import RootOrgViewMixin +from authentication.signals import post_auth_success, post_auth_failed +from users.serializers import UserSerializer +from users.models import User, LoginLog +from users.utils import ( + check_user_valid, check_otp_code, increase_login_failed_count, + is_block_login, clean_failed_count +) +from users.hands import Asset, SystemUser + + +logger = get_logger(__name__) + + +class UserAuthApi(RootOrgViewMixin, APIView): + permission_classes = (AllowAny,) + serializer_class = UserSerializer + + 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', '') + exist = User.objects.filter(username=username).first() + reason = LoginLog.REASON_PASSWORD if exist else LoginLog.REASON_NOT_EXIST + self.send_auth_signal(success=False, username=username, reason=reason) + increase_login_failed_count(username, ip) + return Response({'msg': msg}, status=401) + + if user.password_has_expired: + self.send_auth_signal( + success=False, username=username, + reason=LoginLog.REASON_PASSWORD_EXPIRED + ) + msg = _("The user {} password has expired, please update.".format( + user.username)) + logger.info(msg) + 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 = user.create_bearer_token(request) + return Response( + {'token': token, 'user': self.serializer_class(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.serializer_class(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): + permission_classes = (IsOrgAdminOrAppUser,) + + def post(self, request): + user_id = request.data.get('user', '') + asset_id = request.data.get('asset', '') + system_user_id = request.data.get('system_user', '') + token = str(uuid.uuid4()) + user = get_object_or_404(User, id=user_id) + asset = get_object_or_404(Asset, id=asset_id) + system_user = get_object_or_404(SystemUser, id=system_user_id) + value = { + 'user': user_id, + 'username': user.username, + 'asset': asset_id, + 'hostname': asset.hostname, + 'system_user': system_user_id, + 'system_user_name': system_user.name + } + cache.set(token, value, timeout=20) + return Response({"token": token}, status=201) + + def get(self, request): + token = request.query_params.get('token') + user_only = request.query_params.get('user-only', None) + value = cache.get(token, None) + + if not value: + return Response('', status=404) + + if not user_only: + return Response(value) + else: + return Response({'user': value['user']}) + + def get_permissions(self): + if self.request.query_params.get('user-only', None): + self.permission_classes = (AllowAny,) + return super().get_permissions() + + +class UserToken(APIView): + permission_classes = (AllowAny,) + + def post(self, request): + if not request.user.is_authenticated: + username = request.data.get('username', '') + email = request.data.get('email', '') + password = request.data.get('password', '') + public_key = request.data.get('public_key', '') + + user, msg = check_user_valid( + username=username, email=email, + password=password, public_key=public_key) + else: + user = request.user + msg = None + if user: + token = user.create_bearer_token(request) + return Response({'Token': token, 'Keyword': 'Bearer'}, status=200) + else: + return Response({'error': msg}, status=406) + + +class UserOtpAuthApi(RootOrgViewMixin, APIView): + permission_classes = (AllowAny,) + serializer_class = UserSerializer + + 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=LoginLog.REASON_MFA) + return Response({'msg': _('MFA certification failed')}, status=401) + self.send_auth_signal(success=True, user=user) + token = user.create_bearer_token(request) + data = {'token': token, 'user': self.serializer_class(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 + ) diff --git a/apps/users/authentication.py b/apps/authentication/authentication.py similarity index 99% rename from apps/users/authentication.py rename to apps/authentication/authentication.py index 5faa7bb60..eab168a07 100644 --- a/apps/users/authentication.py +++ b/apps/authentication/authentication.py @@ -14,7 +14,7 @@ from rest_framework import authentication, exceptions from rest_framework.authentication import CSRFCheck from common.utils import get_object_or_none, make_signature, http_to_unixtime -from .models import User, AccessKey, PrivateToken +from users.models import User, AccessKey, PrivateToken def get_request_date_header(request): diff --git a/apps/authentication/urls/api_urls.py b/apps/authentication/urls/api_urls.py index 8b1378917..f87f04613 100644 --- a/apps/authentication/urls/api_urls.py +++ b/apps/authentication/urls/api_urls.py @@ -1 +1,19 @@ +# coding:utf-8 +# +from __future__ import absolute_import + +from django.urls import path + +from .. import api + +app_name = 'authentication' + + +urlpatterns = [ + # path('token/', api.UserToken.as_view(), name='user-token'), + path('auth/', api.UserAuthApi.as_view(), name='user-auth'), + path('connection-token/', + api.UserConnectionTokenApi.as_view(), name='connection-token'), + path('otp/auth/', api.UserOtpAuthApi.as_view(), name='user-otp-auth'), +] diff --git a/apps/authentication/urls/view_urls.py b/apps/authentication/urls/view_urls.py index 9e3aae6d3..2b68a6f71 100644 --- a/apps/authentication/urls/view_urls.py +++ b/apps/authentication/urls/view_urls.py @@ -2,6 +2,7 @@ # from django.urls import path + from .. import views app_name = 'authentication' @@ -9,6 +10,11 @@ app_name = 'authentication' urlpatterns = [ # openid path('openid/login/', views.OpenIDLoginView.as_view(), name='openid-login'), - path('openid/login/complete/', views.OpenIDLoginCompleteView.as_view(), - name='openid-login-complete'), + path('openid/login/complete/', + views.OpenIDLoginCompleteView.as_view(), name='openid-login-complete'), + + # login + path('login/', views.UserLoginView.as_view(), name='login'), + path('login/otp/', views.UserLoginOtpView.as_view(), name='login-otp'), + path('logout/', views.UserLogoutView.as_view(), name='logout'), ] diff --git a/apps/authentication/views/__init__.py b/apps/authentication/views/__init__.py index 1e55af137..b2659cbd7 100644 --- a/apps/authentication/views/__init__.py +++ b/apps/authentication/views/__init__.py @@ -2,3 +2,4 @@ # from .openid import * +from .login import * diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index ba37e3a60..815c010ce 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -1,15 +1,11 @@ # ~*~ coding: utf-8 ~*~ +# from __future__ import unicode_literals import os from django.core.cache import cache -from django.shortcuts import render -from django.utils import timezone from django.contrib.auth import login as auth_login, logout as auth_logout -from django.contrib.auth.mixins import LoginRequiredMixin -from django.views.generic import ListView -from django.core.files.storage import default_storage -from django.http import HttpResponseRedirect, HttpResponse +from django.http import HttpResponse from django.shortcuts import reverse, redirect from django.utils.decorators import method_decorator from django.utils.translation import ugettext as _ @@ -19,23 +15,20 @@ from django.views.decorators.debug import sensitive_post_parameters from django.views.generic.base import TemplateView from django.views.generic.edit import FormView from django.conf import settings -from formtools.wizard.views import SessionWizardView -from common.utils import get_object_or_none, get_request_ip +from common.utils import get_request_ip from authentication.signals import post_auth_success, post_auth_failed -from users.models import User, LoginLog -from users.utils import send_reset_password_mail, check_otp_code, \ - redirect_user_first_login_or_index, get_user_or_tmp_user, \ - set_tmp_user_to_cache, get_password_check_rules, check_password_rules, \ - is_block_login, increase_login_failed_count, clean_failed_count from users import forms +from users.models import User, LoginLog +from users.utils import ( + check_otp_code, is_block_login, clean_failed_count, get_user_or_tmp_user, + set_tmp_user_to_cache, increase_login_failed_count, + redirect_user_first_login_or_index, +) __all__ = [ 'UserLoginView', 'UserLoginOtpView', 'UserLogoutView', - 'UserForgotPasswordView', 'UserForgotPasswordSendmailSuccessView', - 'UserResetPasswordView', 'UserResetPasswordSuccessView', - 'UserFirstLoginView', 'LoginLogListView' ] @@ -122,7 +115,7 @@ class UserLoginView(FormView): if user.otp_enabled and user.otp_secret_key: # 1,2,mfa_setting & T - return reverse('users:login-otp') + return reverse('authentication:login-otp') elif user.otp_enabled and not user.otp_secret_key: # 1,2,mfa_setting & F return reverse('users:user-otp-enable-authentication') @@ -169,7 +162,9 @@ class UserLoginOtpView(FormView): success=False, username=user.username, reason=LoginLog.REASON_MFA ) - form.add_error('otp_code', _('MFA code invalid, or ntp sync server time')) + form.add_error( + 'otp_code', _('MFA code invalid, or ntp sync server time') + ) return super().form_invalid(form) def get_success_url(self): @@ -202,7 +197,7 @@ class UserLogoutView(TemplateView): 'title': _('Logout success'), 'messages': _('Logout success, return login page'), 'interval': 1, - 'redirect_url': reverse('users:login'), + 'redirect_url': reverse('authentication:login'), 'auto_redirect': True, } kwargs.update(context) diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index 22e660bb1..4c0dac0fa 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -135,7 +135,7 @@ TEMPLATES = [ # WSGI_APPLICATION = 'jumpserver.wsgi.applications' LOGIN_REDIRECT_URL = reverse_lazy('index') -LOGIN_URL = reverse_lazy('users:login') +LOGIN_URL = reverse_lazy('authentication:login') SESSION_COOKIE_DOMAIN = CONFIG.SESSION_COOKIE_DOMAIN CSRF_COOKIE_DOMAIN = CONFIG.CSRF_COOKIE_DOMAIN @@ -343,10 +343,10 @@ REST_FRAMEWORK = { ), 'DEFAULT_AUTHENTICATION_CLASSES': ( # 'rest_framework.authentication.BasicAuthentication', - 'users.authentication.AccessKeyAuthentication', - 'users.authentication.AccessTokenAuthentication', - 'users.authentication.PrivateTokenAuthentication', - 'users.authentication.SessionAuthentication', + 'authentication.authentication.AccessKeyAuthentication', + 'authentication.authentication.AccessTokenAuthentication', + 'authentication.authentication.PrivateTokenAuthentication', + 'authentication.authentication.SessionAuthentication', ), 'DEFAULT_FILTER_BACKENDS': ( 'django_filters.rest_framework.DjangoFilterBackend', diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index bdae17a44..8cc9c6eb4 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -21,6 +21,7 @@ api_v1_patterns = [ path('audits/v1/', include('audits.urls.api_urls', namespace='api-audits')), path('orgs/v1/', include('orgs.urls.api_urls', namespace='api-orgs')), path('settings/v1/', include('settings.urls.api_urls', namespace='api-settings')), + path('authentication/v1/', include('authentication.urls.api_urls', namespace='api-auth')), ])) ] diff --git a/apps/templates/_header_bar.html b/apps/templates/_header_bar.html index c2764cce3..7c7f696b3 100644 --- a/apps/templates/_header_bar.html +++ b/apps/templates/_header_bar.html @@ -94,10 +94,10 @@