secure sso token a little more and allow for disabling sso feature.
This commit is contained in:
		@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 4.2.14 on 2024-10-15 15:20
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('core', '0048_coresettings_block_local_user_logon'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='coresettings',
 | 
			
		||||
            name='disable_sso',
 | 
			
		||||
            field=models.BooleanField(default=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,22 @@
 | 
			
		||||
# Generated by Django 4.2.14 on 2024-10-15 20:05
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('core', '0049_coresettings_disable_sso'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='coresettings',
 | 
			
		||||
            name='disable_sso',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='coresettings',
 | 
			
		||||
            name='sso_enabled',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -112,6 +112,7 @@ class CoreSettings(BaseAuditModel):
 | 
			
		||||
    notify_on_warning_alerts = models.BooleanField(default=True)
 | 
			
		||||
 | 
			
		||||
    block_local_user_logon = models.BooleanField(default=True)
 | 
			
		||||
    sso_enabled = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs) -> None:
 | 
			
		||||
        from alerts.tasks import cache_agents_alert_template
 | 
			
		||||
 
 | 
			
		||||
@@ -4,40 +4,19 @@ from allauth.account.utils import user_email, user_field, user_username
 | 
			
		||||
from allauth.utils import valid_email_or_none
 | 
			
		||||
 | 
			
		||||
from accounts.models import Role
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TacticalSocialAdapter(DefaultSocialAccountAdapter):
 | 
			
		||||
 | 
			
		||||
    def populate_user(self, request, sociallogin, data):
 | 
			
		||||
        """
 | 
			
		||||
        Hook that can be used to further populate the user instance.
 | 
			
		||||
 | 
			
		||||
        For convenience, we populate several common fields.
 | 
			
		||||
 | 
			
		||||
        Note that the user instance being populated represents a
 | 
			
		||||
        suggested User instance that represents the social user that is
 | 
			
		||||
        in the process of being logged in.
 | 
			
		||||
 | 
			
		||||
        The User instance need not be completely valid and conflict
 | 
			
		||||
        free. For example, verifying whether or not the username
 | 
			
		||||
        already exists, is not a responsibility.
 | 
			
		||||
        """
 | 
			
		||||
        username = data.get("username")
 | 
			
		||||
        first_name = data.get("first_name")
 | 
			
		||||
        last_name = data.get("last_name")
 | 
			
		||||
        email = data.get("email")
 | 
			
		||||
        name = data.get("name")
 | 
			
		||||
        user = sociallogin.user
 | 
			
		||||
        user_username(user, username or "")
 | 
			
		||||
        user_email(user, valid_email_or_none(email) or "")
 | 
			
		||||
        name_parts = (name or "").partition(" ")
 | 
			
		||||
        user_field(user, "first_name", first_name or name_parts[0])
 | 
			
		||||
        user_field(user, "last_name", last_name or name_parts[2])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        user = super().populate_user(request, sociallogin, data)
 | 
			
		||||
        try:
 | 
			
		||||
            provider = sociallogin.account.get_provider()
 | 
			
		||||
            provider_settings = SocialApp.objects.get(provider_id=provider).settings
 | 
			
		||||
            user.role = Role.objects.get(pk=provider_settings["role"])
 | 
			
		||||
            print(provider, provider_settings)
 | 
			
		||||
        except:
 | 
			
		||||
            print("Provider settings or Role not found. Continuing with blank permissions.")
 | 
			
		||||
            print(
 | 
			
		||||
                "Provider settings or Role not found. Continuing with blank permissions."
 | 
			
		||||
            )
 | 
			
		||||
        return user
 | 
			
		||||
							
								
								
									
										8
									
								
								api/tacticalrmm/ee/sso/permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								api/tacticalrmm/ee/sso/permissions.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
from rest_framework import permissions
 | 
			
		||||
from allauth.socialaccount.models import SocialAccount
 | 
			
		||||
 | 
			
		||||
class SSOLoginPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        connected_apps = SocialAccount.objects.filter(user=r.user)
 | 
			
		||||
 | 
			
		||||
        return len(connected_apps) > 0
 | 
			
		||||
@@ -4,15 +4,75 @@ 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
 | 
			
		||||
from django.urls import path, include, re_path
 | 
			
		||||
from allauth.socialaccount.providers.openid_connect.views import callback
 | 
			
		||||
from allauth.headless.socialaccount.views import (
 | 
			
		||||
    RedirectToProviderView,
 | 
			
		||||
    ManageProvidersView,
 | 
			
		||||
)
 | 
			
		||||
from allauth.headless.base.views import ConfigView
 | 
			
		||||
 | 
			
		||||
from . import views
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path("", include("allauth.urls")),
 | 
			
		||||
    re_path(
 | 
			
		||||
        r"^oidc/(?P<provider_id>[^/]+)/",
 | 
			
		||||
        include(
 | 
			
		||||
            [
 | 
			
		||||
                path(
 | 
			
		||||
                    "login/callback/",
 | 
			
		||||
                    callback,
 | 
			
		||||
                    name="openid_connect_callback",
 | 
			
		||||
                ),
 | 
			
		||||
            ]
 | 
			
		||||
        ),
 | 
			
		||||
    ),
 | 
			
		||||
    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()),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
allauth_urls = [
 | 
			
		||||
    path(
 | 
			
		||||
        "browser/v1/",
 | 
			
		||||
        include(
 | 
			
		||||
            (
 | 
			
		||||
                [
 | 
			
		||||
                    path(
 | 
			
		||||
                        "config",
 | 
			
		||||
                        ConfigView.as_api_view(client="browser"),
 | 
			
		||||
                        name="config",
 | 
			
		||||
                    ),
 | 
			
		||||
                    path(
 | 
			
		||||
                        "",
 | 
			
		||||
                        include(
 | 
			
		||||
                            (
 | 
			
		||||
                                [
 | 
			
		||||
                                    path(
 | 
			
		||||
                                        "auth/provider/redirect",
 | 
			
		||||
                                        RedirectToProviderView.as_api_view(
 | 
			
		||||
                                            client="browser"
 | 
			
		||||
                                        ),
 | 
			
		||||
                                        name="redirect_to_provider",
 | 
			
		||||
                                    ),
 | 
			
		||||
                                    path(
 | 
			
		||||
                                        "providers",
 | 
			
		||||
                                        ManageProvidersView.as_api_view(
 | 
			
		||||
                                            client="browser"
 | 
			
		||||
                                        ),
 | 
			
		||||
                                        name="manage_providers",
 | 
			
		||||
                                    ),
 | 
			
		||||
                                ],
 | 
			
		||||
                                "headless",
 | 
			
		||||
                            ),
 | 
			
		||||
                            namespace="socialaccount",
 | 
			
		||||
                        ),
 | 
			
		||||
                    ),
 | 
			
		||||
                ],
 | 
			
		||||
                "headless",
 | 
			
		||||
            ),
 | 
			
		||||
            namespace="browser",
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ from python_ipware import IpWare
 | 
			
		||||
from accounts.permissions import AccountsPerms
 | 
			
		||||
from logs.models import AuditLog
 | 
			
		||||
from tacticalrmm.utils import get_core_settings
 | 
			
		||||
 | 
			
		||||
from .permissions import SSOLoginPerms
 | 
			
		||||
 | 
			
		||||
class SocialAppSerializer(ModelSerializer):
 | 
			
		||||
    server_url = ReadOnlyField(source="settings.server_url")
 | 
			
		||||
@@ -126,13 +126,17 @@ class GetUpdateDeleteSSOProvider(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetAccessToken(KnoxLoginView):
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
    permission_classes = [SSOLoginPerms]
 | 
			
		||||
    authentication_classes = [SessionAuthentication]
 | 
			
		||||
 | 
			
		||||
    def post(self, request, format=None):
 | 
			
		||||
        
 | 
			
		||||
        core = get_core_settings()
 | 
			
		||||
        
 | 
			
		||||
        # check for auth method before signing in
 | 
			
		||||
        if (
 | 
			
		||||
            "account_authentication_methods" in request.session
 | 
			
		||||
            core.sso_enabled
 | 
			
		||||
            and "account_authentication_methods" in request.session
 | 
			
		||||
            and len(request.session["account_authentication_methods"]) > 0
 | 
			
		||||
        ):
 | 
			
		||||
            login_method = request.session["account_authentication_methods"][0]
 | 
			
		||||
@@ -158,10 +162,9 @@ class GetAccessToken(KnoxLoginView):
 | 
			
		||||
 | 
			
		||||
            return Response(response.data)
 | 
			
		||||
        else:
 | 
			
		||||
            AuditLog.audit_user_login_failed_sso(request.user.username)
 | 
			
		||||
            logout(request)
 | 
			
		||||
            return Response(
 | 
			
		||||
                "The credentials supplied were invalid", status.HTTP_403_FORBIDDEN
 | 
			
		||||
                "No pending login session found", status.HTTP_403_FORBIDDEN
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -173,7 +176,10 @@ class GetUpdateSSOSettings(APIView):
 | 
			
		||||
        core_settings = get_core_settings()
 | 
			
		||||
 | 
			
		||||
        return Response(
 | 
			
		||||
            {"block_local_user_logon": core_settings.block_local_user_logon}
 | 
			
		||||
            {
 | 
			
		||||
                "block_local_user_logon": core_settings.block_local_user_logon,
 | 
			
		||||
                "sso_enabled": core_settings.sso_enabled
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
@@ -183,6 +189,7 @@ class GetUpdateSSOSettings(APIView):
 | 
			
		||||
        core_settings = get_core_settings()
 | 
			
		||||
 | 
			
		||||
        core_settings.block_local_user_logon = data["block_local_user_logon"]
 | 
			
		||||
        core_settings.save(update_fields=["block_local_user_logon"])
 | 
			
		||||
        core_settings.sso_enabled = data["sso_enabled"]
 | 
			
		||||
        core_settings.save(update_fields=["block_local_user_logon", "sso_enabled"])
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 
 | 
			
		||||
@@ -225,18 +225,6 @@ class AuditLog(models.Model):
 | 
			
		||||
            debug_info=debug_info,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def audit_user_login_failed_sso(
 | 
			
		||||
        username: str, debug_info: Dict[Any, Any] = {}
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        AuditLog.objects.create(
 | 
			
		||||
            username=username,
 | 
			
		||||
            object_type=AuditObjType.USER,
 | 
			
		||||
            action=AuditActionType.LOGIN,
 | 
			
		||||
            message=f"{username} failed to login through unknown sso provider",
 | 
			
		||||
            debug_info=debug_info,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def audit_url_action(
 | 
			
		||||
        username: str,
 | 
			
		||||
 
 | 
			
		||||
@@ -208,7 +208,6 @@ SOCIALACCOUNT_EMAIL_VERIFICATION = True
 | 
			
		||||
 | 
			
		||||
SOCIALACCOUNT_PROVIDERS = {"openid_connect": {"OAUTH_PKCE_ENABLED": True}}
 | 
			
		||||
 | 
			
		||||
AUTHENTICATION_BACKENDS = ("allauth.account.auth_backends.AuthenticationBackend",)
 | 
			
		||||
SESSION_COOKIE_SECURE = True
 | 
			
		||||
 | 
			
		||||
# silence cache key length warnings
 | 
			
		||||
 
 | 
			
		||||
@@ -3,12 +3,12 @@ from django.urls import include, path, register_converter
 | 
			
		||||
from knox import views as knox_views
 | 
			
		||||
 | 
			
		||||
from accounts.views import CheckCreds, CheckCredsV2, LoginView, LoginViewV2
 | 
			
		||||
from ee.sso.urls import allauth_urls
 | 
			
		||||
 | 
			
		||||
# from agents.consumers import SendCMD
 | 
			
		||||
from core.consumers import DashInfo, TerminalConsumer
 | 
			
		||||
from core.views import home
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentIDConverter:
 | 
			
		||||
    regex = "[^/]{20}[^/]+"
 | 
			
		||||
 | 
			
		||||
@@ -24,7 +24,7 @@ register_converter(AgentIDConverter, "agent")
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path("", home),
 | 
			
		||||
    # all auth urls
 | 
			
		||||
    path("_allauth/", include("allauth.headless.urls")),
 | 
			
		||||
    path("_allauth/", include(allauth_urls)),
 | 
			
		||||
    path("v2/checkcreds/", CheckCredsV2.as_view()),
 | 
			
		||||
    path("v2/login/", LoginViewV2.as_view()),
 | 
			
		||||
    path("checkcreds/", CheckCreds.as_view()),  # DEPRECATED AS OF 0.19.0
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user