Compare commits

...

57 Commits

Author SHA1 Message Date
wh1te909
2e6c9795ec Release 0.20.0 2024-11-21 16:57:58 +00:00
wh1te909
c6b667f8b3 bump version [skip ci] 2024-11-21 16:57:37 +00:00
wh1te909
ad4cddb4f3 bump web ver [skip ci] 2024-11-20 19:50:21 +00:00
wh1te909
ddba83b993 Merge pull request #2001 from sadnub/sso
feat: single sign-on #508
2024-11-20 11:17:52 -08:00
wh1te909
91c33b0431 add setting override to disable sso 2024-11-16 19:28:28 +00:00
wh1te909
d1df40633a call sync mesh after sso user created 2024-11-16 19:10:17 +00:00
wh1te909
7f9fc484e8 revert as these haven't changed [skip ci] 2024-11-15 20:40:16 +00:00
wh1te909
ecf564648e update reqs 2024-11-15 20:25:54 +00:00
wh1te909
150e3190bc refurb 2024-11-15 20:19:00 +00:00
wh1te909
63947346e9 remove deprecated login endpoints 2024-11-15 20:18:41 +00:00
wh1te909
86816ce357 move name stuff to the correct view and add email fallback 2024-11-10 20:59:27 +00:00
wh1te909
0d34831df4 also check if first name only and display 2024-11-06 20:32:28 +00:00
wh1te909
c35da67401 update reqs 2024-11-05 20:26:50 +00:00
wh1te909
fb47022380 redo migrations 2024-11-04 23:40:59 +00:00
wh1te909
46c5128418 move callback url info to the backend 2024-11-04 21:58:37 +00:00
wh1te909
4a5bfee616 fix failsafe to ensure no lockouts and add self-reset sso perms 2024-11-04 20:28:01 +00:00
wh1te909
f8314e0f8e fix pop 2024-11-04 18:57:09 +00:00
wh1te909
9624af4e67 fix tests 2024-11-03 08:47:40 +00:00
wh1te909
5bec4768e7 forgot frontend 2024-11-03 06:22:33 +00:00
wh1te909
3851b0943a modify settings instead of local_settings 2024-11-03 06:17:04 +00:00
wh1te909
cc1f640a50 set icon based on provider 2024-11-01 17:42:51 +00:00
wh1te909
ec0a2dc053 handle deployment config updates 2024-10-31 19:06:39 +00:00
wh1te909
a6166a1ad7 add random otp to social accounts 2024-10-31 01:25:20 +00:00
wh1te909
41e3d1f490 move check to signup 2024-10-30 23:07:14 +00:00
wh1te909
2cbecaa552 don't show providers list on login screen if sso is disabled globally 2024-10-30 05:13:35 +00:00
wh1te909
8d543dcc7d move inside if block 2024-10-29 20:03:10 +00:00
sadnub
18b1afe34f formatting 2024-10-29 11:40:05 -04:00
sadnub
0f86bbfad8 disable password/mfa reset views if block_local_logon is enabled 2024-10-29 11:29:04 -04:00
wh1te909
0d021a800a use exists 2024-10-29 11:29:04 -04:00
wh1te909
038304384a move sso settings 2024-10-29 11:29:04 -04:00
wh1te909
2c09ad6b91 update headers 2024-10-29 11:29:04 -04:00
wh1te909
0bd09d03c1 fix tests 2024-10-29 11:29:04 -04:00
wh1te909
faa0e6c289 handle orphaned sso providers 2024-10-29 11:29:04 -04:00
wh1te909
c28d800d7f blacked 2024-10-29 11:29:04 -04:00
wh1te909
4fd772ecd8 update reqs 2024-10-29 11:29:04 -04:00
sadnub
5520a84062 fix client ip not showing in audit log for sso logon and disable some unused urls and settings 2024-10-29 11:29:04 -04:00
sadnub
66c7123f7c allow displaying full name in UI if present 2024-10-29 11:29:03 -04:00
sadnub
bacf4154fd fix some 500 errors 2024-10-29 11:29:03 -04:00
wh1te909
61790d2261 blacked 2024-10-29 11:29:03 -04:00
wh1te909
899111a310 remove unused imports 2024-10-29 11:29:03 -04:00
wh1te909
3bfa35e1c7 move settings before local import 2024-10-29 11:29:03 -04:00
wh1te909
ebefcb7fc1 block local should be disabled by default 2024-10-29 11:29:03 -04:00
sadnub
ce11685371 secure sso token a little more and allow for disabling sso feature. 2024-10-29 11:29:03 -04:00
sadnub
9edb848947 implement default role for sso signups and log ip for sso logins 2024-10-29 11:29:03 -04:00
wh1te909
f326096fad isort 2024-10-29 11:29:03 -04:00
wh1te909
46f0b23f4f rename to avoid conflict with django settings 2024-10-29 11:29:03 -04:00
wh1te909
1c1d3bd619 frontend needs to come first 2024-10-29 11:29:03 -04:00
wh1te909
d894f92d5e format 2024-10-29 11:29:03 -04:00
wh1te909
6c44191fe4 blacked 2024-10-29 11:29:03 -04:00
wh1te909
0deb78a9af fix settings 2024-10-29 11:29:03 -04:00
sadnub
9c15f4ba88 implemented user session tracking, social account tracking, and blocking local user logon 2024-10-29 11:29:03 -04:00
sadnub
4ba27ec1d6 add auditing and session key checking to the sso auth token view 2024-10-29 11:29:03 -04:00
sadnub
c8dd80530a fix session auth and restrict it only to access_token view 2024-10-29 11:29:03 -04:00
sadnub
eda5ea7d1a sso init 2024-10-29 11:29:03 -04:00
wh1te909
77a916e1a8 don't force env vars fixes #2048 2024-10-29 04:31:24 +00:00
wh1te909
7ba2a4b27b update chocos 2024-10-24 09:23:16 +00:00
wh1te909
d33f69720a back to dev [skip ci] 2024-10-23 17:31:55 +00:00
34 changed files with 835 additions and 178 deletions

View File

