diff --git a/api/tacticalrmm/accounts/tests.py b/api/tacticalrmm/accounts/tests.py index 8bc3e0d5ae..133fd9ca12 100644 --- a/api/tacticalrmm/accounts/tests.py +++ b/api/tacticalrmm/accounts/tests.py @@ -17,7 +17,7 @@ def setUp(self): self.bob.save() def test_check_creds(self): - url = "/checkcreds/" + url = "/v2/checkcreds/" data = {"username": "bob", "password": "hunter2"} r = self.client.post(url, data, format="json") @@ -50,7 +50,7 @@ def test_check_creds(self): @patch("pyotp.TOTP.verify") def test_login_view(self, mock_verify): - url = "/login/" + url = "/v2/login/" mock_verify.return_value = True data = {"username": "bob", "password": "hunter2", "twofactor": "123456"} diff --git a/api/tacticalrmm/accounts/views.py b/api/tacticalrmm/accounts/views.py index 2b822f12a3..e329323b27 100644 --- a/api/tacticalrmm/accounts/views.py +++ b/api/tacticalrmm/accounts/views.py @@ -1,5 +1,6 @@ -import pyotp import datetime + +import pyotp from django.conf import settings from django.contrib.auth import login from django.db import IntegrityError @@ -27,7 +28,7 @@ ) -class CheckCreds(KnoxLoginView): +class CheckCredsV2(KnoxLoginView): permission_classes = (AllowAny,) # restrict time on tokens issued by this view to 3 min @@ -51,14 +52,14 @@ def post(self, request, format=None): # if totp token not set modify response to notify frontend if not user.totp_key: login(request, user) - response = super(CheckCreds, self).post(request, format=None) + response = super().post(request, format=None) response.data["totp"] = False return response return Response({"totp": True}) -class LoginView(KnoxLoginView): +class LoginViewV2(KnoxLoginView): permission_classes = (AllowAny,) def post(self, request, format=None): @@ -94,7 +95,7 @@ def post(self, request, format=None): AuditLog.audit_user_login_successful( request.data["username"], debug_info={"ip": request._client_ip} ) - response = super(LoginView, self).post(request, format=None) + response = super().post(request, format=None) response.data["username"] = request.user.username return Response(response.data) else: @@ -104,6 +105,83 @@ def post(self, request, format=None): return notify_error("Bad credentials") +class CheckCreds(KnoxLoginView): + # TODO + # This view is deprecated as of 0.19.0 + # Needed for the initial update to 0.19.0 so frontend code doesn't break on login + permission_classes = (AllowAny,) + + def post(self, request, format=None): + # check credentials + serializer = AuthTokenSerializer(data=request.data) + if not serializer.is_valid(): + AuditLog.audit_user_failed_login( + request.data["username"], debug_info={"ip": request._client_ip} + ) + return notify_error("Bad credentials") + + user = serializer.validated_data["user"] + + if user.block_dashboard_login: + return notify_error("Bad credentials") + + # if totp token not set modify response to notify frontend + if not user.totp_key: + login(request, user) + response = super(CheckCreds, self).post(request, format=None) + response.data["totp"] = "totp not set" + return response + + return Response("ok") + + +class LoginView(KnoxLoginView): + # TODO + # This view is deprecated as of 0.19.0 + # Needed for the initial update to 0.19.0 so frontend code doesn't break on login + permission_classes = (AllowAny,) + + def post(self, request, format=None): + valid = False + + serializer = AuthTokenSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = serializer.validated_data["user"] + + if user.block_dashboard_login: + return notify_error("Bad credentials") + + token = request.data["twofactor"] + totp = pyotp.TOTP(user.totp_key) + + if settings.DEBUG and token == "sekret": + valid = True + elif getattr(settings, "DEMO", False): + valid = True + elif totp.verify(token, valid_window=10): + valid = True + + if valid: + login(request, user) + + # save ip information + ipw = IpWare() + client_ip, _ = ipw.get_client_ip(request.META) + if client_ip: + user.last_login_ip = str(client_ip) + user.save() + + AuditLog.audit_user_login_successful( + request.data["username"], debug_info={"ip": request._client_ip} + ) + return super(LoginView, self).post(request, format=None) + else: + AuditLog.audit_user_failed_twofactor( + request.data["username"], debug_info={"ip": request._client_ip} + ) + return notify_error("Bad credentials") + + class GetAddUsers(APIView): permission_classes = [IsAuthenticated, AccountsPerms] diff --git a/api/tacticalrmm/tacticalrmm/urls.py b/api/tacticalrmm/tacticalrmm/urls.py index b739a7f6e6..af2037b377 100644 --- a/api/tacticalrmm/tacticalrmm/urls.py +++ b/api/tacticalrmm/tacticalrmm/urls.py @@ -2,7 +2,7 @@ from django.urls import include, path, register_converter from knox import views as knox_views -from accounts.views import CheckCreds, LoginView +from accounts.views import CheckCreds, CheckCredsV2, LoginView, LoginViewV2 from agents.consumers import SendCMD from core.consumers import DashInfo, TerminalConsumer from core.views import home @@ -22,8 +22,10 @@ def to_url(self, value): urlpatterns = [ path("", home), - path("checkcreds/", CheckCreds.as_view()), - path("login/", LoginView.as_view()), + path("v2/checkcreds/", CheckCredsV2.as_view()), + path("v2/login/", LoginViewV2.as_view()), + path("checkcreds/", CheckCreds.as_view()), # DEPRECATED AS OF 0.19.0 + path("login/", LoginView.as_view()), # DEPRECATED AS OF 0.19.0 path("logout/", knox_views.LogoutView.as_view()), path("logoutall/", knox_views.LogoutAllView.as_view()), path("api/v3/", include("apiv3.urls")),