Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/fastapi_startkit_auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
ApiTokenConfig,
AuthConfig,
OAuth2Config,
OAuthClientConfig,
OAuthClientsConfig,
OAuthTokenConfig,
OAuthTokensConfig,
SessionConfig,
)
Expand Down Expand Up @@ -68,7 +70,9 @@
"AuthConfig",
"SessionConfig",
"OAuth2Config",
"OAuthClientConfig",
"OAuthClientsConfig",
"OAuthTokenConfig",
"OAuthTokensConfig",
"ApiTokenConfig",
"AuthProvider",
Expand Down
4 changes: 4 additions & 0 deletions src/fastapi_startkit_auth/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
AuthConfig,
OAuth2Config,
OAuth2Guard,
OAuthClientConfig,
OAuthClientsConfig,
OAuthTokenConfig,
OAuthTokensConfig,
SessionConfig,
SessionGuard,
Expand All @@ -19,7 +21,9 @@
"AuthConfig",
"SessionConfig",
"OAuth2Config",
"OAuthClientConfig",
"OAuthClientsConfig",
"OAuthTokenConfig",
"OAuthTokensConfig",
"ApiTokenConfig",
"SessionGuard",
Expand Down
55 changes: 36 additions & 19 deletions src/fastapi_startkit_auth/config/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@
ConfigT = TypeVar("ConfigT")


def _canonical_store(store: str, section: str) -> str:
if store == "orm":
def _canonical_driver(driver: str, section: str) -> str:
if driver == "orm":
# stacklevel 4: this helper, __post_init__, the dataclass __init__, then the caller.
warnings.warn(
f'{section}(store="orm") is deprecated; use store="database".', DeprecationWarning, stacklevel=4
f'{section}(driver="orm") is deprecated; use driver="database".', DeprecationWarning, stacklevel=4
)
return "database"
return store
return driver


@dataclass
Expand All @@ -41,11 +41,11 @@ class ApiTokenGuard:
class SessionConfig:
"""Cookie sessions, enabled by ``AuthSessionProvider``.

``store`` is "memory", "database" (the ORM ``sessions`` table; "orm" is a
``driver`` is "memory", "database" (the ORM ``sessions`` table; "orm" is a
deprecated alias) or "instance" (a ready-made store under ``instance``).
"""

store: str = "memory"
driver: str = "memory"
connection: str | None = None
instance: Any = None
cookie: str = "startkit_session"
Expand All @@ -63,7 +63,7 @@ class SessionConfig:
csrf_exempt_paths: list[str] = field(default_factory=list)

def __post_init__(self) -> None:
self.store = _canonical_store(self.store, "SessionConfig")
self.driver = _canonical_driver(self.driver, "SessionConfig")


