From be5c646b5ba3762f1627067f8f5e5d0af2ab453e Mon Sep 17 00:00:00 2001 From: beubax Date: Fri, 12 Jun 2026 12:42:06 +0530 Subject: [PATCH] fix: require production backend URLs --- docker-compose.yml | 1 + docs/guides/self-hosting.md | 3 +- src/authsome/config.py | 2 +- src/authsome/server/config.py | 7 ++++ src/authsome/server/store/database.py | 9 +++-- tests/server/test_config.py | 49 +++++++++++++++++++++++++++ 6 files changed, 66 insertions(+), 5 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 540f784..8b0a46c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -49,6 +49,7 @@ services: AUTHSOME_HOST: 0.0.0.0 AUTHSOME_PORT: "7998" AUTHSOME_HOME: /data/authsome + AUTHSOME_ENV: prod AUTHSOME_BASE_URL: ${AUTHSOME_BASE_URL:-http://localhost:7998} AUTHSOME_DATABASE_URL: postgresql://${AUTHSOME_POSTGRES_USER:-authsome}:${AUTHSOME_POSTGRES_PASSWORD:?set AUTHSOME_POSTGRES_PASSWORD}@postgres:5432/${AUTHSOME_POSTGRES_DB:-authsome} AUTHSOME_REDIS_URL: redis://redis:6379/0 diff --git a/docs/guides/self-hosting.md b/docs/guides/self-hosting.md index 0c8f865..6b10e3f 100644 --- a/docs/guides/self-hosting.md +++ b/docs/guides/self-hosting.md @@ -37,6 +37,7 @@ Do not commit production secrets. Use your platform secret store or Docker secre | Variable | Default | Description | |---|---|---| +| `AUTHSOME_ENV` | `dev` | Runtime mode. Set to `prod` for production deployments; in `prod`, `AUTHSOME_DATABASE_URL` and `AUTHSOME_REDIS_URL` are required. | | `AUTHSOME_DATABASE_URL` | none | Postgres DSN for the daemon-owned registries. The compose file points this at the bundled Postgres service. | | `AUTHSOME_REDIS_URL` | none | Redis URL for shared runtime state and the encrypted vault raw KV backend. | | `AUTHSOME_POSTGRES_PASSWORD` | none | Required password used by the bundled Postgres service and the daemon's database URL. | @@ -54,7 +55,7 @@ Do not commit production secrets. Use your platform secret store or Docker secre | `AUTHSOME_POSTHOG_API_KEY` | none | Enables PostHog analytics when present and telemetry is not opted out. | | `AUTHSOME_POSTHOG_HOST` | `https://us.i.posthog.com` | Override the PostHog ingestion host if needed. | -The daemon still accepts the legacy `DATABASE_URL` alias, but production deployments should set `AUTHSOME_DATABASE_URL`. +The daemon still accepts the legacy `DATABASE_URL` alias, but production deployments should set `AUTHSOME_DATABASE_URL`. The included compose file sets `AUTHSOME_ENV=prod`, which makes the Postgres and Redis URLs mandatory at startup. The included compose file hard-requires `AUTHSOME_MASTER_KEY` and `AUTHSOME_UI_SESSION_KEY` from the host environment; it does not mount secret files or pass `_FILE` paths for you. ## Secret resolution diff --git a/src/authsome/config.py b/src/authsome/config.py index e95930c..b35f454 100644 --- a/src/authsome/config.py +++ b/src/authsome/config.py @@ -15,7 +15,7 @@ class AuthsomeConfig(BaseSettings): model_config = SettingsConfigDict(env_prefix="AUTHSOME_") version: str = __version__ - env: Literal["prod", "dev", "test"] = "prod" + env: Literal["prod", "dev", "test"] = "dev" home: Path = Field(default=Path.home() / ".authsome") base_url: str = Field(default="http://127.0.0.1:7998") diff --git a/src/authsome/server/config.py b/src/authsome/server/config.py index f0362b0..be2703c 100644 --- a/src/authsome/server/config.py +++ b/src/authsome/server/config.py @@ -28,6 +28,13 @@ class ServerConfig(AuthsomeConfig): def validate_postgres_pool_sizes(self) -> "ServerConfig": if self.postgres_pool_min_size > self.postgres_pool_max_size: raise ValueError("postgres_pool_min_size must be less than or equal to postgres_pool_max_size") + if self.env == "prod": + if not self.database_url: + raise ValueError("AUTHSOME_DATABASE_URL is required when AUTHSOME_ENV=prod") + if not self.database_url.startswith(("postgresql://", "postgres://")): + raise ValueError("AUTHSOME_DATABASE_URL must be a Postgres URL when AUTHSOME_ENV=prod") + if not self.redis_url: + raise ValueError("AUTHSOME_REDIS_URL is required when AUTHSOME_ENV=prod") return self # Lifetimes, in seconds diff --git a/src/authsome/server/store/database.py b/src/authsome/server/store/database.py index 64d8978..891766f 100644 --- a/src/authsome/server/store/database.py +++ b/src/authsome/server/store/database.py @@ -117,13 +117,16 @@ class StoreDatabase: async def execute_rowcount(self, sql: str, params: Sequence[Any] = ()) -> int: if self.backend == "sqlite": - cursor = await self._connection.execute(sql, params) - await self._connection.commit() + connection = self._connection + assert connection is not None + cursor = await connection.execute(sql, params) + await connection.commit() rowcount = cursor.rowcount await cursor.close() return rowcount - status = await self._connection.execute(self._sql(sql), *params) + async with self._postgres_connection() as connection: + status = await connection.execute(self._sql(sql), *params) _, _, count = status.partition(" ") return int(count) if count else 0 diff --git a/tests/server/test_config.py b/tests/server/test_config.py index dd28ff4..e6a9111 100644 --- a/tests/server/test_config.py +++ b/tests/server/test_config.py @@ -13,6 +13,14 @@ def test_server_config_reads_redis_url(monkeypatch) -> None: assert config.redis_url == "redis://localhost:6379/0" +def test_server_config_defaults_to_dev_env(monkeypatch) -> None: + monkeypatch.delenv("AUTHSOME_ENV", raising=False) + + config = ServerConfig() + + assert config.env == "dev" + + def test_server_config_reads_authsome_database_url(monkeypatch) -> None: monkeypatch.setenv("AUTHSOME_DATABASE_URL", "postgresql://authsome:secret@localhost/authsome") monkeypatch.delenv("DATABASE_URL", raising=False) @@ -52,6 +60,47 @@ def test_server_config_defaults_preserve_local_paths(tmp_path: Path) -> None: assert config.kv_store_dir == tmp_path / "server" / "kv_store" +def test_server_config_requires_database_url_in_prod(monkeypatch) -> None: + monkeypatch.setenv("AUTHSOME_ENV", "prod") + monkeypatch.delenv("AUTHSOME_DATABASE_URL", raising=False) + monkeypatch.delenv("DATABASE_URL", raising=False) + monkeypatch.setenv("AUTHSOME_REDIS_URL", "redis://localhost:6379/0") + + with pytest.raises(ValueError, match="AUTHSOME_DATABASE_URL is required when AUTHSOME_ENV=prod"): + ServerConfig() + + +def test_server_config_requires_redis_url_in_prod(monkeypatch) -> None: + monkeypatch.setenv("AUTHSOME_ENV", "prod") + monkeypatch.setenv("AUTHSOME_DATABASE_URL", "postgresql://authsome:secret@localhost/authsome") + monkeypatch.delenv("DATABASE_URL", raising=False) + monkeypatch.delenv("AUTHSOME_REDIS_URL", raising=False) + + with pytest.raises(ValueError, match="AUTHSOME_REDIS_URL is required when AUTHSOME_ENV=prod"): + ServerConfig() + + +def test_server_config_requires_postgres_database_url_in_prod(monkeypatch) -> None: + monkeypatch.setenv("AUTHSOME_ENV", "prod") + monkeypatch.setenv("AUTHSOME_DATABASE_URL", "sqlite:////tmp/authsome.db") + monkeypatch.setenv("AUTHSOME_REDIS_URL", "redis://localhost:6379/0") + + with pytest.raises(ValueError, match="AUTHSOME_DATABASE_URL must be a Postgres URL when AUTHSOME_ENV=prod"): + ServerConfig() + + +def test_server_config_accepts_prod_with_database_and_redis_urls(monkeypatch) -> None: + monkeypatch.setenv("AUTHSOME_ENV", "prod") + monkeypatch.setenv("AUTHSOME_DATABASE_URL", "postgresql://authsome:secret@localhost/authsome") + monkeypatch.setenv("AUTHSOME_REDIS_URL", "redis://localhost:6379/0") + + config = ServerConfig() + + assert config.env == "prod" + assert config.database_url == "postgresql://authsome:secret@localhost/authsome" + assert config.redis_url == "redis://localhost:6379/0" + + def test_server_config_rejects_invalid_postgres_pool_range() -> None: with pytest.raises(ValueError, match="postgres_pool_min_size must be less than or equal to postgres_pool_max_size"): ServerConfig(postgres_pool_min_size=10, postgres_pool_max_size=2)