diff --git a/hbd/server/config.py b/hbd/server/config.py index 3d0c8fc..9cc9021 100644 --- a/hbd/server/config.py +++ b/hbd/server/config.py @@ -34,6 +34,9 @@ SERVER_DEFAULTS = { "users": {}, # username -> {full_name, avatar, password, admin, notification_channels} "default_owner": None, # Username that owns hosts with no explicit owner + # OAuth2 providers + "oauth": {}, # oauth.gitea.{url,client_id,client_secret} + # Host management "hosts": {}, # Unified host definitions "dyndnshosts": [], # Hosts with dynamic DNS (legacy) diff --git a/hbd/server/oauth.py b/hbd/server/oauth.py new file mode 100644 index 0000000..93d2134 --- /dev/null +++ b/hbd/server/oauth.py @@ -0,0 +1,43 @@ +"""Gitea OAuth2 support. + +Config shape (in ~/.hb.yaml): + + oauth: + gitea: + url: https://git.example.com + client_id: + client_secret: + +Register a Gitea OAuth2 application at: + Gitea → Settings → Applications → OAuth2 +Set the redirect URI to: + https:///login/oauth/gitea/callback +""" + +import logging +import secrets +import time + +import aiohttp + +logger = logging.getLogger(__name__) + +STATE_TTL = 600 # 10 minutes + +# state_token -> expiry timestamp +_states: dict[str, float] = {} + + +class OAuthError(Exception): + """Raised when the OAuth2 flow fails for any reason.""" + + +def _gitea_cfg(config: dict) -> dict: + """Return the gitea sub-dict or {} if absent/incomplete.""" + return config.get("oauth", {}).get("gitea", {}) + + +def is_enabled(config: dict) -> bool: + """Return True when all three required Gitea OAuth keys are present.""" + g = _gitea_cfg(config) + return bool(g.get("url") and g.get("client_id") and g.get("client_secret")) diff --git a/tests/test_oauth.py b/tests/test_oauth.py new file mode 100644 index 0000000..d68785d --- /dev/null +++ b/tests/test_oauth.py @@ -0,0 +1,27 @@ +import pytest +from hbd.server import oauth + + +CFG_OFF = {} +CFG_ON = { + "oauth": { + "gitea": { + "url": "https://git.example.com", + "client_id": "cid", + "client_secret": "csec", + } + } +} +CFG_PARTIAL = {"oauth": {"gitea": {"url": "https://git.example.com"}}} + + +def test_is_enabled_when_all_keys_present(): + assert oauth.is_enabled(CFG_ON) is True + + +def test_is_enabled_false_when_no_oauth_key(): + assert oauth.is_enabled(CFG_OFF) is False + + +def test_is_enabled_false_when_partial_config(): + assert oauth.is_enabled(CFG_PARTIAL) is False