@dataclass
Expand All @@ -74,7 +74,7 @@ class ApiTokenConfig:
accept the ``session_guard`` cookie session from those origins.
"""

store: str = "memory"
driver: str = "memory"
connection: str | None = None
instance: Any = None
header: str = "Authorization"
Expand All @@ -84,33 +84,50 @@ class ApiTokenConfig:
session_guard: str = "web"

def __post_init__(self) -> None:
self.store = _canonical_store(self.store, "ApiTokenConfig")
self.driver = _canonical_driver(self.driver, "ApiTokenConfig")

@property
def stateful(self) -> bool:
return bool(self.stateful_origins)


@dataclass
class OAuthClientsConfig:
class OAuthClientConfig:
"""Where OAuth clients live: "database" (the ORM ``oauth_clients`` table), "memory" or "instance"."""

store: str = "database"
driver: str = "database"
connection: str | None = None
instance: Any = None

def __post_init__(self) -> None:
self.store = _canonical_store(self.store, "OAuthClientsConfig")
self.driver = _canonical_driver(self.driver, "OAuthClientConfig")


@dataclass
class OAuthTokensConfig:
store: str = "memory"
class OAuthTokenConfig:
driver: str = "memory"
connection: str | None = None
instance: Any = None

def __post_init__(self) -> None:
self.store = _canonical_store(self.store, "OAuthTokensConfig")
self.driver = _canonical_driver(self.driver, "OAuthTokenConfig")


def _deprecated_alias(config_type: type[ConfigT], old_name: str) -> type[ConfigT]:
class Alias(config_type):
def __init__(self, *args: Any, **kwargs: Any) -> None:
warnings.warn(
f"{old_name} is deprecated; use {config_type.__name__}.", DeprecationWarning, stacklevel=2
)
super().__init__(*args, **kwargs)

Alias.__name__ = old_name
Alias.__qualname__ = old_name
return Alias


OAuthClientsConfig = _deprecated_alias(OAuthClientConfig, "OAuthClientsConfig")
OAuthTokensConfig = _deprecated_alias(OAuthTokenConfig, "OAuthTokensConfig")


DEFAULT_GRANT_TYPES = ("authorization_code", "client_credentials", "refresh_token")
Expand Down Expand Up @@ -145,12 +162,12 @@ class OAuth2Config:
require_redirect_uri: bool = True
grant_types: list[str] = field(default_factory=lambda: list(DEFAULT_GRANT_TYPES))
authorization_guard: str | None = None
tokens: OAuthTokensConfig = field(default_factory=OAuthTokensConfig)
clients: OAuthClientsConfig = field(default_factory=OAuthClientsConfig)
tokens: OAuthTokenConfig = field(default_factory=OAuthTokenConfig)
clients: OAuthClientConfig = field(default_factory=OAuthClientConfig)

def __post_init__(self) -> None:
self.tokens = resolve_config(self.tokens, OAuthTokensConfig)
self.clients = resolve_config(self.clients, OAuthClientsConfig)
self.tokens = resolve_config(self.tokens, OAuthTokenConfig)
self.clients = resolve_config(self.clients, OAuthClientConfig)


# v0.6 AuthConfig attributes that moved to OAuth2Config; still read, with a warning.
Expand Down
42 changes: 21 additions & 21 deletions src/fastapi_startkit_auth/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@
_REMOVED_STORES = ("sql", "async_sql")


def _reject_removed_store(kind: str, section: str) -> None:
def _reject_removed_driver(kind: str, section: str) -> None:
if kind in _REMOVED_STORES:
raise ValueError(
f'{section} store "{kind}" was removed: use store "database" (optional ORM '
f'{section} store "{kind}" was removed: use driver "database" (optional ORM '
'"connection" name) after running the published migrations.'
)

Expand Down Expand Up @@ -330,16 +330,16 @@ def session_store(self) -> SessionStore:

def _build_session_store(self) -> SessionStore:
config = self.session_config
if config.store == "memory":
if config.driver == "memory":
return InMemorySessionStore(idle_ttl=config.idle_ttl)
if config.store == "database":
if config.driver == "database":
from .sessions.orm import OrmSessionStore

return OrmSessionStore(config.connection, idle_ttl=config.idle_ttl, purge_interval=config.purge_interval)
_reject_removed_store(config.store, "SessionConfig")
if config.store == "instance":
_reject_removed_driver(config.driver, "SessionConfig")
if config.driver == "instance":
return config.instance
raise ValueError(f"Unknown session store: {config.store!r}")
raise ValueError(f"Unknown session store: {config.driver!r}")

@property
def api_tokens(self) -> ApiTokenManager | AsyncApiTokenManager:
Expand All @@ -353,41 +353,41 @@ def api_tokens(self) -> ApiTokenManager | AsyncApiTokenManager:
return self._api_tokens

def _build_api_token_repository(self, config: ApiTokenConfig) -> ApiTokenRepository:
if config.store == "memory":
if config.driver == "memory":
return InMemoryApiTokenRepository()
if config.store == "database":
if config.driver == "database":
from .apitokens.orm import OrmApiTokenRepository

return OrmApiTokenRepository(config.connection)
_reject_removed_store(config.store, "ApiTokenConfig")
if config.store == "instance":
_reject_removed_driver(config.driver, "ApiTokenConfig")
if config.driver == "instance":
return config.instance
raise ValueError(f"Unknown API token store: {config.store!r}")
raise ValueError(f"Unknown API token store: {config.driver!r}")

def _build_token_repository(self, config: OAuth2Config) -> Any:
tokens = config.tokens
if tokens.store == "memory":
if tokens.driver == "memory":
return InMemoryTokenRepository()
if tokens.store == "database":
if tokens.driver == "database":
from .tokens.orm import OrmTokenRepository

return OrmTokenRepository(tokens.connection)
_reject_removed_store(tokens.store, "OAuth2Config.tokens")
if tokens.store == "instance":
_reject_removed_driver(tokens.driver, "OAuth2Config.tokens")
if tokens.driver == "instance":
return tokens.instance
raise ValueError(f"Unknown OAuth token store: {tokens.store!r}")
raise ValueError(f"Unknown OAuth token store: {tokens.driver!r}")

def _build_client_repository(self, config: OAuth2Config) -> Any:
clients = config.clients
if clients.store == "memory":
if clients.driver == "memory":
return InMemoryClientRepository(hasher=self.hasher)
if clients.store == "database":
if clients.driver == "database":
from .clients.orm import OrmClientRepository

return OrmClientRepository(clients.connection, hasher=self.hasher)
if clients.store == "instance":
if clients.driver == "instance":
return clients.instance
raise ValueError(f"Unknown OAuth client store: {clients.store!r}")
raise ValueError(f"Unknown OAuth client store: {clients.driver!r}")

# --- providers and brokers -----------------------------------------
def _build_provider(self, spec: dict[str, Any]) -> UserProvider:
Expand Down
4 changes: 2 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
AuthProvider,
AuthSessionProvider,
OAuth2Config,
OAuthClientsConfig,
OAuthClientConfig,
current_user,
require_scopes,
)
Expand Down Expand Up @@ -113,7 +113,7 @@ def delete(self, client_id):

def oauth2_config(**overrides):
overrides.setdefault("key", TEST_OAUTH_KEY)
overrides.setdefault("clients", OAuthClientsConfig(store="instance", instance=SyncClientRepository()))
overrides.setdefault("clients", OAuthClientConfig(driver="instance", instance=SyncClientRepository()))
return OAuth2Config(**overrides)


Expand Down
4 changes: 2 additions & 2 deletions tests/test_api_tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,12 +431,12 @@ def test_token_guard_coexists_with_passport_guard():

def test_unknown_api_token_store_raises():
with pytest.raises(ValueError, match="Unknown API token store"):
auth_manager(token_config(), api_tokens={"store": "redis"}).api_tokens
auth_manager(token_config(), api_tokens={"driver": "redis"}).api_tokens


def test_instance_store_is_used_verbatim():
repository = InMemoryApiTokenRepository()
manager = auth_manager(token_config(), api_tokens={"store": "instance", "instance": repository})
manager = auth_manager(token_config(), api_tokens={"driver": "instance", "instance": repository})
assert manager.api_tokens.repository is repository


Expand Down
8 changes: 4 additions & 4 deletions tests/test_async_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ def sync_facade(auth: Auth = Depends(Auth.scoped)):

async def build(connection, **kwargs):
api = FastAPI()
store = {"store": "database", "connection": connection}
store = {"driver": "database", "connection": connection}
manager = register_auth(
api,
make_config(connection, **kwargs),
Expand Down Expand Up @@ -188,9 +188,9 @@ class Config(AuthConfig):
providers = {"users": {"driver": "memory", "users": []}}

features = {
"session": {"session": {"store": removed}},
"api_tokens": {"api_tokens": {"store": removed}},
"tokens": {"oauth2": oauth2_config(tokens={"store": removed})},
"session": {"session": {"driver": removed}},
"api_tokens": {"api_tokens": {"driver": removed}},
"tokens": {"oauth2": oauth2_config(tokens={"driver": removed})},
}[section]
attribute = {"session": "session_store", "api_tokens": "api_tokens", "tokens": "token_repository"}[section]
with pytest.raises(ValueError, match='"database"'):
Expand Down
6 changes: 3 additions & 3 deletions tests/test_async_safety.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ class Config(AuthConfig):


def install(api, connection=None, *, session=None, clients=None, **kwargs):
stores = {"store": "database", "connection": connection} if connection is not None else {"store": "memory"}
stores = {"driver": "database", "connection": connection} if connection is not None else {"driver": "memory"}
return register_auth(
api,
make_config(**kwargs),
Expand Down Expand Up @@ -307,7 +307,7 @@ def test_sync_refresh_grant_denied_when_owner_is_gone(how):
users = [{"id": 1, "email": "ada@example.com", "password": HASHER.make("secret"), "active": True}]
provider = {"driver": "memory", "users": users, "is_active": "active"}
api = FastAPI()
sync_clients = {"store": "instance", "instance": SyncClientRepository()}
sync_clients = {"driver": "instance", "instance": SyncClientRepository()}
manager = install(api, provider=provider, clients=sync_clients)
assert isinstance(manager.refresh_grant(), RefreshTokenGrant)
client = TestClient(api)
Expand Down Expand Up @@ -385,7 +385,7 @@ async def test_sync_provider_and_session_store_never_run_on_the_loop(orm_databas
client, _ = await build(
orm_database,
provider={"driver": "instance", "instance": provider},
session={"store": "instance", "instance": sessions},
session={"driver": "instance", "instance": sessions},
)
async with client:
LOOP_CALLS.clear()
Expand Down
6 changes: 3 additions & 3 deletions tests/test_class_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
OAuth2Config,
SessionConfig,
)
from fastapi_startkit_auth.config import OAuth2Guard, OAuthClientsConfig, SessionGuard, resolve_config
from fastapi_startkit_auth.config import OAuth2Guard, OAuthClientConfig, SessionGuard, resolve_config
from fastapi_startkit_auth.guards.guard import PassportGuard
from fastapi_startkit_auth.guards.session import SessionGuard as RuntimeSessionGuard
from fastapi_startkit_auth.security.jwt import JWTEncoder
Expand Down Expand Up @@ -112,7 +112,7 @@ def test_oauth_settings_configure_signing_and_token_lifetimes():
key="nested-oauth-signing-key",
access_token_ttl=120,
authorization_code_ttl=30,
clients=OAuthClientsConfig(store="memory"),
clients=OAuthClientConfig(driver="memory"),
)
)

Expand All @@ -125,4 +125,4 @@ def test_oauth_settings_configure_signing_and_token_lifetimes():

def test_missing_oauth_key_warns_and_generates_an_ephemeral_key():
with pytest.warns(UserWarning, match="OAuth2Config.key is not set"):
AuthManager(Config).use_oauth2(OAuth2Config(clients=OAuthClientsConfig(store="memory")))
AuthManager(Config).use_oauth2(OAuth2Config(clients=OAuthClientConfig(driver="memory")))
2 changes: 1 addition & 1 deletion tests/test_multi_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ class Config(AuthConfig):


def install(api, providers, connection=None):
stores = {"store": "database", "connection": connection} if connection is not None else {"store": "memory"}
stores = {"driver": "database", "connection": connection} if connection is not None else {"driver": "memory"}
oauth2 = oauth2_config(key=KEY, grant_types=list(PASSWORD_GRANTS), tokens=stores)
return register_auth(api, make_config(providers), oauth2=oauth2)

Expand Down
6 changes: 3 additions & 3 deletions tests/test_oauth2_client_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from fastapi.testclient import TestClient

from fastapi_startkit_auth import AuthConfig, AuthManager, AuthOAuth2Provider, AuthProvider
from fastapi_startkit_auth.config import OAuth2Config, OAuthClientsConfig
from fastapi_startkit_auth.config import OAuth2Config, OAuthClientConfig

from conftest import MIGRATIONS_DIR, ORM_CONNECTION

Expand Down Expand Up @@ -41,7 +41,7 @@ async def migrate():

OAUTH2 = OAuth2Config(
key="command-test-key-with-at-least-32-bytes",
clients=OAuthClientsConfig(store="database", connection=ORM_CONNECTION),
clients=OAuthClientConfig(driver="database", connection=ORM_CONNECTION),
)


Expand Down Expand Up @@ -136,7 +136,7 @@ def test_scopes_outside_the_catalog_create_no_client(client_database):
OAuth2Config(
key="command-test-key-with-at-least-32-bytes",
scopes={"read": "Read"},
clients=OAuthClientsConfig(store="database", connection=ORM_CONNECTION),
clients=OAuthClientConfig(driver="database", connection=ORM_CONNECTION),
)
)
command = OAuth2ClientCommand()
Expand Down
Loading
Loading