@@ -33,12 +33,12 @@ function check_tactical_ready {
}
function django_setup {
until (echo > /dev/tcp/"${POSTGRES_HOST}"/"${POSTGRES_PORT}") &> /dev/null; do
until (echo >/dev/tcp/"${POSTGRES_HOST}"/"${POSTGRES_PORT}") &>/dev/null; do
echo "waiting for postgresql container to be ready..."
sleep 5
done
until (echo > /dev/tcp/"${MESH_SERVICE}"/4443) &> /dev/null; do
until (echo >/dev/tcp/"${MESH_SERVICE}"/4443) &>/dev/null; do
echo "waiting for meshcentral container to be ready..."
sleep 5
done
@@ -50,7 +50,10 @@ function django_setup {
DJANGO_SEKRET=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 80 | head -n 1)
localvars="$(cat << EOF
BASE_DOMAIN=$(echo "import tldextract; no_fetch_extract = tldextract.TLDExtract(suffix_list_urls=()); extracted = no_fetch_extract('${API_HOST}'); print(f'{extracted.domain}.{extracted.suffix}')" | python)
localvars="$(
cat <<EOF
SECRET_KEY = '${DJANGO_SEKRET}'
DEBUG = True
@@ -64,12 +67,17 @@ KEY_FILE = '${CERT_PRIV_PATH}'
SCRIPTS_DIR = '/community-scripts'
ALLOWED_HOSTS = ['${API_HOST}', '*']
ADMIN_URL = 'admin/'
CORS_ORIGIN_ALLOW_ALL = True
CORS_ORIGIN_WHITELIST = ['https://${API_HOST}']
ALLOWED_HOSTS = ['${API_HOST}', '${APP_HOST}', '*']
CORS_ORIGIN_WHITELIST = ['https://${APP_HOST}']
SESSION_COOKIE_DOMAIN = '${BASE_DOMAIN}'
CSRF_COOKIE_DOMAIN = '${BASE_DOMAIN}'
CSRF_TRUSTED_ORIGINS = ['https://${API_HOST}', 'https://${APP_HOST}']
HEADLESS_FRONTEND_URLS = {'socialaccount_login_error': 'https://${APP_HOST}/account/provider/callback'}
DATABASES = {
'default': {
@@ -101,9 +109,9 @@ MESH_WS_URL = '${MESH_WS_URL}'
ADMIN_ENABLED = True
TRMM_INSECURE = True
EOF
)"
)"
echo "${localvars}" > ${WORKSPACE_DIR}/api/tacticalrmm/tacticalrmm/local_settings.py
echo "${localvars}" >${WORKSPACE_DIR}/api/tacticalrmm/tacticalrmm/local_settings.py
# run migrations and init scripts
"${VIRTUAL_ENV}"/bin/python manage.py pre_update_tasks
@@ -119,7 +127,6 @@ EOF
"${VIRTUAL_ENV}"/bin/python manage.py create_installer_user
"${VIRTUAL_ENV}"/bin/python manage.py post_update_tasks
# create super user
echo "from accounts.models import User; User.objects.create_superuser('${TRMM_USER}', 'admin@example.com', '${TRMM_PASS}') if not User.objects.filter(username='${TRMM_USER}').exists() else 0;" | python manage.py shell
}

View File

@@ -1,10 +1,11 @@
import subprocess
import pyotp
from django.conf import settings
from django.core.management.base import BaseCommand
from accounts.models import User
from tacticalrmm.helpers import get_webdomain
from tacticalrmm.util_settings import get_webdomain
class Command(BaseCommand):
@@ -26,7 +27,7 @@ class Command(BaseCommand):
user.save(update_fields=["totp_key"])
url = pyotp.totp.TOTP(code).provisioning_uri(
username, issuer_name=get_webdomain()
username, issuer_name=get_webdomain(settings.CORS_ORIGIN_WHITELIST[0])
)
subprocess.run(f'qr "{url}"', shell=True)
self.stdout.write(

View File

@@ -1,5 +1,6 @@
from typing import Optional
from allauth.socialaccount.models import SocialAccount
from django.contrib.auth.models import AbstractUser
from django.core.cache import cache
from django.db import models
@@ -73,6 +74,10 @@ class User(AbstractUser, BaseAuditModel):
# lower() needed for mesh api
return f"{self.username.replace(' ', '').lower()}___{self.pk}"
@property
def is_sso_user(self):
return SocialAccount.objects.filter(user_id=self.pk).exists()
@staticmethod
def serialize(user):
# serializes the task and returns json

View File

@@ -1,6 +1,7 @@
from rest_framework import permissions
from tacticalrmm.permissions import _has_perm
from tacticalrmm.utils import get_core_settings
class AccountsPerms(permissions.BasePermission):
@@ -40,3 +41,14 @@ class APIKeyPerms(permissions.BasePermission):
return _has_perm(r, "can_list_api_keys")
return _has_perm(r, "can_manage_api_keys")
class LocalUserPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool:
settings = get_core_settings()
return not settings.block_local_user_logon
class SelfResetSSOPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool:
return not r.user.is_sso_user

View File

@@ -1,11 +1,12 @@
import pyotp
from django.conf import settings
from rest_framework.serializers import (
ModelSerializer,
ReadOnlyField,
SerializerMethodField,
)
from tacticalrmm.helpers import get_webdomain
from tacticalrmm.util_settings import get_webdomain
from .models import APIKey, Role, User
@@ -63,7 +64,7 @@ class TOTPSetupSerializer(ModelSerializer):
def get_qr_url(self, obj):
return pyotp.totp.TOTP(obj.totp_key).provisioning_uri(
obj.username, issuer_name=get_webdomain()
obj.username, issuer_name=get_webdomain(settings.CORS_ORIGIN_WHITELIST[0])
)

View File

@@ -11,6 +11,7 @@ from tacticalrmm.test import TacticalTestCase
class TestAccounts(TacticalTestCase):
def setUp(self):
self.setup_coresettings()
self.setup_client()
self.bob = User(username="bob")
self.bob.set_password("hunter2")

View File

@@ -5,6 +5,10 @@ from . import views
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()),

View File

