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: 0 additions & 4 deletions backend/app/core/metrics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,11 @@
from app.core.metrics.connections import ConnectionMetrics
from app.core.metrics.database import IdempotencyMetrics
from app.core.metrics.events import EventMetrics
from app.core.metrics.rate_limit import RateLimitMetrics
from app.core.metrics.security import SecurityMetrics

__all__ = [
"BaseMetrics",
"ConnectionMetrics",
"IdempotencyMetrics",
"EventMetrics",
"RateLimitMetrics",
"SecurityMetrics",
"setup_metrics",
]
44 changes: 0 additions & 44 deletions backend/app/core/metrics/rate_limit.py

This file was deleted.

113 changes: 0 additions & 113 deletions backend/app/core/metrics/security.py

This file was deleted.

26 changes: 8 additions & 18 deletions backend/app/core/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@
ConnectionMetrics,
EventMetrics,
IdempotencyMetrics,
RateLimitMetrics,
SecurityMetrics,
)
from app.core.security import SecurityService
from app.core.tracing import Tracer
Expand Down Expand Up @@ -143,18 +141,18 @@ def get_rate_limit_service(
self,
redis_client: redis.Redis,
settings: Settings,
rate_limit_metrics: RateLimitMetrics,
tracer: trace.Tracer,
logger: structlog.stdlib.BoundLogger,
) -> RateLimitService:
return RateLimitService(redis_client, settings, rate_limit_metrics, logger)
return RateLimitService(redis_client, settings, tracer, logger)


class CoreServicesProvider(Provider):
scope = Scope.APP

@provide
def get_security_service(self, settings: Settings, security_metrics: SecurityMetrics) -> SecurityService:
return SecurityService(settings, security_metrics)
def get_security_service(self, settings: Settings, tracer: trace.Tracer) -> SecurityService:
return SecurityService(settings, tracer)

