Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c7ea6748d | |||
| 33a0e49acd | |||
| ce4bca0186 | |||
| 4b07e996ad | |||
| 720c9c11b0 |
+1
-1
@@ -1 +1 @@
|
|||||||
2026-03-22T17:37:50Z
|
2026-03-22T17:46:27Z
|
||||||
|
|||||||
@@ -10,6 +10,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
<!-- version list -->
|
<!-- version list -->
|
||||||
|
|
||||||
|
## v0.171.3 (2026-03-22)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **ui**: Add missing opening script tag in base.html Sentry block
|
||||||
|
([`425472c`](https://github.com/christianlouis/DocuElevate/commit/425472c839b3564c20a29b6e983fa6b9e7d6cf9c))
|
||||||
|
|
||||||
|
|
||||||
## v0.171.2 (2026-03-22)
|
## v0.171.2 (2026-03-22)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
+6
-6
@@ -1,10 +1,10 @@
|
|||||||
DocuElevate Build Information
|
DocuElevate Build Information
|
||||||
==============================
|
==============================
|
||||||
Version: 0.171.2
|
Version: 0.171.3
|
||||||
Build Date: 2026-03-22T17:37:50Z
|
Build Date: 2026-03-22T17:46:27Z
|
||||||
Git Commit: 3e1b35293006b76737c7d2edfc50a14b003ef266
|
Git Commit: 720c9c11b05a7af6ac33478ecab6cc80c24c83e1
|
||||||
Git Short SHA: 3e1b352
|
Git Short SHA: 720c9c1
|
||||||
Git Branch: main
|
Git Branch: main
|
||||||
Commit Date: 2026-03-22T18:37:25+01:00
|
Commit Date: 2026-03-22T18:46:06+01:00
|
||||||
Build Timestamp: 2026-03-22T17:37:50Z
|
Build Timestamp: 2026-03-22T17:46:27Z
|
||||||
==============================
|
==============================
|
||||||
|
|||||||
+219
-147
@@ -45,78 +45,27 @@ OAUTH_PROVIDER_NAME = "Single Sign-On"
|
|||||||
# Social login providers that are enabled and registered
|
# Social login providers that are enabled and registered
|
||||||
SOCIAL_PROVIDERS: dict[str, dict[str, str]] = {}
|
SOCIAL_PROVIDERS: dict[str, dict[str, str]] = {}
|
||||||
|
|
||||||
if AUTH_ENABLED and settings.authentik_client_id and settings.authentik_client_secret:
|
|
||||||
oauth.register(
|
|
||||||
name="authentik",
|
|
||||||
client_id=settings.authentik_client_id,
|
|
||||||
client_secret=settings.authentik_client_secret,
|
|
||||||
server_metadata_url=settings.authentik_config_url,
|
|
||||||
client_kwargs={"scope": "openid profile email"},
|
|
||||||
)
|
|
||||||
OAUTH_CONFIGURED = True
|
|
||||||
OAUTH_PROVIDER_NAME = settings.oauth_provider_name or "Authentik SSO"
|
|
||||||
|
|
||||||
# --- Social Login Providers ---------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
if AUTH_ENABLED and settings.social_auth_google_enabled:
|
# Helpers for dynamic (re-)registration of OAuth providers
|
||||||
# Determine which credentials to use for Google social login
|
# ---------------------------------------------------------------------------
|
||||||
_google_client_id = settings.social_auth_google_client_id
|
|
||||||
_google_client_secret = settings.social_auth_google_client_secret
|
|
||||||
if settings.social_auth_google_use_global_credentials and not (_google_client_id and _google_client_secret):
|
|
||||||
_google_client_id = settings.google_drive_client_id
|
|
||||||
_google_client_secret = settings.google_drive_client_secret
|
|
||||||
|
|
||||||
if _google_client_id and _google_client_secret:
|
|
||||||
oauth.register(
|
|
||||||
name="google",
|
|
||||||
client_id=_google_client_id,
|
|
||||||
client_secret=_google_client_secret,
|
|
||||||
server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
|
|
||||||
client_kwargs={"scope": "openid profile email"},
|
|
||||||
)
|
|
||||||
SOCIAL_PROVIDERS["google"] = {"name": "Google", "icon": "fab fa-google", "color": "red"}
|
|
||||||
logger.info("Social login provider registered: Google")
|
|
||||||
else:
|
|
||||||
logger.warning("SOCIAL_AUTH_GOOGLE_ENABLED=true but client ID/secret not configured")
|
|
||||||
|
|
||||||
if AUTH_ENABLED and settings.social_auth_microsoft_enabled:
|
def _register_oauth_client(name: str, **kwargs: object) -> None:
|
||||||
# Determine which credentials to use for Microsoft social login
|
"""Register (or re-register) an authlib OAuth client, clearing any cached instance.
|
||||||
_microsoft_client_id = settings.social_auth_microsoft_client_id
|
|
||||||
_microsoft_client_secret = settings.social_auth_microsoft_client_secret
|
|
||||||
if settings.social_auth_microsoft_use_global_credentials and not (
|
|
||||||
_microsoft_client_id and _microsoft_client_secret
|
|
||||||
):
|
|
||||||
_microsoft_client_id = settings.onedrive_client_id
|
|
||||||
_microsoft_client_secret = settings.onedrive_client_secret
|
|
||||||
|
|
||||||
if _microsoft_client_id and _microsoft_client_secret:
|
authlib caches the constructed client object in ``oauth._clients`` after the
|
||||||
tenant = settings.social_auth_microsoft_tenant or "common"
|
first ``register()`` call. Subsequent ``register()`` calls overwrite the
|
||||||
oauth.register(
|
registry entry but the stale cached client is still returned by
|
||||||
name="microsoft",
|
``create_client()`` / ``__getattr__``. Popping the name from ``_clients``
|
||||||
client_id=_microsoft_client_id,
|
before re-registering ensures the new credentials are picked up immediately.
|
||||||
client_secret=_microsoft_client_secret,
|
|
||||||
server_metadata_url=f"https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration",
|
|
||||||
client_kwargs={"scope": "openid profile email"},
|
|
||||||
)
|
|
||||||
SOCIAL_PROVIDERS["microsoft"] = {"name": "Microsoft", "icon": "fab fa-microsoft", "color": "blue"}
|
|
||||||
logger.info("Social login provider registered: Microsoft (tenant=%s)", tenant)
|
|
||||||
else:
|
|
||||||
logger.warning("SOCIAL_AUTH_MICROSOFT_ENABLED=true but client ID/secret not configured")
|
|
||||||
|
|
||||||
if AUTH_ENABLED and settings.social_auth_apple_enabled:
|
Args:
|
||||||
if settings.social_auth_apple_client_id and settings.social_auth_apple_team_id:
|
name: Provider name (e.g. ``"google"``, ``"github"``).
|
||||||
oauth.register(
|
**kwargs: Keyword arguments forwarded verbatim to ``oauth.register()``.
|
||||||
name="apple",
|
"""
|
||||||
client_id=settings.social_auth_apple_client_id,
|
oauth._clients.pop(name, None)
|
||||||
server_metadata_url="https://appleid.apple.com/.well-known/openid-configuration",
|
oauth.register(name, **kwargs)
|
||||||
client_kwargs={
|
|
||||||
"scope": "openid name email",
|
|
||||||
"response_mode": "form_post",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
SOCIAL_PROVIDERS["apple"] = {"name": "Apple", "icon": "fab fa-apple", "color": "gray"}
|
|
||||||
logger.info("Social login provider registered: Apple")
|
|
||||||
else:
|
|
||||||
logger.warning("SOCIAL_AUTH_APPLE_ENABLED=true but client ID/team ID not configured")
|
|
||||||
|
|
||||||
|
|
||||||
def _dropbox_userinfo_compliance_fix(client, user_cls, token, data):
|
def _dropbox_userinfo_compliance_fix(client, user_cls, token, data):
|
||||||
@@ -145,92 +94,215 @@ def _dropbox_userinfo_compliance_fix(client, user_cls, token, data):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
if AUTH_ENABLED and settings.social_auth_dropbox_enabled:
|
def _setup_social_providers() -> None:
|
||||||
# Determine which credentials to use for Dropbox social login
|
"""Register all configured OAuth / social-login providers from current settings.
|
||||||
_dropbox_client_id = settings.social_auth_dropbox_client_id
|
|
||||||
_dropbox_client_secret = settings.social_auth_dropbox_client_secret
|
|
||||||
if settings.social_auth_dropbox_use_global_credentials and not (_dropbox_client_id and _dropbox_client_secret):
|
|
||||||
_dropbox_client_id = settings.dropbox_app_key
|
|
||||||
_dropbox_client_secret = settings.dropbox_app_secret
|
|
||||||
|
|
||||||
if _dropbox_client_id and _dropbox_client_secret:
|
This function is **idempotent**: it clears ``SOCIAL_PROVIDERS``,
|
||||||
oauth.register(
|
``OAUTH_CONFIGURED``, and ``OAUTH_PROVIDER_NAME`` before rebuilding them,
|
||||||
name="dropbox",
|
and calls :func:`_register_oauth_client` (which also clears the authlib
|
||||||
client_id=_dropbox_client_id,
|
client cache) so that credential changes in the database are reflected
|
||||||
client_secret=_dropbox_client_secret,
|
without an application restart.
|
||||||
authorize_url="https://www.dropbox.com/oauth2/authorize",
|
|
||||||
access_token_url="https://api.dropboxapi.com/oauth2/token",
|
|
||||||
userinfo_endpoint="https://api.dropboxapi.com/2/users/get_current_account",
|
|
||||||
userinfo_compliance_fix=_dropbox_userinfo_compliance_fix,
|
|
||||||
client_kwargs={
|
|
||||||
"token_endpoint_auth_method": "client_secret_post",
|
|
||||||
"token_access_type": "offline",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
SOCIAL_PROVIDERS["dropbox"] = {"name": "Dropbox", "icon": "fab fa-dropbox", "color": "blue"}
|
|
||||||
logger.info("Social login provider registered: Dropbox")
|
|
||||||
else:
|
|
||||||
logger.warning("SOCIAL_AUTH_DROPBOX_ENABLED=true but client ID/secret not configured")
|
|
||||||
|
|
||||||
if AUTH_ENABLED and settings.social_auth_github_enabled:
|
Can safely be called multiple times, e.g. after a settings reload.
|
||||||
if settings.social_auth_github_client_id and settings.social_auth_github_client_secret:
|
"""
|
||||||
oauth.register(
|
global OAUTH_CONFIGURED, OAUTH_PROVIDER_NAME
|
||||||
name="github",
|
|
||||||
client_id=settings.social_auth_github_client_id,
|
|
||||||
client_secret=settings.social_auth_github_client_secret,
|
|
||||||
authorize_url="https://github.com/login/oauth/authorize",
|
|
||||||
access_token_url="https://github.com/login/oauth/access_token",
|
|
||||||
userinfo_endpoint="https://api.github.com/user",
|
|
||||||
client_kwargs={"scope": "read:user user:email"},
|
|
||||||
)
|
|
||||||
SOCIAL_PROVIDERS["github"] = {"name": "GitHub", "icon": "fab fa-github", "color": "gray"}
|
|
||||||
logger.info("Social login provider registered: GitHub")
|
|
||||||
else:
|
|
||||||
logger.warning("SOCIAL_AUTH_GITHUB_ENABLED=true but client ID/secret not configured")
|
|
||||||
|
|
||||||
if AUTH_ENABLED and settings.social_auth_keycloak_enabled:
|
SOCIAL_PROVIDERS.clear()
|
||||||
_kc_server = settings.social_auth_keycloak_server_url
|
OAUTH_CONFIGURED = False
|
||||||
_kc_realm = settings.social_auth_keycloak_realm
|
OAUTH_PROVIDER_NAME = "Single Sign-On"
|
||||||
if (
|
|
||||||
settings.social_auth_keycloak_client_id
|
if not AUTH_ENABLED:
|
||||||
and settings.social_auth_keycloak_client_secret
|
return
|
||||||
and _kc_server
|
|
||||||
and _kc_realm
|
# --- Authentik / OIDC ---
|
||||||
):
|
if settings.authentik_client_id and settings.authentik_client_secret:
|
||||||
_kc_base = f"{_kc_server.rstrip('/')}/realms/{_kc_realm}"
|
_register_oauth_client(
|
||||||
oauth.register(
|
"authentik",
|
||||||
name="keycloak",
|
client_id=settings.authentik_client_id,
|
||||||
client_id=settings.social_auth_keycloak_client_id,
|
client_secret=settings.authentik_client_secret,
|
||||||
client_secret=settings.social_auth_keycloak_client_secret,
|
server_metadata_url=settings.authentik_config_url,
|
||||||
server_metadata_url=f"{_kc_base}/.well-known/openid-configuration",
|
|
||||||
client_kwargs={"scope": "openid profile email"},
|
client_kwargs={"scope": "openid profile email"},
|
||||||
)
|
)
|
||||||
SOCIAL_PROVIDERS["keycloak"] = {"name": "Keycloak", "icon": "fas fa-key", "color": "gray"}
|
OAUTH_CONFIGURED = True
|
||||||
logger.info("Social login provider registered: Keycloak (realm=%s)", _kc_realm)
|
OAUTH_PROVIDER_NAME = settings.oauth_provider_name or "Authentik SSO"
|
||||||
else:
|
|
||||||
logger.warning("SOCIAL_AUTH_KEYCLOAK_ENABLED=true but required settings not configured")
|
|
||||||
|
|
||||||
if AUTH_ENABLED and settings.social_auth_generic_oauth2_enabled:
|
# --- Social Login Providers ---
|
||||||
if (
|
|
||||||
settings.social_auth_generic_oauth2_client_id
|
# Google
|
||||||
and settings.social_auth_generic_oauth2_client_secret
|
if settings.social_auth_google_enabled:
|
||||||
and settings.social_auth_generic_oauth2_authorize_url
|
_google_client_id = settings.social_auth_google_client_id
|
||||||
and settings.social_auth_generic_oauth2_token_url
|
_google_client_secret = settings.social_auth_google_client_secret
|
||||||
):
|
if settings.social_auth_google_use_global_credentials and not (_google_client_id and _google_client_secret):
|
||||||
oauth.register(
|
_google_client_id = settings.google_drive_client_id
|
||||||
name="generic_oauth2",
|
_google_client_secret = settings.google_drive_client_secret
|
||||||
client_id=settings.social_auth_generic_oauth2_client_id,
|
|
||||||
client_secret=settings.social_auth_generic_oauth2_client_secret,
|
if _google_client_id and _google_client_secret:
|
||||||
authorize_url=settings.social_auth_generic_oauth2_authorize_url,
|
_register_oauth_client(
|
||||||
access_token_url=settings.social_auth_generic_oauth2_token_url,
|
"google",
|
||||||
userinfo_endpoint=settings.social_auth_generic_oauth2_userinfo_url,
|
client_id=_google_client_id,
|
||||||
client_kwargs={"scope": settings.social_auth_generic_oauth2_scope},
|
client_secret=_google_client_secret,
|
||||||
)
|
server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
|
||||||
_generic_name = settings.social_auth_generic_oauth2_name or "OAuth2"
|
client_kwargs={"scope": "openid profile email"},
|
||||||
SOCIAL_PROVIDERS["generic_oauth2"] = {"name": _generic_name, "icon": "fas fa-sign-in-alt", "color": "indigo"}
|
)
|
||||||
logger.info("Social login provider registered: Generic OAuth2 (%s)", _generic_name)
|
SOCIAL_PROVIDERS["google"] = {"name": "Google", "icon": "fab fa-google", "color": "red"}
|
||||||
else:
|
logger.info("Social login provider registered: Google")
|
||||||
logger.warning("SOCIAL_AUTH_GENERIC_OAUTH2_ENABLED=true but required settings not configured")
|
else:
|
||||||
|
logger.warning("SOCIAL_AUTH_GOOGLE_ENABLED=true but client ID/secret not configured")
|
||||||
|
|
||||||
|
# Microsoft
|
||||||
|
if settings.social_auth_microsoft_enabled:
|
||||||
|
_microsoft_client_id = settings.social_auth_microsoft_client_id
|
||||||
|
_microsoft_client_secret = settings.social_auth_microsoft_client_secret
|
||||||
|
if settings.social_auth_microsoft_use_global_credentials and not (
|
||||||
|
_microsoft_client_id and _microsoft_client_secret
|
||||||
|
):
|
||||||
|
_microsoft_client_id = settings.onedrive_client_id
|
||||||
|
_microsoft_client_secret = settings.onedrive_client_secret
|
||||||
|
|
||||||
|
if _microsoft_client_id and _microsoft_client_secret:
|
||||||
|
tenant = settings.social_auth_microsoft_tenant or "common"
|
||||||
|
_register_oauth_client(
|
||||||
|
"microsoft",
|
||||||
|
client_id=_microsoft_client_id,
|
||||||
|
client_secret=_microsoft_client_secret,
|
||||||
|
server_metadata_url=f"https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration",
|
||||||
|
client_kwargs={"scope": "openid profile email"},
|
||||||
|
)
|
||||||
|
SOCIAL_PROVIDERS["microsoft"] = {"name": "Microsoft", "icon": "fab fa-microsoft", "color": "blue"}
|
||||||
|
logger.info("Social login provider registered: Microsoft (tenant=%s)", tenant)
|
||||||
|
else:
|
||||||
|
logger.warning("SOCIAL_AUTH_MICROSOFT_ENABLED=true but client ID/secret not configured")
|
||||||
|
|
||||||
|
# Apple
|
||||||
|
if settings.social_auth_apple_enabled:
|
||||||
|
if settings.social_auth_apple_client_id and settings.social_auth_apple_team_id:
|
||||||
|
_register_oauth_client(
|
||||||
|
"apple",
|
||||||
|
client_id=settings.social_auth_apple_client_id,
|
||||||
|
server_metadata_url="https://appleid.apple.com/.well-known/openid-configuration",
|
||||||
|
client_kwargs={
|
||||||
|
"scope": "openid name email",
|
||||||
|
"response_mode": "form_post",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
SOCIAL_PROVIDERS["apple"] = {"name": "Apple", "icon": "fab fa-apple", "color": "gray"}
|
||||||
|
logger.info("Social login provider registered: Apple")
|
||||||
|
else:
|
||||||
|
logger.warning("SOCIAL_AUTH_APPLE_ENABLED=true but client ID/team ID not configured")
|
||||||
|
|
||||||
|
# Dropbox
|
||||||
|
if settings.social_auth_dropbox_enabled:
|
||||||
|
_dropbox_client_id = settings.social_auth_dropbox_client_id
|
||||||
|
_dropbox_client_secret = settings.social_auth_dropbox_client_secret
|
||||||
|
if settings.social_auth_dropbox_use_global_credentials and not (_dropbox_client_id and _dropbox_client_secret):
|
||||||
|
_dropbox_client_id = settings.dropbox_app_key
|
||||||
|
_dropbox_client_secret = settings.dropbox_app_secret
|
||||||
|
|
||||||
|
if _dropbox_client_id and _dropbox_client_secret:
|
||||||
|
_register_oauth_client(
|
||||||
|
"dropbox",
|
||||||
|
client_id=_dropbox_client_id,
|
||||||
|
client_secret=_dropbox_client_secret,
|
||||||
|
authorize_url="https://www.dropbox.com/oauth2/authorize",
|
||||||
|
access_token_url="https://api.dropboxapi.com/oauth2/token",
|
||||||
|
userinfo_endpoint="https://api.dropboxapi.com/2/users/get_current_account",
|
||||||
|
userinfo_compliance_fix=_dropbox_userinfo_compliance_fix,
|
||||||
|
client_kwargs={
|
||||||
|
"token_endpoint_auth_method": "client_secret_post",
|
||||||
|
"token_access_type": "offline",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
SOCIAL_PROVIDERS["dropbox"] = {"name": "Dropbox", "icon": "fab fa-dropbox", "color": "blue"}
|
||||||
|
logger.info("Social login provider registered: Dropbox")
|
||||||
|
else:
|
||||||
|
logger.warning("SOCIAL_AUTH_DROPBOX_ENABLED=true but client ID/secret not configured")
|
||||||
|
|
||||||
|
# GitHub
|
||||||
|
if settings.social_auth_github_enabled:
|
||||||
|
if settings.social_auth_github_client_id and settings.social_auth_github_client_secret:
|
||||||
|
_register_oauth_client(
|
||||||
|
"github",
|
||||||
|
client_id=settings.social_auth_github_client_id,
|
||||||
|
client_secret=settings.social_auth_github_client_secret,
|
||||||
|
authorize_url="https://github.com/login/oauth/authorize",
|
||||||
|
access_token_url="https://github.com/login/oauth/access_token",
|
||||||
|
userinfo_endpoint="https://api.github.com/user",
|
||||||
|
client_kwargs={"scope": "read:user user:email"},
|
||||||
|
)
|
||||||
|
SOCIAL_PROVIDERS["github"] = {"name": "GitHub", "icon": "fab fa-github", "color": "gray"}
|
||||||
|
logger.info("Social login provider registered: GitHub")
|
||||||
|
else:
|
||||||
|
logger.warning("SOCIAL_AUTH_GITHUB_ENABLED=true but client ID/secret not configured")
|
||||||
|
|
||||||
|
# Keycloak
|
||||||
|
if settings.social_auth_keycloak_enabled:
|
||||||
|
_kc_server = settings.social_auth_keycloak_server_url
|
||||||
|
_kc_realm = settings.social_auth_keycloak_realm
|
||||||
|
if (
|
||||||
|
settings.social_auth_keycloak_client_id
|
||||||
|
and settings.social_auth_keycloak_client_secret
|
||||||
|
and _kc_server
|
||||||
|
and _kc_realm
|
||||||
|
):
|
||||||
|
_kc_base = f"{_kc_server.rstrip('/')}/realms/{_kc_realm}"
|
||||||
|
_register_oauth_client(
|
||||||
|
"keycloak",
|
||||||
|
client_id=settings.social_auth_keycloak_client_id,
|
||||||
|
client_secret=settings.social_auth_keycloak_client_secret,
|
||||||
|
server_metadata_url=f"{_kc_base}/.well-known/openid-configuration",
|
||||||
|
client_kwargs={"scope": "openid profile email"},
|
||||||
|
)
|
||||||
|
SOCIAL_PROVIDERS["keycloak"] = {"name": "Keycloak", "icon": "fas fa-key", "color": "gray"}
|
||||||
|
logger.info("Social login provider registered: Keycloak (realm=%s)", _kc_realm)
|
||||||
|
else:
|
||||||
|
logger.warning("SOCIAL_AUTH_KEYCLOAK_ENABLED=true but required settings not configured")
|
||||||
|
|
||||||
|
# Generic OAuth2
|
||||||
|
if settings.social_auth_generic_oauth2_enabled:
|
||||||
|
if (
|
||||||
|
settings.social_auth_generic_oauth2_client_id
|
||||||
|
and settings.social_auth_generic_oauth2_client_secret
|
||||||
|
and settings.social_auth_generic_oauth2_authorize_url
|
||||||
|
and settings.social_auth_generic_oauth2_token_url
|
||||||
|
):
|
||||||
|
_register_oauth_client(
|
||||||
|
"generic_oauth2",
|
||||||
|
client_id=settings.social_auth_generic_oauth2_client_id,
|
||||||
|
client_secret=settings.social_auth_generic_oauth2_client_secret,
|
||||||
|
authorize_url=settings.social_auth_generic_oauth2_authorize_url,
|
||||||
|
access_token_url=settings.social_auth_generic_oauth2_token_url,
|
||||||
|
userinfo_endpoint=settings.social_auth_generic_oauth2_userinfo_url,
|
||||||
|
client_kwargs={"scope": settings.social_auth_generic_oauth2_scope},
|
||||||
|
)
|
||||||
|
_generic_name = settings.social_auth_generic_oauth2_name or "OAuth2"
|
||||||
|
SOCIAL_PROVIDERS["generic_oauth2"] = {
|
||||||
|
"name": _generic_name,
|
||||||
|
"icon": "fas fa-sign-in-alt",
|
||||||
|
"color": "indigo",
|
||||||
|
}
|
||||||
|
logger.info("Social login provider registered: Generic OAuth2 (%s)", _generic_name)
|
||||||
|
else:
|
||||||
|
logger.warning("SOCIAL_AUTH_GENERIC_OAUTH2_ENABLED=true but required settings not configured")
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_social_providers() -> None:
|
||||||
|
"""Re-register all OAuth providers from the *current* settings object.
|
||||||
|
|
||||||
|
Call this after loading or reloading settings from the database so that
|
||||||
|
providers configured (or updated) through the admin UI take effect
|
||||||
|
immediately — **no application restart required**.
|
||||||
|
|
||||||
|
This function is safe to call multiple times and is idempotent.
|
||||||
|
"""
|
||||||
|
logger.info("Refreshing social login provider registrations from current settings")
|
||||||
|
_setup_social_providers()
|
||||||
|
|
||||||
|
|
||||||
|
# Perform the initial registration from environment / default settings at
|
||||||
|
# import time. The lifespan hook and settings_sync will call
|
||||||
|
# refresh_social_providers() again after DB settings are loaded so that
|
||||||
|
# any providers configured only in the database are also active.
|
||||||
|
_setup_social_providers()
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|||||||
+12
@@ -189,6 +189,18 @@ async def lifespan(app: FastAPI):
|
|||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
# Re-register OAuth / social-login providers now that DB settings are
|
||||||
|
# loaded. auth.py runs its initial registration at import time (before
|
||||||
|
# the lifespan runs), so providers that are only configured in the
|
||||||
|
# database would not be registered yet. Calling refresh here ensures
|
||||||
|
# they are active immediately on startup without any manual restart.
|
||||||
|
try:
|
||||||
|
from app.auth import refresh_social_providers
|
||||||
|
|
||||||
|
refresh_social_providers()
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(f"Could not refresh social login providers on startup: {e}")
|
||||||
|
|
||||||
# Initialize Sentry after DB settings are loaded so that values configured
|
# Initialize Sentry after DB settings are loaded so that values configured
|
||||||
# via the database UI (e.g. SENTRY_DSN) are respected in addition to env vars.
|
# via the database UI (e.g. SENTRY_DSN) are respected in addition to env vars.
|
||||||
init_sentry()
|
init_sentry()
|
||||||
|
|||||||
@@ -71,6 +71,16 @@ def notify_settings_updated() -> None:
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(f"Could not reload in-process settings: {exc}")
|
logger.warning(f"Could not reload in-process settings: {exc}")
|
||||||
|
|
||||||
|
# Re-register OAuth / social-login providers so that any provider whose
|
||||||
|
# credentials were just saved (or updated) in the database is active
|
||||||
|
# immediately on the login page — no restart required.
|
||||||
|
try:
|
||||||
|
from app.auth import refresh_social_providers
|
||||||
|
|
||||||
|
refresh_social_providers()
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(f"Could not refresh social login providers after settings update: {exc}")
|
||||||
|
|
||||||
# Re-check OCR language availability in the background whenever settings
|
# Re-check OCR language availability in the background whenever settings
|
||||||
# are updated. This ensures that if a user changes tesseract_language or
|
# are updated. This ensures that if a user changes tesseract_language or
|
||||||
# easyocr_languages via the UI, the new language data is downloaded without
|
# easyocr_languages via the UI, the new language data is downloaded without
|
||||||
|
|||||||
+57
-12
@@ -206,8 +206,6 @@ async def connections_page(request: Request, db: Session = Depends(get_db)):
|
|||||||
SSO settings, and service integrations through a wizard-like interface.
|
SSO settings, and service integrations through a wizard-like interface.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
from app.auth import OAUTH_CONFIGURED, SOCIAL_PROVIDERS
|
|
||||||
|
|
||||||
db_settings = get_all_settings_from_db(db)
|
db_settings = get_all_settings_from_db(db)
|
||||||
|
|
||||||
def _get_effective(key: str):
|
def _get_effective(key: str):
|
||||||
@@ -227,13 +225,14 @@ async def connections_page(request: Request, db: Session = Depends(get_db)):
|
|||||||
services = []
|
services = []
|
||||||
|
|
||||||
# --- SSO (Authentik / OIDC) ---
|
# --- SSO (Authentik / OIDC) ---
|
||||||
|
_oidc_linked = bool(_get_effective("authentik_client_id") and _get_effective("authentik_client_secret"))
|
||||||
services.append(
|
services.append(
|
||||||
{
|
{
|
||||||
"key": "oidc",
|
"key": "oidc",
|
||||||
"name": settings.oauth_provider_name or "Single Sign-On",
|
"name": _get_effective("oauth_provider_name") or "Single Sign-On",
|
||||||
"icon": "fas fa-lock",
|
"icon": "fas fa-lock",
|
||||||
"type": "SSO",
|
"type": "SSO",
|
||||||
"linked": OAUTH_CONFIGURED,
|
"linked": _oidc_linked,
|
||||||
"description": "OpenID Connect SSO provider",
|
"description": "OpenID Connect SSO provider",
|
||||||
"settings_keys": [
|
"settings_keys": [
|
||||||
"authentik_client_id",
|
"authentik_client_id",
|
||||||
@@ -245,13 +244,23 @@ async def connections_page(request: Request, db: Session = Depends(get_db)):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# --- Google ---
|
# --- Google ---
|
||||||
|
_google_id = _get_effective("social_auth_google_client_id")
|
||||||
|
_google_secret = _get_effective("social_auth_google_client_secret")
|
||||||
|
if _is_truthy(_get_effective("social_auth_google_use_global_credentials")) and not (
|
||||||
|
_google_id and _google_secret
|
||||||
|
):
|
||||||
|
_google_id = _google_id or _get_effective("google_drive_client_id")
|
||||||
|
_google_secret = _google_secret or _get_effective("google_drive_client_secret")
|
||||||
|
_google_linked = bool(
|
||||||
|
_is_truthy(_get_effective("social_auth_google_enabled")) and _google_id and _google_secret
|
||||||
|
)
|
||||||
services.append(
|
services.append(
|
||||||
{
|
{
|
||||||
"key": "google",
|
"key": "google",
|
||||||
"name": "Google",
|
"name": "Google",
|
||||||
"icon": "fab fa-google",
|
"icon": "fab fa-google",
|
||||||
"type": "Sign-in authentication",
|
"type": "Sign-in authentication",
|
||||||
"linked": "google" in SOCIAL_PROVIDERS,
|
"linked": _google_linked,
|
||||||
"description": "Sign-in authentication",
|
"description": "Sign-in authentication",
|
||||||
"settings_keys": [
|
"settings_keys": [
|
||||||
"social_auth_google_enabled",
|
"social_auth_google_enabled",
|
||||||
@@ -263,13 +272,18 @@ async def connections_page(request: Request, db: Session = Depends(get_db)):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# --- GitHub ---
|
# --- GitHub ---
|
||||||
|
_github_linked = bool(
|
||||||
|
_is_truthy(_get_effective("social_auth_github_enabled"))
|
||||||
|
and _get_effective("social_auth_github_client_id")
|
||||||
|
and _get_effective("social_auth_github_client_secret")
|
||||||
|
)
|
||||||
services.append(
|
services.append(
|
||||||
{
|
{
|
||||||
"key": "github",
|
"key": "github",
|
||||||
"name": "GitHub",
|
"name": "GitHub",
|
||||||
"icon": "fab fa-github",
|
"icon": "fab fa-github",
|
||||||
"type": "Sign-in authentication",
|
"type": "Sign-in authentication",
|
||||||
"linked": "github" in SOCIAL_PROVIDERS,
|
"linked": _github_linked,
|
||||||
"description": "Sign-in authentication",
|
"description": "Sign-in authentication",
|
||||||
"settings_keys": [
|
"settings_keys": [
|
||||||
"social_auth_github_enabled",
|
"social_auth_github_enabled",
|
||||||
@@ -280,13 +294,19 @@ async def connections_page(request: Request, db: Session = Depends(get_db)):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# --- Microsoft ---
|
# --- Microsoft ---
|
||||||
|
_ms_id = _get_effective("social_auth_microsoft_client_id")
|
||||||
|
_ms_secret = _get_effective("social_auth_microsoft_client_secret")
|
||||||
|
if _is_truthy(_get_effective("social_auth_microsoft_use_global_credentials")) and not (_ms_id and _ms_secret):
|
||||||
|
_ms_id = _ms_id or _get_effective("onedrive_client_id")
|
||||||
|
_ms_secret = _ms_secret or _get_effective("onedrive_client_secret")
|
||||||
|
_microsoft_linked = bool(_is_truthy(_get_effective("social_auth_microsoft_enabled")) and _ms_id and _ms_secret)
|
||||||
services.append(
|
services.append(
|
||||||
{
|
{
|
||||||
"key": "microsoft",
|
"key": "microsoft",
|
||||||
"name": "Microsoft",
|
"name": "Microsoft",
|
||||||
"icon": "fab fa-microsoft",
|
"icon": "fab fa-microsoft",
|
||||||
"type": "Sign-in authentication",
|
"type": "Sign-in authentication",
|
||||||
"linked": "microsoft" in SOCIAL_PROVIDERS,
|
"linked": _microsoft_linked,
|
||||||
"description": "Sign-in authentication",
|
"description": "Sign-in authentication",
|
||||||
"settings_keys": [
|
"settings_keys": [
|
||||||
"social_auth_microsoft_enabled",
|
"social_auth_microsoft_enabled",
|
||||||
@@ -299,13 +319,18 @@ async def connections_page(request: Request, db: Session = Depends(get_db)):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# --- Apple ---
|
# --- Apple ---
|
||||||
|
_apple_linked = bool(
|
||||||
|
_is_truthy(_get_effective("social_auth_apple_enabled"))
|
||||||
|
and _get_effective("social_auth_apple_client_id")
|
||||||
|
and _get_effective("social_auth_apple_team_id")
|
||||||
|
)
|
||||||
services.append(
|
services.append(
|
||||||
{
|
{
|
||||||
"key": "apple",
|
"key": "apple",
|
||||||
"name": "Apple",
|
"name": "Apple",
|
||||||
"icon": "fab fa-apple",
|
"icon": "fab fa-apple",
|
||||||
"type": "Sign-in authentication",
|
"type": "Sign-in authentication",
|
||||||
"linked": "apple" in SOCIAL_PROVIDERS,
|
"linked": _apple_linked,
|
||||||
"description": "Sign-in authentication",
|
"description": "Sign-in authentication",
|
||||||
"settings_keys": [
|
"settings_keys": [
|
||||||
"social_auth_apple_enabled",
|
"social_auth_apple_enabled",
|
||||||
@@ -318,13 +343,19 @@ async def connections_page(request: Request, db: Session = Depends(get_db)):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# --- Dropbox ---
|
# --- Dropbox ---
|
||||||
|
_dbx_id = _get_effective("social_auth_dropbox_client_id")
|
||||||
|
_dbx_secret = _get_effective("social_auth_dropbox_client_secret")
|
||||||
|
if _is_truthy(_get_effective("social_auth_dropbox_use_global_credentials")) and not (_dbx_id and _dbx_secret):
|
||||||
|
_dbx_id = _dbx_id or _get_effective("dropbox_app_key")
|
||||||
|
_dbx_secret = _dbx_secret or _get_effective("dropbox_app_secret")
|
||||||
|
_dropbox_linked = bool(_is_truthy(_get_effective("social_auth_dropbox_enabled")) and _dbx_id and _dbx_secret)
|
||||||
services.append(
|
services.append(
|
||||||
{
|
{
|
||||||
"key": "dropbox",
|
"key": "dropbox",
|
||||||
"name": "Dropbox",
|
"name": "Dropbox",
|
||||||
"icon": "fab fa-dropbox",
|
"icon": "fab fa-dropbox",
|
||||||
"type": "Sign-in authentication",
|
"type": "Sign-in authentication",
|
||||||
"linked": "dropbox" in SOCIAL_PROVIDERS,
|
"linked": _dropbox_linked,
|
||||||
"description": "Sign-in authentication",
|
"description": "Sign-in authentication",
|
||||||
"settings_keys": [
|
"settings_keys": [
|
||||||
"social_auth_dropbox_enabled",
|
"social_auth_dropbox_enabled",
|
||||||
@@ -336,13 +367,20 @@ async def connections_page(request: Request, db: Session = Depends(get_db)):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# --- Keycloak ---
|
# --- Keycloak ---
|
||||||
|
_keycloak_linked = bool(
|
||||||
|
_is_truthy(_get_effective("social_auth_keycloak_enabled"))
|
||||||
|
and _get_effective("social_auth_keycloak_client_id")
|
||||||
|
and _get_effective("social_auth_keycloak_client_secret")
|
||||||
|
and _get_effective("social_auth_keycloak_server_url")
|
||||||
|
and _get_effective("social_auth_keycloak_realm")
|
||||||
|
)
|
||||||
services.append(
|
services.append(
|
||||||
{
|
{
|
||||||
"key": "keycloak",
|
"key": "keycloak",
|
||||||
"name": "Keycloak",
|
"name": "Keycloak",
|
||||||
"icon": "fas fa-key",
|
"icon": "fas fa-key",
|
||||||
"type": "SSO",
|
"type": "SSO",
|
||||||
"linked": "keycloak" in SOCIAL_PROVIDERS,
|
"linked": _keycloak_linked,
|
||||||
"description": "SSO",
|
"description": "SSO",
|
||||||
"settings_keys": [
|
"settings_keys": [
|
||||||
"social_auth_keycloak_enabled",
|
"social_auth_keycloak_enabled",
|
||||||
@@ -355,13 +393,20 @@ async def connections_page(request: Request, db: Session = Depends(get_db)):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# --- Generic OAuth2 ---
|
# --- Generic OAuth2 ---
|
||||||
|
_generic_oauth2_linked = bool(
|
||||||
|
_is_truthy(_get_effective("social_auth_generic_oauth2_enabled"))
|
||||||
|
and _get_effective("social_auth_generic_oauth2_client_id")
|
||||||
|
and _get_effective("social_auth_generic_oauth2_client_secret")
|
||||||
|
and _get_effective("social_auth_generic_oauth2_authorize_url")
|
||||||
|
and _get_effective("social_auth_generic_oauth2_token_url")
|
||||||
|
)
|
||||||
services.append(
|
services.append(
|
||||||
{
|
{
|
||||||
"key": "generic_oauth2",
|
"key": "generic_oauth2",
|
||||||
"name": "Generic OAuth2",
|
"name": "Generic OAuth2",
|
||||||
"icon": "fas fa-sign-in-alt",
|
"icon": "fas fa-sign-in-alt",
|
||||||
"type": "SSO",
|
"type": "SSO",
|
||||||
"linked": "generic_oauth2" in SOCIAL_PROVIDERS,
|
"linked": _generic_oauth2_linked,
|
||||||
"description": "SSO",
|
"description": "SSO",
|
||||||
"settings_keys": [
|
"settings_keys": [
|
||||||
"social_auth_generic_oauth2_enabled",
|
"social_auth_generic_oauth2_enabled",
|
||||||
@@ -474,7 +519,7 @@ async def connections_page(request: Request, db: Session = Depends(get_db)):
|
|||||||
"services": services,
|
"services": services,
|
||||||
"service_settings": service_settings,
|
"service_settings": service_settings,
|
||||||
"sso_auto_login": sso_auto_login,
|
"sso_auto_login": sso_auto_login,
|
||||||
"oauth_configured": OAUTH_CONFIGURED,
|
"oauth_configured": _oidc_linked,
|
||||||
"qr_login_enabled": qr_login_enabled,
|
"qr_login_enabled": qr_login_enabled,
|
||||||
"frontend_url_configured": frontend_url_configured,
|
"frontend_url_configured": frontend_url_configured,
|
||||||
"app_version": settings.version,
|
"app_version": settings.version,
|
||||||
|
|||||||
+169
-2
@@ -268,8 +268,6 @@ class TestConnectionsPageRoute:
|
|||||||
patch("app.views.settings.get_all_settings_from_db", return_value={}),
|
patch("app.views.settings.get_all_settings_from_db", return_value={}),
|
||||||
patch("app.views.settings.templates") as mock_templates,
|
patch("app.views.settings.templates") as mock_templates,
|
||||||
patch("app.views.settings.SETTING_METADATA", {}),
|
patch("app.views.settings.SETTING_METADATA", {}),
|
||||||
patch("app.auth.OAUTH_CONFIGURED", False),
|
|
||||||
patch("app.auth.SOCIAL_PROVIDERS", {}),
|
|
||||||
patch("app.views.settings.get_setting_metadata", return_value={}),
|
patch("app.views.settings.get_setting_metadata", return_value={}),
|
||||||
):
|
):
|
||||||
mock_templates.TemplateResponse.return_value = "response"
|
mock_templates.TemplateResponse.return_value = "response"
|
||||||
@@ -296,6 +294,175 @@ class TestConnectionsPageRoute:
|
|||||||
assert "smtp" in service_keys
|
assert "smtp" in service_keys
|
||||||
assert "telegram" in service_keys
|
assert "telegram" in service_keys
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_connections_page_linked_status_from_db(self):
|
||||||
|
"""Linked status is derived from DB/effective settings, not SOCIAL_PROVIDERS."""
|
||||||
|
from app.views.settings import connections_page
|
||||||
|
|
||||||
|
mock_request = MagicMock()
|
||||||
|
mock_request.session = {"user": {"is_admin": True}}
|
||||||
|
mock_db = MagicMock()
|
||||||
|
|
||||||
|
# Simulate GitHub configured only in DB (not in SOCIAL_PROVIDERS yet)
|
||||||
|
db_values = {
|
||||||
|
"social_auth_github_enabled": "true",
|
||||||
|
"social_auth_github_client_id": "gh-id",
|
||||||
|
"social_auth_github_client_secret": "gh-secret",
|
||||||
|
}
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("app.views.settings.get_all_settings_from_db", return_value=db_values),
|
||||||
|
patch("app.views.settings.templates") as mock_templates,
|
||||||
|
patch("app.views.settings.SETTING_METADATA", {}),
|
||||||
|
patch("app.views.settings.get_setting_metadata", return_value={}),
|
||||||
|
):
|
||||||
|
mock_templates.TemplateResponse.return_value = "response"
|
||||||
|
await connections_page(mock_request, db=mock_db)
|
||||||
|
|
||||||
|
context = mock_templates.TemplateResponse.call_args[0][1]
|
||||||
|
services_by_key = {s["key"]: s for s in context["services"]}
|
||||||
|
|
||||||
|
# GitHub should be linked because DB values say so
|
||||||
|
assert services_by_key["github"]["linked"] is True
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_connections_page_unlinked_when_credentials_missing(self):
|
||||||
|
"""Provider is unlinked when enabled=true but credentials are absent."""
|
||||||
|
from app.views.settings import connections_page
|
||||||
|
|
||||||
|
mock_request = MagicMock()
|
||||||
|
mock_request.session = {"user": {"is_admin": True}}
|
||||||
|
mock_db = MagicMock()
|
||||||
|
|
||||||
|
# enabled but no credentials
|
||||||
|
db_values = {"social_auth_github_enabled": "true"}
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("app.views.settings.get_all_settings_from_db", return_value=db_values),
|
||||||
|
patch("app.views.settings.templates") as mock_templates,
|
||||||
|
patch("app.views.settings.SETTING_METADATA", {}),
|
||||||
|
patch("app.views.settings.get_setting_metadata", return_value={}),
|
||||||
|
):
|
||||||
|
mock_templates.TemplateResponse.return_value = "response"
|
||||||
|
await connections_page(mock_request, db=mock_db)
|
||||||
|
|
||||||
|
context = mock_templates.TemplateResponse.call_args[0][1]
|
||||||
|
services_by_key = {s["key"]: s for s in context["services"]}
|
||||||
|
assert services_by_key["github"]["linked"] is False
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_connections_page_oidc_linked_from_db(self):
|
||||||
|
"""OIDC linked status derives from DB effective settings."""
|
||||||
|
from app.views.settings import connections_page
|
||||||
|
|
||||||
|
mock_request = MagicMock()
|
||||||
|
mock_request.session = {"user": {"is_admin": True}}
|
||||||
|
mock_db = MagicMock()
|
||||||
|
|
||||||
|
db_values = {
|
||||||
|
"authentik_client_id": "my-client-id",
|
||||||
|
"authentik_client_secret": "my-secret",
|
||||||
|
"oauth_provider_name": "My SSO",
|
||||||
|
}
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("app.views.settings.get_all_settings_from_db", return_value=db_values),
|
||||||
|
patch("app.views.settings.templates") as mock_templates,
|
||||||
|
patch("app.views.settings.SETTING_METADATA", {}),
|
||||||
|
patch("app.views.settings.get_setting_metadata", return_value={}),
|
||||||
|
):
|
||||||
|
mock_templates.TemplateResponse.return_value = "response"
|
||||||
|
await connections_page(mock_request, db=mock_db)
|
||||||
|
|
||||||
|
context = mock_templates.TemplateResponse.call_args[0][1]
|
||||||
|
services_by_key = {s["key"]: s for s in context["services"]}
|
||||||
|
assert services_by_key["oidc"]["linked"] is True
|
||||||
|
assert services_by_key["oidc"]["name"] == "My SSO"
|
||||||
|
# oauth_configured template var should also reflect the DB state
|
||||||
|
assert context["oauth_configured"] is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
class TestRefreshSocialProviders:
|
||||||
|
"""Tests for the refresh_social_providers() mechanism."""
|
||||||
|
|
||||||
|
def test_refresh_social_providers_exists(self):
|
||||||
|
"""refresh_social_providers is importable from app.auth."""
|
||||||
|
from app.auth import refresh_social_providers
|
||||||
|
|
||||||
|
assert callable(refresh_social_providers)
|
||||||
|
|
||||||
|
def test_refresh_social_providers_clears_and_repopulates(self):
|
||||||
|
"""After refresh, SOCIAL_PROVIDERS reflects current settings."""
|
||||||
|
import app.auth as auth_module
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch.object(auth_module, "AUTH_ENABLED", True),
|
||||||
|
patch.object(auth_module, "settings") as mock_settings,
|
||||||
|
):
|
||||||
|
mock_settings.authentik_client_id = None
|
||||||
|
mock_settings.authentik_client_secret = None
|
||||||
|
mock_settings.social_auth_google_enabled = True
|
||||||
|
mock_settings.social_auth_google_client_id = "gid"
|
||||||
|
mock_settings.social_auth_google_client_secret = "gsecret"
|
||||||
|
mock_settings.social_auth_google_use_global_credentials = False
|
||||||
|
# All other providers disabled
|
||||||
|
for attr in (
|
||||||
|
"social_auth_microsoft_enabled",
|
||||||
|
"social_auth_apple_enabled",
|
||||||
|
"social_auth_dropbox_enabled",
|
||||||
|
"social_auth_github_enabled",
|
||||||
|
"social_auth_keycloak_enabled",
|
||||||
|
"social_auth_generic_oauth2_enabled",
|
||||||
|
):
|
||||||
|
setattr(mock_settings, attr, False)
|
||||||
|
|
||||||
|
with patch.object(auth_module, "_register_oauth_client"):
|
||||||
|
auth_module._setup_social_providers()
|
||||||
|
|
||||||
|
assert "google" in auth_module.SOCIAL_PROVIDERS
|
||||||
|
assert auth_module.OAUTH_CONFIGURED is False
|
||||||
|
|
||||||
|
def test_refresh_clears_previous_providers(self):
|
||||||
|
"""Providers removed from settings are cleared after refresh."""
|
||||||
|
import app.auth as auth_module
|
||||||
|
|
||||||
|
# Pre-populate with a stale entry
|
||||||
|
auth_module.SOCIAL_PROVIDERS["stale_provider"] = {"name": "Stale", "icon": "", "color": ""}
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch.object(auth_module, "AUTH_ENABLED", True),
|
||||||
|
patch.object(auth_module, "settings") as mock_settings,
|
||||||
|
):
|
||||||
|
mock_settings.authentik_client_id = None
|
||||||
|
mock_settings.authentik_client_secret = None
|
||||||
|
for attr in (
|
||||||
|
"social_auth_google_enabled",
|
||||||
|
"social_auth_microsoft_enabled",
|
||||||
|
"social_auth_apple_enabled",
|
||||||
|
"social_auth_dropbox_enabled",
|
||||||
|
"social_auth_github_enabled",
|
||||||
|
"social_auth_keycloak_enabled",
|
||||||
|
"social_auth_generic_oauth2_enabled",
|
||||||
|
):
|
||||||
|
setattr(mock_settings, attr, False)
|
||||||
|
|
||||||
|
with patch.object(auth_module, "_register_oauth_client"):
|
||||||
|
auth_module._setup_social_providers()
|
||||||
|
|
||||||
|
assert "stale_provider" not in auth_module.SOCIAL_PROVIDERS
|
||||||
|
|
||||||
|
def test_register_oauth_client_clears_cache(self):
|
||||||
|
"""_register_oauth_client removes the cached client before re-registering."""
|
||||||
|
import app.auth as auth_module
|
||||||
|
|
||||||
|
# Inject a fake cached client
|
||||||
|
auth_module.oauth._clients["test_provider"] = object()
|
||||||
|
|
||||||
|
with patch.object(auth_module.oauth, "register"):
|
||||||
|
auth_module._register_oauth_client("test_provider", client_id="x", client_secret="y")
|
||||||
|
assert "test_provider" not in auth_module.oauth._clients
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
class TestTranslationKeys:
|
class TestTranslationKeys:
|
||||||
|
|||||||
Reference in New Issue
Block a user