@@ -1,24 +1,39 @@
import datetime
import pyotp
from allauth.socialaccount.models import SocialAccount, SocialApp
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.models import AuthToken
from knox.views import LoginView as KnoxLoginView
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.serializers import (
ModelSerializer,
ReadOnlyField,
SerializerMethodField,
)
from rest_framework.views import APIView
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
from .permissions import (
AccountsPerms,
APIKeyPerms,
LocalUserPerms,
RolesPerms,
SelfResetSSOPerms,
)
from .serializers import (
APIKeySerializer,
RoleSerializer,
@@ -46,7 +61,12 @@ class CheckCredsV2(KnoxLoginView):
user = serializer.validated_data["user"]
if user.block_dashboard_login:
if user.block_dashboard_login or user.is_sso_user:
return notify_error("Bad credentials")
# block local logon if configured
core_settings = get_core_settings()
if not user.is_superuser and core_settings.block_local_user_logon:
return notify_error("Bad credentials")
# if totp token not set modify response to notify frontend
@@ -72,6 +92,14 @@ class LoginViewV2(KnoxLoginView):
if user.block_dashboard_login:
return notify_error("Bad credentials")
# block local logon if configured
core_settings = get_core_settings()
if not user.is_superuser and core_settings.block_local_user_logon:
return notify_error("Bad credentials")
if user.is_sso_user:
return notify_error("Bad credentials")
token = request.data["twofactor"]
totp = pyotp.TOTP(user.totp_key)
@@ -97,6 +125,8 @@ class LoginViewV2(KnoxLoginView):
)
response = super().post(request, format=None)
response.data["username"] = request.user.username
response.data["name"] = None
return Response(response.data)
else:
AuditLog.audit_user_failed_twofactor(
@@ -105,86 +135,100 @@ class LoginViewV2(KnoxLoginView):
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,)
class GetDeleteActiveLoginSessionsPerUser(APIView):
permission_classes = [IsAuthenticated, AccountsPerms]
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}
class TokenSerializer(ModelSerializer):
user = ReadOnlyField(source="user.username")
class Meta:
model = AuthToken
fields = (
"digest",
"user",
"created",
"expiry",
)
return notify_error("Bad credentials")
user = serializer.validated_data["user"]
def get(self, request, pk):
tokens = get_object_or_404(User, pk=pk).auth_token_set.filter(
expiry__gt=djangotime.now()
)
if user.block_dashboard_login:
return notify_error("Bad credentials")
return Response(self.TokenSerializer(tokens, many=True).data)
# 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
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 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]
class UserSerializerSSO(ModelSerializer):
social_accounts = SerializerMethodField()
def get_social_accounts(self, obj):
accounts = SocialAccount.objects.filter(user_id=obj.pk)
if accounts:
social_accounts = []
for account in accounts:
try:
provider_account = account.get_provider_account()
display = provider_account.to_str()
except SocialApp.DoesNotExist:
display = "Orphaned Provider"
except Exception:
display = "Unknown"
social_accounts.append(
{
"uid": account.uid,
"provider": account.provider,
"display": display,
"last_login": account.last_login,
"date_joined": account.date_joined,
"extra_data": account.extra_data,
}
)
return social_accounts
return []
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 +239,7 @@ class GetAddUsers(APIView):
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
@@ -255,7 +299,7 @@ class GetUpdateDeleteUser(APIView):
class UserActions(APIView):
permission_classes = [IsAuthenticated, AccountsPerms]
permission_classes = [IsAuthenticated, AccountsPerms, LocalUserPerms]
# reset password
def post(self, request):
@@ -381,7 +425,7 @@ class GetUpdateDeleteAPIKey(APIView):
class ResetPass(APIView):
permission_classes = [IsAuthenticated]
permission_classes = [IsAuthenticated, SelfResetSSOPerms]
def put(self, request):
user = request.user
@@ -391,7 +435,7 @@ class ResetPass(APIView):
class Reset2FA(APIView):
permission_classes = [IsAuthenticated]
permission_classes = [IsAuthenticated, SelfResetSSOPerms]
def put(self, request):
user = request.user

View File

@@ -177,8 +177,7 @@ class CheckRunnerGetSerializer(serializers.ModelSerializer):
return Script.parse_script_env_vars(
agent=agent,
shell=obj.script.shell,
env_vars=obj.env_vars
or obj.script.env_vars, # check's env_vars override the script's env vars
env_vars=obj.env_vars,
)
class Meta:

View File

@@ -3,7 +3,7 @@ from urllib.parse import urlparse
from django.conf import settings
from django.core.management.base import BaseCommand
from tacticalrmm.helpers import get_webdomain
from tacticalrmm.util_settings import get_backend_url, get_root_domain, get_webdomain
from tacticalrmm.utils import get_certs
@@ -17,6 +17,8 @@ class Command(BaseCommand):
match kwargs["name"]:
case "api":
self.stdout.write(settings.ALLOWED_HOSTS[0])
case "rootdomain":
self.stdout.write(get_root_domain(settings.ALLOWED_HOSTS[0]))
case "version":
self.stdout.write(settings.TRMM_VERSION)
case "webversion":
@@ -27,8 +29,16 @@ class Command(BaseCommand):
self.stdout.write(settings.NATS_SERVER_VER)
case "frontend":
self.stdout.write(settings.CORS_ORIGIN_WHITELIST[0])
case "backend_url":
self.stdout.write(
get_backend_url(
settings.ALLOWED_HOSTS[0],
settings.TRMM_PROTO,
settings.TRMM_BACKEND_PORT,
)
)
case "webdomain":
self.stdout.write(get_webdomain())
self.stdout.write(get_webdomain(settings.CORS_ORIGIN_WHITELIST[0]))
case "djangoadmin":
url = f"https://{settings.ALLOWED_HOSTS[0]}/{settings.ADMIN_URL}"
self.stdout.write(url)

View File

@@ -0,0 +1,23 @@
# Generated by Django 4.2.16 on 2024-11-04 23:37
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=False),
),
migrations.AddField(
model_name="coresettings",
name="sso_enabled",
field=models.BooleanField(default=False),
),
]

View File

