Skip to content

Commit

Permalink
Merge pull request #2001 from sadnub/sso
Browse files Browse the repository at this point in the history
feat: single sign-on #508
  • Loading branch information
wh1te909 authored Nov 20, 2024
2 parents 77a916e + 91c33b0 commit ddba83b
Show file tree
Hide file tree
Showing 32 changed files with 825 additions and 167 deletions.
31 changes: 19 additions & 12 deletions .devcontainer/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -49,8 +49,11 @@ function django_setup {
MESH_TOKEN="$(cat ${TACTICAL_DIR}/tmp/mesh_token)"

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
Expand All @@ -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': {
Expand Down Expand Up @@ -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
Expand All @@ -118,9 +126,8 @@ EOF
"${VIRTUAL_ENV}"/bin/python manage.py create_natsapi_conf
"${VIRTUAL_ENV}"/bin/python manage.py create_installer_user
"${VIRTUAL_ENV}"/bin/python manage.py post_update_tasks


# create super user
# 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
}

Expand Down
5 changes: 3 additions & 2 deletions api/tacticalrmm/accounts/management/commands/reset_2fa.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -26,7 +27,7 @@ def handle(self, *args, **kwargs):
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(
Expand Down
5 changes: 5 additions & 0 deletions api/tacticalrmm/accounts/models.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -73,6 +74,10 @@ def mesh_username(self):
# 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
Expand Down
12 changes: 12 additions & 0 deletions api/tacticalrmm/accounts/permissions.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -40,3 +41,14 @@ def has_permission(self, r, view) -> bool:
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
5 changes: 3 additions & 2 deletions api/tacticalrmm/accounts/serializers.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -63,7 +64,7 @@ class Meta:

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])
)


Expand Down
1 change: 1 addition & 0 deletions api/tacticalrmm/accounts/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

class TestAccounts(TacticalTestCase):
def setUp(self):
self.setup_coresettings()
self.setup_client()
self.bob = User(username="bob")
self.bob.set_password("hunter2")
Expand Down
4 changes: 4 additions & 0 deletions api/tacticalrmm/accounts/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
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
Loading

0 comments on commit ddba83b

Please sign in to comment.