feat(cloud-console): add user authentication and administration
This commit is contained in:
@@ -5,6 +5,7 @@ import logging
|
||||
from collections.abc import Callable
|
||||
from contextlib import asynccontextmanager
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -19,6 +20,7 @@ from cloud.auth import (
|
||||
ChainedAuthProvider,
|
||||
ConfiguredEnrollmentTokenProvider,
|
||||
RepositoryHostAuthProvider,
|
||||
UserSessionAuthProvider,
|
||||
create_auth_provider,
|
||||
)
|
||||
from cloud.config import CloudConfig
|
||||
@@ -42,6 +44,8 @@ from cloud.pool import DevicePool
|
||||
from cloud.scheduler import TaskScheduler
|
||||
from cloud.schema import require_current_schema
|
||||
from cloud.sdk.api import create_cloud_router
|
||||
from cloud.sdk.user_api import create_user_auth_router
|
||||
from cloud.user_auth import USER_CSRF_COOKIE, USER_SESSION_COOKIE, UserAuthService, UserAuthSettings
|
||||
from core.models import utc_now
|
||||
|
||||
|
||||
@@ -72,6 +76,7 @@ class CloudApplicationServices:
|
||||
scheduler: TaskScheduler
|
||||
plugin_registry: PluginRegistry
|
||||
auth_provider: Any
|
||||
user_auth_service: UserAuthService
|
||||
|
||||
|
||||
class SpaStaticFiles(StaticFiles):
|
||||
@@ -107,9 +112,25 @@ def create_app(
|
||||
allow_insecure_anonymous=control_config.allow_insecure_anonymous,
|
||||
)
|
||||
repository = _RepositoryProxy()
|
||||
user_auth_service = UserAuthService(
|
||||
repository,
|
||||
settings=UserAuthSettings(
|
||||
session_idle_ttl=timedelta(seconds=control_config.user_session_idle_seconds),
|
||||
session_absolute_ttl=timedelta(
|
||||
seconds=control_config.user_session_absolute_seconds
|
||||
),
|
||||
login_failure_limit=control_config.login_failure_limit,
|
||||
login_failure_window=timedelta(
|
||||
seconds=control_config.login_failure_window_seconds
|
||||
),
|
||||
login_block_duration=timedelta(seconds=control_config.login_block_seconds),
|
||||
cookie_secure=control_config.session_cookie_secure,
|
||||
),
|
||||
)
|
||||
auth_provider = ChainedAuthProvider(
|
||||
(
|
||||
configured_auth_provider,
|
||||
UserSessionAuthProvider(user_auth_service),
|
||||
RepositoryHostAuthProvider(repository), # type: ignore[arg-type]
|
||||
)
|
||||
)
|
||||
@@ -128,6 +149,7 @@ def create_app(
|
||||
scheduler=scheduler,
|
||||
plugin_registry=plugin_registry,
|
||||
auth_provider=auth_provider,
|
||||
user_auth_service=user_auth_service,
|
||||
)
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -201,6 +223,12 @@ def create_app(
|
||||
correlation_token = bind_correlation_id(correlation_id)
|
||||
try:
|
||||
response = await call_next(request)
|
||||
if (
|
||||
request.cookies.get(USER_SESSION_COOKIE)
|
||||
and not request.headers.get("authorization")
|
||||
and response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
):
|
||||
_clear_user_auth_cookies(response, control_config)
|
||||
logger.info(
|
||||
"cloud request completed",
|
||||
extra={
|
||||
@@ -251,6 +279,18 @@ def create_app(
|
||||
scheduler=scheduler,
|
||||
plugin_registry=plugin_registry,
|
||||
auth_provider=auth_provider,
|
||||
csrf_validator=lambda request, principal: _valid_csrf_request(
|
||||
request,
|
||||
principal,
|
||||
user_auth_service,
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
create_user_auth_router(
|
||||
user_auth_service=user_auth_service,
|
||||
auth_provider=auth_provider,
|
||||
config=control_config,
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
@@ -289,6 +329,37 @@ def _default_database_factory(config: CloudControlConfig) -> CloudDatabase:
|
||||
)
|
||||
|
||||
|
||||
def _valid_csrf_request(
|
||||
request: Request,
|
||||
principal: Any,
|
||||
user_auth_service: UserAuthService,
|
||||
) -> bool:
|
||||
if principal.session_id is None:
|
||||
return True
|
||||
return user_auth_service.validate_csrf(
|
||||
session_token=request.cookies.get(USER_SESSION_COOKIE),
|
||||
csrf_cookie=request.cookies.get(USER_CSRF_COOKIE),
|
||||
csrf_header=request.headers.get("x-csrf-token"),
|
||||
)
|
||||
|
||||
|
||||
def _clear_user_auth_cookies(response: Any, config: CloudControlConfig) -> None:
|
||||
response.delete_cookie(
|
||||
USER_SESSION_COOKIE,
|
||||
path="/",
|
||||
secure=config.session_cookie_secure,
|
||||
httponly=True,
|
||||
samesite="lax",
|
||||
)
|
||||
response.delete_cookie(
|
||||
USER_CSRF_COOKIE,
|
||||
path="/",
|
||||
secure=config.session_cookie_secure,
|
||||
httponly=False,
|
||||
samesite="lax",
|
||||
)
|
||||
|
||||
|
||||
async def _run_scheduler_loop(
|
||||
services: CloudApplicationServices,
|
||||
stop: asyncio.Event,
|
||||
|
||||
Reference in New Issue
Block a user