@provide
def get_tracer(
Expand Down Expand Up @@ -257,18 +255,10 @@ def get_event_metrics(self, settings: Settings) -> EventMetrics:
def get_connection_metrics(self, settings: Settings) -> ConnectionMetrics:
return ConnectionMetrics(settings)

@provide
def get_rate_limit_metrics(self, settings: Settings) -> RateLimitMetrics:
return RateLimitMetrics(settings)

@provide
def get_idempotency_metrics(self, settings: Settings) -> IdempotencyMetrics:
return IdempotencyMetrics(settings)

@provide
def get_security_metrics(self, settings: Settings) -> SecurityMetrics:
return SecurityMetrics(settings)


class RepositoryProvider(Provider):
"""Provides all repository instances.
Expand Down Expand Up @@ -360,7 +350,7 @@ def get_auth_service(
self,
user_repository: UserRepository,
security_service: SecurityService,
security_metrics: SecurityMetrics,
tracer: trace.Tracer,
logger: structlog.stdlib.BoundLogger,
lockout_service: LoginLockoutService,
runtime_settings: RuntimeSettingsLoader,
Expand All @@ -370,7 +360,7 @@ def get_auth_service(
return AuthService(
user_repo=user_repository,
security_service=security_service,
security_metrics=security_metrics,
tracer=tracer,
logger=logger,
lockout_service=lockout_service,
runtime_settings=runtime_settings,
Expand Down Expand Up @@ -560,7 +550,7 @@ def get_admin_user_service(
execution_service: ExecutionService,
rate_limit_service: RateLimitService,
security_service: SecurityService,
security_metrics: SecurityMetrics,
tracer: trace.Tracer,
logger: structlog.stdlib.BoundLogger,
) -> AdminUserService:
return AdminUserService(
Expand All @@ -569,7 +559,7 @@ def get_admin_user_service(
execution_service=execution_service,
rate_limit_service=rate_limit_service,
security_service=security_service,
security_metrics=security_metrics,
tracer=tracer,
logger=logger,
)

Expand Down
20 changes: 10 additions & 10 deletions backend/app/core/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
import jwt
from fastapi import Request
from fastapi.security import OAuth2PasswordBearer
from opentelemetry import trace
from pwdlib import PasswordHash
from pwdlib.hashers.bcrypt import BcryptHasher
from pydantic import TypeAdapter, ValidationError

from app.core.metrics import SecurityMetrics
from app.domain.user import CSRFValidationError, InvalidCredentialsError
from app.settings import Settings

Expand All @@ -30,9 +30,9 @@


class SecurityService:
def __init__(self, settings: Settings, security_metrics: SecurityMetrics) -> None:
def __init__(self, settings: Settings, tracer: trace.Tracer) -> None:
self.settings = settings
self._security_metrics = security_metrics
self._tracer = tracer
# --8<-- [start:password_hashing]
self._password_hash = PasswordHash((BcryptHasher(rounds=self.settings.BCRYPT_ROUNDS),))

Expand Down Expand Up @@ -69,18 +69,18 @@
options={"verify_exp": not allow_expired},
)
except jwt.ExpiredSignatureError:
self._security_metrics.record_token_validation_failure("expired")
self._tracer.start_span("auth.token_rejected", attributes={"auth.reason": "expired"}).end()

Check failure on line 72 in backend/app/core/security.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "auth.reason" 7 times.

See more on https://sonarcloud.io/project/issues?id=HardMax71_Integr8sCode&issues=AaFRe7-9CiUwZk3hxUiJ&open=AaFRe7-9CiUwZk3hxUiJ&pullRequest=404

Check failure on line 72 in backend/app/core/security.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "auth.token_rejected" 4 times.

See more on https://sonarcloud.io/project/issues?id=HardMax71_Integr8sCode&issues=AaFRe7-9CiUwZk3hxUiK&open=AaFRe7-9CiUwZk3hxUiK&pullRequest=404
raise InvalidCredentialsError() from None
except jwt.InvalidSignatureError:
self._security_metrics.record_token_validation_failure("invalid_signature")
self._tracer.start_span("auth.token_rejected", attributes={"auth.reason": "invalid_signature"}).end()
raise InvalidCredentialsError() from None
except jwt.PyJWTError:
self._security_metrics.record_token_validation_failure("invalid")
self._tracer.start_span("auth.token_rejected", attributes={"auth.reason": "invalid"}).end()
raise InvalidCredentialsError() from None
try:
claims = _subject_adapter.validate_python(payload)
except ValidationError:
self._security_metrics.record_token_validation_failure("missing_subject")
self._tracer.start_span("auth.token_rejected", attributes={"auth.reason": "missing_subject"}).end()
raise InvalidCredentialsError() from None
return claims["sub"]

Expand Down Expand Up @@ -175,15 +175,15 @@
cookie_token = request.cookies.get("csrf_token", "")

if not header_token:
self._security_metrics.record_csrf_validation_failure("missing_header")
self._tracer.start_span("auth.csrf_rejected", attributes={"auth.reason": "missing_header"}).end()

Check failure on line 178 in backend/app/core/security.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "auth.csrf_rejected" 3 times.

See more on https://sonarcloud.io/project/issues?id=HardMax71_Integr8sCode&issues=AaFRe7-9CiUwZk3hxUiI&open=AaFRe7-9CiUwZk3hxUiI&pullRequest=404
raise CSRFValidationError("CSRF token missing from X-CSRF-Token header")

if not self.validate_csrf_token(header_token, cookie_token):
self._security_metrics.record_csrf_validation_failure("token_mismatch")
self._tracer.start_span("auth.csrf_rejected", attributes={"auth.reason": "token_mismatch"}).end()
raise CSRFValidationError("CSRF token invalid or does not match cookie")

if not self._verify_csrf_signature(header_token, access_token):
self._security_metrics.record_csrf_validation_failure("invalid_signature")
self._tracer.start_span("auth.csrf_rejected", attributes={"auth.reason": "invalid_signature"}).end()
raise CSRFValidationError("CSRF token signature invalid")

return header_token
Expand Down
8 changes: 4 additions & 4 deletions backend/app/services/admin/admin_user_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
from datetime import datetime, timedelta, timezone

import structlog
from opentelemetry import trace

from app.core.metrics import SecurityMetrics
from app.core.security import SecurityService
from app.db import UserRepository
from app.domain.admin import AdminUserOverviewDomain, DerivedCountsDomain
Expand Down Expand Up @@ -39,15 +39,15 @@ def __init__(
execution_service: ExecutionService,
rate_limit_service: RateLimitService,
security_service: SecurityService,
security_metrics: SecurityMetrics,
tracer: trace.Tracer,
logger: structlog.stdlib.BoundLogger,
) -> None:
self._users = user_repository
self._events = event_service
self._executions = execution_service
self._rate_limits = rate_limit_service
self._security = security_service
self._security_metrics = security_metrics
self._tracer = tracer
self.logger = logger

async def get_user_overview(self, user_id: str, hours: int = 24) -> AdminUserOverviewDomain:
Expand Down Expand Up @@ -266,7 +266,7 @@ async def delete_user(self, *, admin_user_id: str, user_id: str, cascade: bool)

async def reset_user_password(self, *, admin_user_id: str, user_id: str, new_password: str) -> None:
self.logger.info("Admin resetting user password", admin_user_id=admin_user_id, target_user_id=user_id)
self._security_metrics.record_password_reset_request(method="admin")
self._tracer.start_span("auth.password_reset", attributes={"auth.method": "admin"}).end()
hashed = self._security.get_password_hash(new_password)
pr = PasswordReset(user_id=user_id, new_password=hashed)
if not await self._users.reset_user_password(pr):
Expand Down
Loading
Loading