From 03a984a0eb9b47894c6b0c051d596462fd9763d1 Mon Sep 17 00:00:00 2001 From: sadnub Date: Sat, 28 Sep 2024 15:34:08 -0400 Subject: [PATCH] implemented user session tracking, social account tracking, and blocking local user logon --- api/tacticalrmm/accounts/urls.py | 2 + api/tacticalrmm/accounts/views.py | 107 +++++++++++++++++- ...048_coresettings_block_local_user_logon.py | 18 +++ api/tacticalrmm/core/models.py | 2 + api/tacticalrmm/ee/sso/__init__.py | 5 + api/tacticalrmm/ee/sso/urls.py | 9 +- api/tacticalrmm/ee/sso/views.py | 30 +++++ api/tacticalrmm/tacticalrmm/middleware.py | 3 + 8 files changed, 172 insertions(+), 4 deletions(-) create mode 100644 api/tacticalrmm/core/migrations/0048_coresettings_block_local_user_logon.py diff --git a/api/tacticalrmm/accounts/urls.py b/api/tacticalrmm/accounts/urls.py index 5aeb2178e9..69e439453d 100644 --- a/api/tacticalrmm/accounts/urls.py +++ b/api/tacticalrmm/accounts/urls.py @@ -5,6 +5,8 @@ urlpatterns = [ path("users/", views.GetAddUsers.as_view()), path("/users/", views.GetUpdateDeleteUser.as_view()), + path("sessions//", views.DeleteActiveLoginSession.as_view()), + path("users//sessions/", views.GetDeleteActiveLoginSessionsPerUser.as_view()), path("users/reset/", views.UserActions.as_view()), path("users/reset_totp/", views.UserActions.as_view()), path("users/setup_totp/", views.TOTPSetup.as_view()), diff --git a/api/tacticalrmm/accounts/views.py b/api/tacticalrmm/accounts/views.py index bd3c4e509c..e18a7add57 100644 --- a/api/tacticalrmm/accounts/views.py +++ b/api/tacticalrmm/accounts/views.py @@ -1,21 +1,27 @@ import datetime import pyotp -from django.conf import settings from django.contrib.auth import login from django.db import IntegrityError from django.shortcuts import get_object_or_404 +from django.utils import timezone as djangotime from knox.views import LoginView as KnoxLoginView +from knox.models import AuthToken from python_ipware import IpWare from rest_framework.authtoken.serializers import AuthTokenSerializer - from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView +from rest_framework.serializers import ( + ModelSerializer, + SerializerMethodField, + ReadOnlyField +) from accounts.utils import is_root_user from core.tasks import sync_mesh_perms_task from logs.models import AuditLog from tacticalrmm.helpers import notify_error +from tacticalrmm.utils import get_core_settings from .models import APIKey, Role, User from .permissions import AccountsPerms, APIKeyPerms, RolesPerms @@ -48,6 +54,11 @@ def post(self, request, format=None): if user.block_dashboard_login: return notify_error("Bad credentials") + + # block local logon if configured + settings = get_core_settings() + if not user.is_superuser and settings.block_local_user_logon: + return notify_error("Bad credentials") # if totp token not set modify response to notify frontend if not user.totp_key: @@ -72,6 +83,11 @@ def post(self, request, format=None): if user.block_dashboard_login: return notify_error("Bad credentials") + # block local logon if configured + settings = get_core_settings() + if not user.is_superuser and settings.block_local_user_logon: + return notify_error("Bad credentials") + token = request.data["twofactor"] totp = pyotp.TOTP(user.totp_key) @@ -124,6 +140,11 @@ def post(self, request, format=None): if user.block_dashboard_login: return notify_error("Bad credentials") + + # block local logon if configured + settings = get_core_settings() + if not user.is_superuser and settings.block_local_user_logon: + return notify_error("Bad credentials") # if totp token not set modify response to notify frontend if not user.totp_key: @@ -150,6 +171,11 @@ def post(self, request, format=None): if user.block_dashboard_login: return notify_error("Bad credentials") + + # block local logon if configured + settings = get_core_settings() + if not user.is_superuser and settings.block_local_user_logon: + return notify_error("Bad credentials") token = request.data["twofactor"] totp = pyotp.TOTP(user.totp_key) @@ -182,9 +208,84 @@ def post(self, request, format=None): return notify_error("Bad credentials") +class GetDeleteActiveLoginSessionsPerUser(APIView): + permission_classes = [IsAuthenticated, AccountsPerms] + + class TokenSerializer(ModelSerializer): + user = ReadOnlyField(source="user.username") + class Meta: + model = AuthToken + fields = ( + "digest", + "user", + "created", + "expiry", + ) + + + def get(self, request, pk): + tokens = get_object_or_404(User, pk=pk).auth_token_set.filter(expiry__gt=djangotime.now()) + + return Response(self.TokenSerializer(tokens, many=True).data) + + + def delete(self, request, pk): + tokens = get_object_or_404(User, pk=pk).auth_token_set.filter(expiry__gt=djangotime.now()) + + tokens.delete() + return Response("ok") + + +class DeleteActiveLoginSession(APIView): + permission_classes = [IsAuthenticated, AccountsPerms] + + def delete(self, request, pk): + token = get_object_or_404(AuthToken, digest=pk) + + token.delete() + + return Response("ok") + class GetAddUsers(APIView): permission_classes = [IsAuthenticated, AccountsPerms] + class UserSerializerSSO(ModelSerializer): + social_accounts = SerializerMethodField() + + def get_social_accounts(self, obj): + from allauth.socialaccount.models import SocialAccount + + accounts = SocialAccount.objects.filter(user_id=obj.pk) + + return [ + { + "uid": account.uid, + "provider": account.provider, + "display": account.get_provider_account().to_str(), + "last_login": account.last_login, + "date_joined": account.date_joined, + "extra_data": account.extra_data + } + for account in accounts + ] + + class Meta: + model = User + fields = [ + "id", + "username", + "first_name", + "last_name", + "email", + "is_active", + "last_login", + "last_login_ip", + "role", + "block_dashboard_login", + "date_format", + "social_accounts" + ] + def get(self, request): search = request.GET.get("search", None) @@ -195,7 +296,7 @@ def get(self, request): else: users = User.objects.filter(agent=None, is_installer_user=False) - return Response(UserSerializer(users, many=True).data) + return Response(self.UserSerializerSSO(users, many=True).data) def post(self, request): # add new user diff --git a/api/tacticalrmm/core/migrations/0048_coresettings_block_local_user_logon.py b/api/tacticalrmm/core/migrations/0048_coresettings_block_local_user_logon.py new file mode 100644 index 0000000000..dd2eaf4f23 --- /dev/null +++ b/api/tacticalrmm/core/migrations/0048_coresettings_block_local_user_logon.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.14 on 2024-09-22 04:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0047_alter_coresettings_notify_on_warning_alerts'), + ] + + operations = [ + migrations.AddField( + model_name='coresettings', + name='block_local_user_logon', + field=models.BooleanField(default=True), + ), + ] diff --git a/api/tacticalrmm/core/models.py b/api/tacticalrmm/core/models.py index 54d22d7846..38eb958405 100644 --- a/api/tacticalrmm/core/models.py +++ b/api/tacticalrmm/core/models.py @@ -111,6 +111,8 @@ class CoreSettings(BaseAuditModel): notify_on_info_alerts = models.BooleanField(default=False) notify_on_warning_alerts = models.BooleanField(default=True) + block_local_user_logon = models.BooleanField(default=True) + def save(self, *args, **kwargs) -> None: from alerts.tasks import cache_agents_alert_template diff --git a/api/tacticalrmm/ee/sso/__init__.py b/api/tacticalrmm/ee/sso/__init__.py index e69de29bb2..38bd5902d7 100644 --- a/api/tacticalrmm/ee/sso/__init__.py +++ b/api/tacticalrmm/ee/sso/__init__.py @@ -0,0 +1,5 @@ +""" +Copyright (c) 2023-present Amidaware Inc. +This file is subject to the EE License Agreement. +For details, see: https://license.tacticalrmm.com/ee +""" diff --git a/api/tacticalrmm/ee/sso/urls.py b/api/tacticalrmm/ee/sso/urls.py index c0e9c07b9e..df23729a3d 100644 --- a/api/tacticalrmm/ee/sso/urls.py +++ b/api/tacticalrmm/ee/sso/urls.py @@ -1,4 +1,10 @@ - +""" +Copyright (c) 2023-present Amidaware Inc. +This file is subject to the EE License Agreement. +For details, see: https://license.tacticalrmm.com/ee +""" + + from django.urls import path from django.urls import include @@ -9,4 +15,5 @@ path("ssoproviders/", views.GetAddSSOProvider.as_view()), path("ssoproviders//", views.GetUpdateDeleteSSOProvider.as_view()), path("ssoproviders/token/", views.GetAccessToken.as_view()), + path("ssoproviders/settings/", views.GetUpdateSSOSettings.as_view()), ] \ No newline at end of file diff --git a/api/tacticalrmm/ee/sso/views.py b/api/tacticalrmm/ee/sso/views.py index 264e55edd5..7c7f5699ea 100644 --- a/api/tacticalrmm/ee/sso/views.py +++ b/api/tacticalrmm/ee/sso/views.py @@ -1,3 +1,10 @@ +""" +Copyright (c) 2023-present Amidaware Inc. +This file is subject to the EE License Agreement. +For details, see: https://license.tacticalrmm.com/ee +""" + + import re from django.shortcuts import get_object_or_404 @@ -12,6 +19,8 @@ from knox.views import LoginView as KnoxLoginView from django.contrib.auth import logout from logs.models import AuditLog +from tacticalrmm.utils import get_core_settings + class SocialAppSerializer(ModelSerializer): server_url = ReadOnlyField(source="settings.server_url") class Meta: @@ -119,6 +128,7 @@ def post(self, request, format=None): # get token response = super().post(request, format=None) response.data["username"] = request.user.username + response.data["provider"] = login_method["provider"] AuditLog.audit_user_login_successful_sso(request.user.username, login_method["provider"], login_method) @@ -131,3 +141,23 @@ def post(self, request, format=None): logout(request) return Response("The credentials supplied were invalid", status.HTTP_403_FORBIDDEN) + +class GetUpdateSSOSettings(APIView): + permission_classes = [IsAuthenticated, AccountsPerms] + + def get(self, request): + + settings = get_core_settings() + + return Response({"block_local_user_logon": settings.block_local_user_logon}) + + def post(self, request): + + data = request.data + + settings = get_core_settings() + + settings.block_local_user_logon = data["block_local_user_logon"] + settings.save(update_fields=["block_local_user_logon"]) + + return Response("ok") \ No newline at end of file diff --git a/api/tacticalrmm/tacticalrmm/middleware.py b/api/tacticalrmm/tacticalrmm/middleware.py index eaecfe7ac1..35e073a3c8 100644 --- a/api/tacticalrmm/tacticalrmm/middleware.py +++ b/api/tacticalrmm/tacticalrmm/middleware.py @@ -27,6 +27,9 @@ def get_debug_info() -> Dict[str, Any]: "/logout", "/agents/installer", "/api/schema", + "/accounts/ssoproviders/token", + "/_allauth/browser/v1/config", + "/_allauth/browser/v1/auth/provider/redirect" ) DEMO_EXCLUDE_PATHS = (