Skip to content

Commit

Permalink
implemented user session tracking, social account tracking, and block…
Browse files Browse the repository at this point in the history
…ing local user logon
  • Loading branch information
sadnub committed Sep 28, 2024
1 parent 32cfd98 commit 03a984a
Show file tree
Hide file tree
Showing 8 changed files with 172 additions and 4 deletions.
2 changes: 2 additions & 0 deletions api/tacticalrmm/accounts/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
urlpatterns = [
path("users/", views.GetAddUsers.as_view()),
path("<int:pk>/users/", views.GetUpdateDeleteUser.as_view()),
path("sessions/<str:pk>/", views.DeleteActiveLoginSession.as_view()),
path("users/<int:pk>/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()),
Expand Down
107 changes: 104 additions & 3 deletions api/tacticalrmm/accounts/views.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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)

Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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),
),
]
2 changes: 2 additions & 0 deletions api/tacticalrmm/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions api/tacticalrmm/ee/sso/__init__.py
Original file line number Diff line number Diff line change
@@ -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
"""
9 changes: 8 additions & 1 deletion api/tacticalrmm/ee/sso/urls.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -9,4 +15,5 @@
path("ssoproviders/", views.GetAddSSOProvider.as_view()),
path("ssoproviders/<int:pk>/", views.GetUpdateDeleteSSOProvider.as_view()),
path("ssoproviders/token/", views.GetAccessToken.as_view()),
path("ssoproviders/settings/", views.GetUpdateSSOSettings.as_view()),
]
30 changes: 30 additions & 0 deletions api/tacticalrmm/ee/sso/views.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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:
Expand Down Expand Up @@ -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)

Expand All @@ -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")
3 changes: 3 additions & 0 deletions api/tacticalrmm/tacticalrmm/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down

0 comments on commit 03a984a

Please sign in to comment.