@@ -10,6 +10,9 @@ from django.contrib.postgres.fields import ArrayField
from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.db import models
from twilio.base.exceptions import TwilioRestException
from twilio.rest import Client as TwClient
from logs.models import BaseAuditModel, DebugLog
from tacticalrmm.constants import (
ALL_TIMEZONES,
@@ -20,8 +23,6 @@ from tacticalrmm.constants import (
URLActionRestMethod,
URLActionType,
)
from twilio.base.exceptions import TwilioRestException
from twilio.rest import Client as TwClient
if TYPE_CHECKING:
from alerts.models import AlertTemplate
@@ -111,6 +112,9 @@ 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=False)
sso_enabled = models.BooleanField(default=False)
def save(self, *args, **kwargs) -> None:
from alerts.tasks import cache_agents_alert_template
@@ -127,9 +131,23 @@ class CoreSettings(BaseAuditModel):
self.mesh_token = settings.MESH_TOKEN_KEY
old_settings = type(self).objects.get(pk=self.pk) if self.pk else None
if old_settings:
# fail safe to not lock out user logons
if not self.sso_enabled and self.block_local_user_logon:
self.block_local_user_logon = False
if old_settings.sso_enabled != self.sso_enabled and self.sso_enabled:
from core.utils import token_is_valid
_, valid = token_is_valid()
if not valid:
raise ValidationError("")
super().save(*args, **kwargs)
if old_settings:
if (
old_settings.alert_template != self.alert_template
or old_settings.server_policy != self.server_policy

View File

@@ -137,6 +137,8 @@ def dashboard_info(request):
"run_cmd_placeholder_text": runcmd_placeholder_text(),
"server_scripts_enabled": core_settings.server_scripts_enabled,
"web_terminal_enabled": core_settings.web_terminal_enabled,
"block_local_user_logon": core_settings.block_local_user_logon,
"sso_enabled": core_settings.sso_enabled,
}
)

View File

@@ -4,7 +4,7 @@ Copyright (c) 2023 Amidaware Inc. All rights reserved.
This Agreement is entered into between the licensee ("You" or the "Licensee") and Amidaware Inc. ("Amidaware") and governs the use of the enterprise features of the Tactical RMM Software (hereinafter referred to as the "Software").
The EE features of the Software, including but not limited to Reporting and White-labeling, are exclusively contained within directories named "ee," "enterprise," or "premium" in Amidaware's repositories, or in any files bearing the EE License header. The use of the Software is also governed by the terms and conditions set forth in the Tactical RMM License, available at https://license.tacticalrmm.com, which terms are incorporated herein by reference.
The EE features of the Software, including but not limited to SSO (Single Sign-On), Reporting and White-labeling, are exclusively contained within directories named "ee," "enterprise," or "premium" in Amidaware's repositories, or in any files bearing the EE License header. The use of the Software is also governed by the terms and conditions set forth in the Tactical RMM License, available at https://license.tacticalrmm.com, which terms are incorporated herein by reference.
## License Grant

View File

@@ -5,10 +5,8 @@ For details, see: https://license.tacticalrmm.com/ee
"""
import urllib.parse
from time import sleep
from typing import Any, Optional
import requests
from core.models import CodeSignToken
from django.conf import settings
from django.core.management.base import BaseCommand
@@ -26,39 +24,12 @@ class Command(BaseCommand):
self.stdout.write(url)
return
attempts = 0
while 1:
try:
r = requests.post(
settings.REPORTING_CHECK_URL,
json={"token": t.token, "api": settings.ALLOWED_HOSTS[0]},
headers={"Content-type": "application/json"},
timeout=15,
)
except Exception as e:
self.stderr.write(str(e))
attempts += 1
sleep(3)
else:
if r.status_code // 100 in (3, 5):
self.stderr.write(f"Error getting web tarball: {r.status_code}")
attempts += 1
sleep(3)
else:
attempts = 0
if attempts == 0:
break
elif attempts > 5:
self.stdout.write(url)
return
if r.status_code == 200: # type: ignore
if t.is_valid:
params = {
"token": t.token,
"webver": settings.WEB_VERSION,
"api": settings.ALLOWED_HOSTS[0],
}
url = settings.REPORTING_DL_URL + urllib.parse.urlencode(params)
url = settings.WEBTAR_DL_URL + urllib.parse.urlencode(params)
self.stdout.write(url)

View File

@@ -0,0 +1,5 @@
"""
Copyright (c) 2024-present Amidaware Inc.
This file is subject to the EE License Agreement.
For details, see: https://license.tacticalrmm.com/ee
"""

View File

@@ -0,0 +1,47 @@
"""
Copyright (c) 2024-present Amidaware Inc.
This file is subject to the EE License Agreement.
For details, see: https://license.tacticalrmm.com/ee
"""
import pyotp
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
from allauth.socialaccount.models import SocialApp
from django.core.exceptions import PermissionDenied
from accounts.models import Role
from core.tasks import sync_mesh_perms_task
from core.utils import token_is_valid
from tacticalrmm.logger import logger
from tacticalrmm.utils import get_core_settings
class TacticalSocialAdapter(DefaultSocialAccountAdapter):
def populate_user(self, request, sociallogin, data):
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"])
except Exception:
logger.debug(
"Provider settings or Role not found. Continuing with blank permissions."
)
user.totp_key = pyotp.random_base32() # not actually used
sync_mesh_perms_task.delay()
return user
def is_open_for_signup(self, request, sociallogin):
_, valid = token_is_valid()
if not valid:
raise PermissionDenied()
return super().is_open_for_signup(request, sociallogin)
def list_providers(self, request):
core_settings = get_core_settings()
if not core_settings.sso_enabled:
return []
return super().list_providers(request)

View File

@@ -0,0 +1,64 @@
"""
Copyright (c) 2024-present Amidaware Inc.
This file is subject to the EE License Agreement.
For details, see: https://license.tacticalrmm.com/ee
"""
import json
from contextlib import suppress
from allauth.headless.base.response import ConfigResponse
from allauth.socialaccount.models import SocialApp
def set_provider_icon(provider, url):
icon_map = {
"google.com": "mdi-google",
"microsoft": "mdi-microsoft",
"discord.com": "fa-brands fa-discord",
"github.com": "fa-brands fa-github",
"slack.com": "fa-brands fa-slack",
"facebook.com": "fa-brands fa-facebook",
"linkedin.com": "fa-brands fa-linkedin",
"apple.com": "fa-brands fa-apple",
"amazon.com": "fa-brands fa-amazon",
"auth0.com": "mdi-lock",
"gitlab.com": "fa-brands fa-gitlab",
"twitter.com": "fa-brands fa-twitter",
"paypal.com": "fa-brands fa-paypal",
"yahoo.com": "fa-brands fa-yahoo",
}
provider["icon"] = "mdi-key"
for key, icon in icon_map.items():
if key in url.lower():
provider["icon"] = icon
break
class SSOIconMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
if request.path == "/_allauth/browser/v1/config/" and isinstance(
response, ConfigResponse
):
with suppress(Exception):
data = json.loads(response.content.decode("utf-8", "ignore"))
data["data"].pop("account", None)
for provider in data["data"]["socialaccount"].get("providers", []):
provider.pop("client_id", None)
provider.pop("flows", None)
app = SocialApp.objects.get(provider_id=provider["id"])
set_provider_icon(provider, app.settings["server_url"])
new_content = json.dumps(data)
response.content = new_content.encode("utf-8", "ignore")
response["Content-Length"] = str(len(response.content))
return response

View File

@@ -0,0 +1,13 @@
"""
Copyright (c) 2024-present Amidaware Inc.
This file is subject to the EE License Agreement.
For details, see: https://license.tacticalrmm.com/ee
"""
from rest_framework import permissions
from allauth.socialaccount.models import SocialAccount
class SSOLoginPerms(permissions.BasePermission):
def has_permission(self, r, view):
return SocialAccount.objects.filter(user=r.user).exists()

View File

@@ -0,0 +1,19 @@
"""
Copyright (c) 2024-present Amidaware Inc.
This file is subject to the EE License Agreement.
For details, see: https://license.tacticalrmm.com/ee
"""
HEADLESS_ONLY = True
SOCIALACCOUNT_ONLY = True
ACCOUNT_DEFAULT_HTTP_PROTOCOL = "https"
ACCOUNT_EMAIL_VERIFICATION = "none"
SOCIALACCOUNT_ADAPTER = "ee.sso.adapter.TacticalSocialAdapter"
SOCIALACCOUNT_EMAIL_AUTHENTICATION = True
SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT = True
SOCIALACCOUNT_EMAIL_VERIFICATION = True
SOCIALACCOUNT_PROVIDERS = {"openid_connect": {"OAUTH_PKCE_ENABLED": True}}
SESSION_COOKIE_SECURE = True
CORS_ALLOW_CREDENTIALS = True

View File

@@ -0,0 +1,69 @@
"""
Copyright (c) 2024-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, include, re_path
from allauth.socialaccount.providers.openid_connect.views import callback
from allauth.headless.socialaccount.views import RedirectToProviderView
from allauth.headless.base.views import ConfigView
from . import views
urlpatterns = [
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()),
path("ssoproviders/account/", views.DisconnectSSOAccount.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",
)
],
"headless",
),
namespace="socialaccount",
),
),
],
"headless",
),
namespace="browser",
),
)
]

