Files
tabby-web/backend/tabby/settings.py
Brian Olson 9544e363c9 feat: add Azure AD single-tenant authentication support
Add support for AzureADTenantOAuth2 backend which allows restricting
authentication to a specific Azure AD tenant (organization).

Configuration:
- SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_KEY: Azure app client ID
- SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_SECRET: Azure app client secret
- SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_TENANT_ID: Azure AD Directory ID

This enables organizations to restrict Tabby Web access to only users
from their Azure AD tenant, preventing personal Microsoft accounts or
users from other organizations from logging in.

Fixes #120
2025-12-31 20:46:53 -05:00

264 lines
7.2 KiB
Python

import os
import dj_database_url
from dotenv import load_dotenv
from pathlib import Path
from urllib.parse import urlparse
load_dotenv()
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "django-insecure")
DEBUG = bool(os.getenv("DEBUG", False))
ALLOWED_HOSTS = ["*"]
USE_X_FORWARDED_HOST = True
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
# Application definition
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"social_django",
"corsheaders",
"tabby.app",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"corsheaders.middleware.CorsMiddleware",
"tabby.middleware.TokenMiddleware",
"tabby.middleware.GAMiddleware",
]
ROOT_URLCONF = "tabby.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "tabby.wsgi.application"
DATABASES = {"default": dj_database_url.config(conn_max_age=600)}
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
}
}
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
AUTH_USER_MODEL = "app.User"
REST_FRAMEWORK = {
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",)
}
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_L10N = True
USE_TZ = True
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"simple": {"format": "%(levelname)s %(message)s"},
},
"handlers": {
"console": {
"level": "INFO",
"class": "logging.StreamHandler",
"formatter": "simple",
},
},
"loggers": {
"": {
"handlers": ["console"],
"propagate": False,
"level": "INFO",
},
},
}
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
CSRF_USE_SESSIONS = False
CSRF_COOKIE_HTTPONLY = False
CSRF_COOKIE_NAME = "XSRF-TOKEN"
CSRF_HEADER_NAME = "HTTP_X_XSRF_TOKEN"
AUTHENTICATION_BACKENDS = (
"social_core.backends.github.GithubOAuth2",
"social_core.backends.gitlab.GitLabOAuth2",
"social_core.backends.azuread.AzureADOAuth2",
"social_core.backends.azuread_tenant.AzureADTenantOAuth2", # Single-tenant Azure AD
"social_core.backends.microsoft.MicrosoftOAuth2",
"social_core.backends.google.GoogleOAuth2",
"django.contrib.auth.backends.ModelBackend",
)
SOCIAL_AUTH_GITHUB_SCOPE = ["read:user", "user:email"]
SOCIAL_AUTH_PIPELINE = (
"social_core.pipeline.social_auth.social_details",
"social_core.pipeline.social_auth.social_uid",
"social_core.pipeline.social_auth.auth_allowed",
"social_core.pipeline.social_auth.social_user",
"social_core.pipeline.user.get_username",
"social_core.pipeline.social_auth.associate_by_email",
"social_core.pipeline.user.create_user",
"social_core.pipeline.social_auth.associate_user",
"social_core.pipeline.social_auth.load_extra_data",
"social_core.pipeline.user.user_details",
)
APP_DIST_STORAGE = os.getenv("APP_DIST_STORAGE", "file://" + str(BASE_DIR / "app-dist"))
NPM_REGISTRY = os.getenv("NPM_REGISTRY", "https://registry.npmjs.org").rstrip("/")
FRONTEND_BUILD_DIR = Path(
os.getenv("FRONTEND_BUILD_DIR", BASE_DIR / "../frontend/build")
)
FRONTEND_URL = None
CORS_EXTRA_URL = None
BACKEND_URL = None
GITHUB_ELIGIBLE_SPONSORSHIPS = None
for key in [
"CORS_EXTRA_URL",
"FRONTEND_URL",
"BACKEND_URL",
"SOCIAL_AUTH_GITHUB_KEY",
"SOCIAL_AUTH_GITHUB_SECRET",
"SOCIAL_AUTH_GITLAB_KEY",
"SOCIAL_AUTH_GITLAB_SECRET",
"SOCIAL_AUTH_GOOGLE_OAUTH2_KEY",
"SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET",
"SOCIAL_AUTH_MICROSOFT_GRAPH_KEY",
"SOCIAL_AUTH_MICROSOFT_GRAPH_SECRET",
# Azure AD single-tenant (use instead of multi-tenant for org-only access)
"SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_KEY",
"SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_SECRET",
"SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_TENANT_ID",
"CONNECTION_GATEWAY_AUTH_CA",
"CONNECTION_GATEWAY_AUTH_CERTIFICATE",
"CONNECTION_GATEWAY_AUTH_KEY",
"GITHUB_ELIGIBLE_SPONSORSHIPS",
"GITHUB_SPONSORS_MIN_PAYMENT",
"GA_ID",
"GA_DOMAIN",
]:
globals()[key] = os.getenv(key)
for key in [
"GITHUB_SPONSORS_MIN_PAYMENT",
]:
globals()[key] = int(globals()[key]) if globals()[key] else None
for key in [
"CONNECTION_GATEWAY_AUTH_CA",
"CONNECTION_GATEWAY_AUTH_CERTIFICATE",
"CONNECTION_GATEWAY_AUTH_KEY",
]:
v = globals()[key]
if v and not os.path.exists(v):
raise ValueError(f"{v} does not exist")
if GITHUB_ELIGIBLE_SPONSORSHIPS:
GITHUB_ELIGIBLE_SPONSORSHIPS = GITHUB_ELIGIBLE_SPONSORSHIPS.split(",")
else:
GITHUB_ELIGIBLE_SPONSORSHIPS = []
STATIC_URL = "/static/"
if FRONTEND_BUILD_DIR.exists():
STATICFILES_DIRS = [FRONTEND_BUILD_DIR]
STATIC_ROOT = BASE_DIR / "public"
if FRONTEND_URL or CORS_EXTRA_URL:
cors_url = CORS_EXTRA_URL or FRONTEND_URL
CORS_ALLOWED_ORIGINS = [cors_url, "https://tabby.sh"]
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_HEADERS = [
"accept",
"accept-encoding",
"authorization",
"content-type",
"dnt",
"origin",
"user-agent",
"x-xsrf-token",
"x-requested-with",
]
cors_domain = urlparse(cors_url).hostname
CSRF_TRUSTED_ORIGINS = [cors_domain]
if BACKEND_URL:
CSRF_TRUSTED_ORIGINS.append(urlparse(BACKEND_URL).hostname)
cors_url = cors_url.rstrip("/")
if cors_url.startswith("https://"):
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
else:
FRONTEND_URL = ""
if FRONTEND_URL:
LOGIN_REDIRECT_URL = FRONTEND_URL
frontend_domain = urlparse(FRONTEND_URL).hostname
SESSION_COOKIE_DOMAIN = os.getenv("SESSION_COOKIE_DOMAIN", cors_domain)
SESSION_COOKIE_SAMESITE = None
CSRF_COOKIE_DOMAIN = cors_domain
if FRONTEND_URL.startswith("https://"):
CSRF_COOKIE_SECURE = True
else:
LOGIN_REDIRECT_URL = '/'