diff --git a/src/repixelizer/access.py b/src/repixelizer/access.py index 29c528f..6cf4618 100644 --- a/src/repixelizer/access.py +++ b/src/repixelizer/access.py @@ -197,6 +197,8 @@ class AccessRuntimeConfig: auth_attempt_ttl_seconds: int = 600 http_timeout_seconds: float = 10.0 jwks_cache_seconds: int = 300 + discord_access_guild_id: str | None = None + discord_access_role_ids: tuple[str, ...] = () @property def enabled(self) -> bool: @@ -460,6 +462,8 @@ class AccessController: heimdall_jwks_url = None app_public_base_url = None start_endpoint = None + discord_access_guild_id = None + discord_access_role_ids: tuple[str, ...] = () login_url = _env_text("GC_ACCESS_LOGIN_URL") logout_url = _env_text("GC_ACCESS_LOGOUT_URL") verifier: HeimdallVerifier | None = None @@ -498,6 +502,8 @@ class AccessController: login_url = login_url or "/" logout_url = logout_url or _AUTH_LOGOUT_PATH start_endpoint = _AUTH_START_PATH + discord_access_guild_id = _env_text("REPIXELIZER_ACCESS_DISCORD_GUILD_ID") + discord_access_role_ids = tuple(_parse_csv(_env_text("REPIXELIZER_ACCESS_DISCORD_ALLOWED_ROLE_IDS"))) verifier = HeimdallVerifier( issuer=heimdall_issuer, app_slug=app_slug, @@ -526,6 +532,8 @@ class AccessController: auth_attempt_ttl_seconds=attempt_ttl_seconds, http_timeout_seconds=timeout_seconds, jwks_cache_seconds=jwks_cache_seconds, + discord_access_guild_id=discord_access_guild_id, + discord_access_role_ids=discord_access_role_ids, ) return cls(runtime, verifier=verifier) @@ -610,6 +618,16 @@ class AccessController: "callbackUrl": callback_url, }, } + if ( + provider_slug == "discord" + and self.runtime.discord_access_guild_id + and self.runtime.discord_access_role_ids + ): + request_payload["entitlementPolicy"] = { + "kind": "discord_role_access", + "guildId": self.runtime.discord_access_guild_id, + "allowedRoleIds": list(self.runtime.discord_access_role_ids), + } start_url = urljoin(f"{heimdall_base_url}/", _AUTH_START_ENDPOINT_TEMPLATE.format(provider=provider_slug).lstrip("/")) try: response_payload = _post_json( diff --git a/tests/test_gui.py b/tests/test_gui.py index 18fbfa1..07db98d 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -426,6 +426,8 @@ def test_gui_hosted_root_renders_heimdall_login_buttons(monkeypatch, tmp_path: P monkeypatch.setenv("GC_ACCESS_HEIMDALL_BASE_URL", "https://heimdall.gamecult.org") monkeypatch.setenv("GC_ACCESS_APP_PUBLIC_BASE_URL", "https://repixelizer.gamecult.org") monkeypatch.setenv("GC_ACCESS_ALLOWED_PROVIDERS", "discord,patreon") + monkeypatch.setenv("REPIXELIZER_ACCESS_DISCORD_GUILD_ID", "gamecult-guild") + monkeypatch.setenv("REPIXELIZER_ACCESS_DISCORD_ALLOWED_ROLE_IDS", "role-repixelizer,role-patreon") app = create_app() status, _headers, body = asyncio.run(_get_response(app, "/")) @@ -752,6 +754,8 @@ def test_gui_heimdall_callback_flow_adopts_local_cookie_session(monkeypatch, tmp monkeypatch.setenv("GC_ACCESS_HEIMDALL_BASE_URL", "https://heimdall.gamecult.org") monkeypatch.setenv("GC_ACCESS_APP_PUBLIC_BASE_URL", "https://repixelizer.gamecult.org") monkeypatch.setenv("GC_ACCESS_ALLOWED_PROVIDERS", "discord,patreon") + monkeypatch.setenv("REPIXELIZER_ACCESS_DISCORD_GUILD_ID", "gamecult-guild") + monkeypatch.setenv("REPIXELIZER_ACCESS_DISCORD_ALLOWED_ROLE_IDS", "role-repixelizer,role-patreon") monkeypatch.setattr("repixelizer.access._post_json", fake_post_json) monkeypatch.setattr("repixelizer.access._fetch_json", fake_fetch_json) @@ -774,6 +778,11 @@ def test_gui_heimdall_callback_flow_adopts_local_cookie_session(monkeypatch, tmp assert start_calls assert start_calls[0][0] == "https://heimdall.gamecult.org/v1/oauth/discord/start" assert start_calls[0][1]["handoff"]["callbackUrl"] == "https://repixelizer.gamecult.org/api/auth/heimdall/callback" + assert start_calls[0][1]["entitlementPolicy"] == { + "kind": "discord_role_access", + "guildId": "gamecult-guild", + "allowedRoleIds": ["role-repixelizer", "role-patreon"], + } pending_status, _pending_headers, pending_body = asyncio.run(_get_response(app, f"/api/auth/attempts/{attempt_id}")) pending_payload = _response_json(type("Response", (), {"body": pending_body})())