View File

@@ -0,0 +1,251 @@
"""
Copyright (c) 2024-present Amidaware Inc.
This file is subject to the EE License Agreement.
For details, see: https://license.tacticalrmm.com/ee
"""
import re
from allauth.socialaccount.models import SocialAccount, SocialApp
from django.conf import settings
from django.contrib.auth import logout
from django.core.exceptions import ValidationError
from django.shortcuts import get_object_or_404
from knox.views import LoginView as KnoxLoginView
from python_ipware import IpWare
from rest_framework import status
from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.serializers import (
ModelSerializer,
ReadOnlyField,
SerializerMethodField,
)
from rest_framework.views import APIView
from accounts.permissions import AccountsPerms
from logs.models import AuditLog
from tacticalrmm.util_settings import get_backend_url
from tacticalrmm.utils import get_core_settings
from .permissions import SSOLoginPerms
class SocialAppSerializer(ModelSerializer):
server_url = ReadOnlyField(source="settings.server_url")
role = ReadOnlyField(source="settings.role")
callback_url = SerializerMethodField()
javascript_origin_url = SerializerMethodField()
def get_callback_url(self, obj):
backend_url = self.context["backend_url"]
return f"{backend_url}/accounts/oidc/{obj.provider_id}/login/callback/"
def get_javascript_origin_url(self, obj):
return self.context["frontend_url"]
class Meta:
model = SocialApp
fields = [
"id",
"name",
"provider",
"provider_id",
"client_id",
"secret",
"server_url",
"settings",
"role",
"callback_url",
"javascript_origin_url",
]
class GetAddSSOProvider(APIView):
permission_classes = [IsAuthenticated, AccountsPerms]
def get(self, request):
ctx = {
"backend_url": get_backend_url(
settings.ALLOWED_HOSTS[0],
settings.TRMM_PROTO,
settings.TRMM_BACKEND_PORT,
),
"frontend_url": settings.CORS_ORIGIN_WHITELIST[0],
}
providers = SocialApp.objects.all()
return Response(SocialAppSerializer(providers, many=True, context=ctx).data)
class InputSerializer(ModelSerializer):
server_url = ReadOnlyField()
role = ReadOnlyField()
class Meta:
model = SocialApp
fields = [
"name",
"client_id",
"secret",
"server_url",
"provider",
"provider_id",
"settings",
"role",
]
# removed any special characters and replaces spaces with a hyphen
def generate_provider_id(self, string):
return re.sub(r"[^A-Za-z0-9\s]", "", string).replace(" ", "-")
def post(self, request):
data = request.data
# need to move server_url into json settings
data["settings"] = {}
data["settings"]["server_url"] = data["server_url"]
data["settings"]["role"] = data["role"] or None
# set provider to 'openid_connect'
data["provider"] = "openid_connect"
# generate a url friendly provider id from the name
data["provider_id"] = self.generate_provider_id(data["name"])
serializer = self.InputSerializer(data=data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
class GetUpdateDeleteSSOProvider(APIView):
permission_classes = [IsAuthenticated, AccountsPerms]
class InputSerialzer(ModelSerializer):
server_url = ReadOnlyField()
role = ReadOnlyField()
class Meta:
model = SocialApp
fields = ["client_id", "secret", "server_url", "settings", "role"]
def put(self, request, pk):
provider = get_object_or_404(SocialApp, pk=pk)
data = request.data
# need to move server_url into json settings
data["settings"] = {}
data["settings"]["server_url"] = data["server_url"]
data["settings"]["role"] = data["role"] or None
serializer = self.InputSerialzer(instance=provider, data=data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
def delete(self, request, pk):
provider = get_object_or_404(SocialApp, pk=pk)
provider.delete()
return Response("ok")
class DisconnectSSOAccount(APIView):
permission_classes = [IsAuthenticated, AccountsPerms]
def delete(self, request):
account = get_object_or_404(
SocialAccount,
uid=request.data["account"],
provider=request.data["provider"],
)
account.delete()
return Response("ok")
class GetAccessToken(KnoxLoginView):
permission_classes = [IsAuthenticated, SSOLoginPerms]
authentication_classes = [SessionAuthentication]
def post(self, request, format=None):
core = get_core_settings()
# check for auth method before signing in
if (
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]
# get token
response = super().post(request, format=None)
response.data["username"] = request.user.username
response.data["provider"] = login_method["provider"]
response.data["name"] = None
if request.user.first_name and request.user.last_name:
response.data["name"] = (
f"{request.user.first_name} {request.user.last_name}"
)
elif request.user.first_name:
response.data["name"] = request.user.first_name
elif request.user.email:
response.data["name"] = request.user.email
# log ip
ipw = IpWare()
client_ip, _ = ipw.get_client_ip(request.META)
if client_ip:
request.user.last_login_ip = str(client_ip)
request.user.save(update_fields=["last_login_ip"])
login_method["ip"] = str(client_ip)
AuditLog.audit_user_login_successful_sso(
request.user.username, login_method["provider"], login_method
)
# invalid user session since we have an access token now
logout(request)
return Response(response.data)
else:
logout(request)
return Response("No pending login session found", status.HTTP_403_FORBIDDEN)
class GetUpdateSSOSettings(APIView):
permission_classes = [IsAuthenticated, AccountsPerms]
def get(self, request):
core_settings = get_core_settings()
return Response(
{
"block_local_user_logon": core_settings.block_local_user_logon,
"sso_enabled": core_settings.sso_enabled,
}
)
def post(self, request):
data = request.data
core_settings = get_core_settings()
try:
core_settings.block_local_user_logon = data["block_local_user_logon"]
core_settings.sso_enabled = data["sso_enabled"]
core_settings.save(update_fields=["block_local_user_logon", "sso_enabled"])
except ValidationError:
return Response(
"This feature requires a Tier 1 or higher sponsorship: https://docs.tacticalrmm.com/sponsor",
status=status.HTTP_423_LOCKED,
)
return Response("ok")

View File

@@ -213,6 +213,18 @@ class AuditLog(models.Model):
debug_info=debug_info,
)
@staticmethod
def audit_user_login_successful_sso(
username: str, provider: str, debug_info: Dict[Any, Any] = {}
) -> None:
AuditLog.objects.create(
username=username,
object_type=AuditObjType.USER,
action=AuditActionType.LOGIN,
message=f"{username} logged in successfully through SSO Provider {provider}",
debug_info=debug_info,
)
@staticmethod
def audit_url_action(
username: str,
@@ -462,7 +474,7 @@ class PendingAction(models.Model):
PAAction.RUN_PATCH_SCAN,
PAAction.RUN_PATCH_INSTALL,
):
return f"{self.action_type}"
return str(self.action_type)
return None

View File

@@ -6,7 +6,8 @@ channels==4.1.0
channels_redis==4.2.0
cryptography==43.0.3
Django==4.2.16
django-cors-headers==4.5.0
django-cors-headers==4.6.0
django-allauth[socialaccount]==65.2.0
django-filter==24.3
django-rest-knox==4.2.0
djangorestframework==3.15.2
@@ -16,7 +17,7 @@ kombu==5.3.7
meshctrl==0.1.15
msgpack==1.1.0
nats-py==2.9.0
packaging==24.1
packaging==24.2
psutil==6.0.0
psycopg[binary]==3.2.3
pycparser==2.22
@@ -29,10 +30,11 @@ redis==5.0.8
requests==2.32.3
six==1.16.0
sqlparse==0.5.1
tldextract==5.1.3
twilio==8.13.0
urllib3==2.2.3
uvicorn[standard]==0.31.1
uWSGI==2.0.27
uWSGI==2.0.28
validators==0.24.0
vine==5.1.0
websockets==13.1

File diff suppressed because one or more lines are too long

View File

@@ -6,7 +6,6 @@ import secrets
import string
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal
from urllib.parse import urlparse
from zoneinfo import ZoneInfo
from cryptography import x509
@@ -103,10 +102,6 @@ def date_is_in_past(*, datetime_obj: "datetime", agent_tz: str) -> bool:
return djangotime.now() > utc_time
def get_webdomain() -> str:
return urlparse(settings.CORS_ORIGIN_WHITELIST[0]).netloc
def rand_range(min: int, max: int) -> float:
"""
Input is milliseconds.

View File

@@ -27,6 +27,9 @@ EXCLUDE_PATHS = (
"/logout",
"/agents/installer",
"/api/schema",
"/accounts/ssoproviders/token",
"/_allauth/browser/v1/config",
"/_allauth/browser/v1/auth/provider/redirect",
)
DEMO_EXCLUDE_PATHS = (
@@ -72,7 +75,9 @@ class AuditMiddleware:
# gather and save debug info
debug_info["url"] = request.path
debug_info["method"] = request.method
debug_info["view_class"] = view_func.cls.__name__
debug_info["view_class"] = (
view_func.cls.__name__ if hasattr(view_func, "cls") else None
)
debug_info["view_func"] = view_Name
debug_info["view_args"] = view_args
debug_info["view_kwargs"] = view_kwargs

View File

@@ -4,6 +4,8 @@ from contextlib import suppress
from datetime import timedelta
from pathlib import Path
from tacticalrmm.util_settings import get_backend_url, get_root_domain, get_webdomain
BASE_DIR = Path(__file__).resolve().parent.parent
SCRIPTS_DIR = "/opt/trmm-community-scripts"
@@ -21,14 +23,14 @@ MAC_UNINSTALL = BASE_DIR / "core" / "mac_uninstall.sh"
AUTH_USER_MODEL = "accounts.User"
# latest release
TRMM_VERSION = "0.19.4"
TRMM_VERSION = "0.20.0"
# https://github.com/amidaware/tacticalrmm-web
WEB_VERSION = "0.101.49"
WEB_VERSION = "0.101.50"
# bump this version everytime vue code is changed
# to alert user they need to manually refresh their browser
APP_VER = "0.0.195"
APP_VER = "0.0.196"
# https://github.com/amidaware/rmmagent
LATEST_AGENT_VER = "2.8.0"
@@ -116,15 +118,63 @@ SWAGGER_ENABLED = False
REDIS_HOST = "127.0.0.1"
TRMM_LOG_LEVEL = "ERROR"
TRMM_LOG_TO = "file"
TRMM_PROTO = "https"
TRMM_BACKEND_PORT = None
if not DOCKER_BUILD:
ALLOWED_HOSTS = []
CORS_ORIGIN_WHITELIST = []
with suppress(ImportError):
from ee.sso.sso_settings import * # noqa
with suppress(ImportError):
from .local_settings import * # noqa
if "GHACTIONS" in os.environ:
print("-----------------------GHACTIONS----------------------------")
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "pipeline",
"USER": "pipeline",
"PASSWORD": "pipeline123456",
"HOST": "127.0.0.1",
"PORT": "",
}
}
SECRET_KEY = "abcdefghijklmnoptravis123456789"
ALLOWED_HOSTS = ["api.example.com"]
ADMIN_URL = "abc123456/"
CORS_ORIGIN_WHITELIST = ["https://rmm.example.com"]
MESH_USERNAME = "pipeline"
MESH_SITE = "https://example.com"
MESH_TOKEN_KEY = "bd65e957a1e70c622d32523f61508400d6cd0937001a7ac12042227eba0b9ed625233851a316d4f489f02994145f74537a331415d00047dbbf13d940f556806dffe7a8ce1de216dc49edbad0c1a7399c"
REDIS_HOST = "localhost"
if not DOCKER_BUILD:
TRMM_ROOT_DOMAIN = get_root_domain(ALLOWED_HOSTS[0])
frontend_domain = get_webdomain(CORS_ORIGIN_WHITELIST[0]).split(":")[0]
ALLOWED_HOSTS.append(frontend_domain)
if DEBUG:
ALLOWED_HOSTS.append("*")
backend_url = get_backend_url(ALLOWED_HOSTS[0], TRMM_PROTO, TRMM_BACKEND_PORT)
SESSION_COOKIE_DOMAIN = TRMM_ROOT_DOMAIN
CSRF_COOKIE_DOMAIN = TRMM_ROOT_DOMAIN
CSRF_TRUSTED_ORIGINS = [CORS_ORIGIN_WHITELIST[0], backend_url]
HEADLESS_FRONTEND_URLS = {
"socialaccount_login_error": f"{CORS_ORIGIN_WHITELIST[0]}/account/provider/callback"
}
CHECK_TOKEN_URL = f"{AGENT_BASE_URL}/api/v2/checktoken"
AGENTS_URL = f"{AGENT_BASE_URL}/api/v2/agents/?"
EXE_GEN_URL = f"{AGENT_BASE_URL}/api/v2/exe"
REPORTING_CHECK_URL = f"{AGENT_BASE_URL}/api/v2/reporting/check"
REPORTING_DL_URL = f"{AGENT_BASE_URL}/api/v2/reporting/download/?"
WEBTAR_DL_URL = f"{AGENT_BASE_URL}/api/v2/webtar/?"
if "GHACTIONS" in os.environ:
DEBUG = False
@@ -164,6 +214,11 @@ INSTALLED_APPS = [
"knox",
"corsheaders",
"accounts",
"allauth",
"allauth.account",
"allauth.socialaccount",
"allauth.socialaccount.providers.openid_connect",
"allauth.headless",
"apiv3",
"clients",
"agents",
@@ -178,6 +233,7 @@ INSTALLED_APPS = [
"scripts",
"alerts",
"ee.reporting",
"ee.sso",
]
CHANNEL_LAYERS = {
@@ -189,6 +245,7 @@ CHANNEL_LAYERS = {
},
}
# silence cache key length warnings
import warnings # noqa
@@ -216,6 +273,8 @@ MIDDLEWARE = [
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"tacticalrmm.middleware.AuditMiddleware",
"allauth.account.middleware.AccountMiddleware",
"ee.sso.middleware.SSOIconMiddleware",
]
if SWAGGER_ENABLED:
@@ -326,25 +385,3 @@ LOGGING = {
"trmm": {"handlers": ["trmm"], "level": get_log_level(), "propagate": False},
},
}
if "GHACTIONS" in os.environ:
print("-----------------------GHACTIONS----------------------------")
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "pipeline",
"USER": "pipeline",
"PASSWORD": "pipeline123456",
"HOST": "127.0.0.1",
"PORT": "",
}
}
SECRET_KEY = "abcdefghijklmnoptravis123456789"
ALLOWED_HOSTS = ["api.example.com"]
ADMIN_URL = "abc123456/"
CORS_ORIGIN_WHITELIST = ["https://rmm.example.com"]
MESH_USERNAME = "pipeline"
MESH_SITE = "https://example.com"
MESH_TOKEN_KEY = "bd65e957a1e70c622d32523f61508400d6cd0937001a7ac12042227eba0b9ed625233851a316d4f489f02994145f74537a331415d00047dbbf13d940f556806dffe7a8ce1de216dc49edbad0c1a7399c"
REDIS_HOST = "localhost"

View File

@@ -2,7 +2,8 @@ from django.conf import settings
from django.urls import include, path, register_converter
from knox import views as knox_views
from accounts.views import CheckCreds, CheckCredsV2, LoginView, LoginViewV2
from accounts.views import CheckCredsV2, LoginViewV2
from ee.sso.urls import allauth_urls
# from agents.consumers import SendCMD
from core.consumers import DashInfo, TerminalConsumer
@@ -25,8 +26,6 @@ urlpatterns = [
path("", home),
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")),
@@ -46,6 +45,12 @@ urlpatterns = [
path("reporting/", include("ee.reporting.urls")),
]
if not getattr(settings, "TRMM_DISABLE_SSO", False):
urlpatterns += (
path("_allauth/", include(allauth_urls)),
path("accounts/", include("ee.sso.urls")),
)
if getattr(settings, "BETA_API_ENABLED", False):
urlpatterns += (path("beta/v1/", include("beta.v1.urls")),)

View File

@@ -0,0 +1,23 @@
# this file must not import anything from django settings to avoid circular import issues
from urllib.parse import urlparse
import tldextract
def get_webdomain(url: str) -> str:
return urlparse(url).netloc
def get_root_domain(subdomain) -> str:
no_fetch_extract = tldextract.TLDExtract(suffix_list_urls=())
extracted = no_fetch_extract(subdomain)
return f"{extracted.domain}.{extracted.suffix}"
def get_backend_url(subdomain, proto, port) -> str:
url = f"{proto}://{subdomain}"
if port:
url = f"{url}:{port}"
return url

View File

@@ -30,3 +30,6 @@ TRMM_DISABLE_WEB_TERMINAL=False
# disable server side scripts
TRMM_DISABLE_SERVER_SCRIPTS=False
# disable sso
TRMM_DISABLE_SSO=False

View File

@@ -20,6 +20,7 @@ set -e
: "${SKIP_UWSGI_CONFIG:=0}"
: "${TRMM_DISABLE_WEB_TERMINAL:=False}"
: "${TRMM_DISABLE_SERVER_SCRIPTS:=False}"
: "${TRMM_DISABLE_SSO:=False}"
: "${CERT_PRIV_PATH:=${TACTICAL_DIR}/certs/privkey.pem}"
: "${CERT_PUB_PATH:=${TACTICAL_DIR}/certs/fullchain.pem}"
@@ -71,6 +72,7 @@ if [ "$1" = 'tactical-init' ]; then
MESH_TOKEN=$(cat ${TACTICAL_DIR}/tmp/mesh_token)
ADMINURL=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 70 | head -n 1)
DJANGO_SEKRET=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 80 | head -n 1)
BASE_DOMAIN=$(echo "import tldextract; no_fetch_extract = tldextract.TLDExtract(suffix_list_urls=()); extracted = no_fetch_extract('${API_HOST}'); print(f'{extracted.domain}.{extracted.suffix}')" | python)
localvars="$(
cat <<EOF
@@ -88,13 +90,17 @@ LOG_DIR = '/opt/tactical/api/tacticalrmm/private/log'
SCRIPTS_DIR = '/opt/tactical/community-scripts'
ALLOWED_HOSTS = ['${API_HOST}', 'tactical-backend']
ALLOWED_HOSTS = ['${API_HOST}', '${APP_HOST}', 'tactical-backend']
ADMIN_URL = '${ADMINURL}/'
CORS_ORIGIN_WHITELIST = [
'https://${APP_HOST}'
]
CORS_ORIGIN_WHITELIST = ['https://${APP_HOST}']
SESSION_COOKIE_DOMAIN = '${BASE_DOMAIN}'
CSRF_COOKIE_DOMAIN = '${BASE_DOMAIN}'
CSRF_TRUSTED_ORIGINS = ['https://${API_HOST}', 'https://${APP_HOST}']
HEADLESS_FRONTEND_URLS = {'socialaccount_login_error': 'https://${APP_HOST}/account/provider/callback'}
DATABASES = {
'default': {
@@ -115,6 +121,7 @@ MESH_WS_URL = '${MESH_WS_URL}'
ADMIN_ENABLED = False
TRMM_DISABLE_WEB_TERMINAL = ${TRMM_DISABLE_WEB_TERMINAL}
TRMM_DISABLE_SERVER_SCRIPTS = ${TRMM_DISABLE_SERVER_SCRIPTS}
TRMM_DISABLE_SSO = ${TRMM_DISABLE_SSO}
EOF
)"

View File

@@ -64,6 +64,7 @@ services:
TRMM_PASS: ${TRMM_PASS}
TRMM_DISABLE_WEB_TERMINAL: ${TRMM_DISABLE_WEB_TERMINAL}
TRMM_DISABLE_SERVER_SCRIPTS: ${TRMM_DISABLE_SERVER_SCRIPTS}
TRMM_DISABLE_SSO: ${TRMM_DISABLE_SSO}
depends_on:
- tactical-postgres
- tactical-meshcentral

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash
SCRIPT_VERSION="60"
SCRIPT_VERSION="61"
SCRIPT_URL='https://raw.githubusercontent.com/amidaware/tacticalrmm/master/restore.sh'
sudo apt update
@@ -15,6 +15,7 @@ NC='\033[0m'
SCRIPTS_DIR='/opt/trmm-community-scripts'
PYTHON_VER='3.11.8'
SETTINGS_FILE='/rmm/api/tacticalrmm/tacticalrmm/settings.py'
local_settings='/rmm/api/tacticalrmm/tacticalrmm/local_settings.py'
TMP_FILE=$(mktemp -p "" "rmmrestore_XXXXXXXXXX")
curl -s -L "${SCRIPT_URL}" >${TMP_FILE}
@@ -445,8 +446,8 @@ sudo chmod +x /usr/local/bin/nats-api
print_green 'Restoring the trmm database'
pgusername=$(grep -w USER /rmm/api/tacticalrmm/tacticalrmm/local_settings.py | sed 's/^.*: //' | sed 's/.//' | sed -r 's/.{2}$//')
pgpw=$(grep -w PASSWORD /rmm/api/tacticalrmm/tacticalrmm/local_settings.py | sed 's/^.*: //' | sed 's/.//' | sed -r 's/.{2}$//')
pgusername=$(grep -w USER $local_settings | sed 's/^.*: //' | sed 's/.//' | sed -r 's/.{2}$//')
pgpw=$(grep -w PASSWORD $local_settings | sed 's/^.*: //' | sed 's/.//' | sed -r 's/.{2}$//')
sudo -iu postgres psql -c "CREATE DATABASE tacticalrmm"
sudo -iu postgres psql -c "CREATE USER ${pgusername} WITH PASSWORD '${pgpw}'"