diff --git a/app/api/audit/adapter.py b/app/api/audit/adapter.py index e7a39665d..9d438beb5 100644 --- a/app/api/audit/adapter.py +++ b/app/api/audit/adapter.py @@ -4,17 +4,17 @@ License: https://github.com/MultiDirectoryLab/MultiDirectory/blob/main/LICENSE """ -from api.base_adapter import BaseAdapter -from ldap_protocol.policies.audit.dataclasses import ( - AuditDestinationDTO, - AuditPolicyDTO, -) -from ldap_protocol.policies.audit.schemas import ( +from api.audit.schemas import ( AuditDestinationResponse, AuditDestinationSchemaRequest, AuditPolicyResponse, AuditPolicySchemaRequest, ) +from api.base_adapter import BaseAdapter +from ldap_protocol.policies.audit.dataclasses import ( + AuditDestinationDTO, + AuditPolicyDTO, +) from ldap_protocol.policies.audit.service import AuditService @@ -49,7 +49,7 @@ async def get_destinations(self) -> list[AuditDestinationResponse]: """Get all audit destinations.""" return [ AuditDestinationResponse( - id=destination.id, # type: ignore + id=destination.id, name=destination.name, service_type=destination.service_type.name.lower(), host=destination.host, diff --git a/app/api/audit/router.py b/app/api/audit/router.py index 6209d0740..24ccbe3c9 100644 --- a/app/api/audit/router.py +++ b/app/api/audit/router.py @@ -9,6 +9,12 @@ from fastapi_error_map.routing import ErrorAwareRouter from fastapi_error_map.rules import rule +from api.audit.schemas import ( + AuditDestinationResponse, + AuditDestinationSchemaRequest, + AuditPolicyResponse, + AuditPolicySchemaRequest, +) from api.auth.utils import verify_auth from api.error_routing import ( ERROR_MAP_TYPE, @@ -21,12 +27,6 @@ AuditAlreadyExistsError, AuditNotFoundError, ) -from ldap_protocol.policies.audit.schemas import ( - AuditDestinationResponse, - AuditDestinationSchemaRequest, - AuditPolicyResponse, - AuditPolicySchemaRequest, -) from .adapter import AuditPoliciesAdapter diff --git a/app/ldap_protocol/policies/audit/schemas.py b/app/api/audit/schemas.py similarity index 86% rename from app/ldap_protocol/policies/audit/schemas.py rename to app/api/audit/schemas.py index a23387467..40bbf52e9 100644 --- a/app/ldap_protocol/policies/audit/schemas.py +++ b/app/api/audit/schemas.py @@ -1,11 +1,9 @@ -"""Audit policies schemas module. +"""Audit schemas. Copyright (c) 2025 MultiFactor License: https://github.com/MultiDirectoryLab/MultiDirectory/blob/main/LICENSE """ -from dataclasses import dataclass - from pydantic import BaseModel, Field from enums import AuditDestinationProtocolType, AuditDestinationServiceType @@ -20,8 +18,7 @@ class AuditPolicySchemaRequest(BaseModel): severity: str -@dataclass -class AuditPolicyResponse: +class AuditPolicyResponse(BaseModel): """Audit policy schema.""" id: int @@ -44,8 +41,7 @@ class Config: # noqa: D106 use_enum_values = True -@dataclass -class AuditDestinationResponse: +class AuditDestinationResponse(BaseModel): """Audit destination schema.""" id: int diff --git a/app/api/auth/adapters/auth.py b/app/api/auth/adapters/auth.py index 50ed85ee7..bb7f766fb 100644 --- a/app/api/auth/adapters/auth.py +++ b/app/api/auth/adapters/auth.py @@ -9,14 +9,10 @@ from adaptix.conversion import get_converter from fastapi import Request +from api.auth.schemas import MFAChallengeResponse, OAuth2Form, SetupRequest from api.base_adapter import BaseAdapter from ldap_protocol.auth import AuthManager -from ldap_protocol.auth.dto import SetupDTO -from ldap_protocol.auth.schemas import ( - MFAChallengeResponse, - OAuth2Form, - SetupRequest, -) +from ldap_protocol.auth.dto import LoginRequestDTO, SetupDTO from ldap_protocol.dialogue import UserSchema _convert_request_to_dto = get_converter(SetupRequest, SetupDTO) @@ -42,10 +38,13 @@ async def login( :raises HTTPException: 403 if access is forbidden (e.g. not in admins, disabled, expired, or policy failed) :raises HTTPException: 426 if MFA is required - :return: None + :return: MFAChallengeResponse | None """ login_dto = await self._service.login( - form=form, + form=LoginRequestDTO( + username=form.username, + password=form.password, + ), url=request.url_for("callback_mfa"), ip=ip, user_agent=user_agent, @@ -54,7 +53,12 @@ async def login( self._service.set_new_session_key( login_dto.session_key, ) - return login_dto.mfa_challenge + if login_dto.mfa_challenge is not None: + return MFAChallengeResponse( + status=login_dto.mfa_challenge.status, + message=login_dto.mfa_challenge.message, + ) + return None async def reset_password( self, diff --git a/app/api/auth/adapters/mfa.py b/app/api/auth/adapters/mfa.py index 9fa3b4a02..163858ba6 100644 --- a/app/api/auth/adapters/mfa.py +++ b/app/api/auth/adapters/mfa.py @@ -9,10 +9,11 @@ from fastapi import status from fastapi.responses import RedirectResponse +from api.auth.schemas import MFACreateRequest, MFAGetResponse from api.base_adapter import BaseAdapter from ldap_protocol.auth import MFAManager +from ldap_protocol.auth.dto import MFACreateRequestDTO from ldap_protocol.auth.exceptions.mfa import MFATokenError -from ldap_protocol.auth.schemas import MFACreateRequest, MFAGetResponse from ldap_protocol.multifactor import MFA_HTTP_Creds, MFA_LDAP_Creds @@ -25,7 +26,15 @@ async def setup_mfa(self, mfa: MFACreateRequest) -> bool: :param mfa: MFACreateRequest :return: bool """ - return await self._service.setup_mfa(mfa) + return await self._service.setup_mfa( + MFACreateRequestDTO( + mfa_key=mfa.mfa_key, + mfa_secret=mfa.mfa_secret, + is_ldap_scope=mfa.is_ldap_scope, + key_name=mfa.key_name, + secret_name=mfa.secret_name, + ), + ) async def remove_mfa(self, scope: str) -> None: """Delete MFA keys by scope. @@ -46,7 +55,16 @@ async def get_mfa( :param mfa_creds_ldap: MFA_LDAP_Creds :return: MFAGetResponse """ - return await self._service.get_mfa(mfa_creds, mfa_creds_ldap) + mfa_get_response = await self._service.get_mfa( + mfa_creds, + mfa_creds_ldap, + ) + return MFAGetResponse( + mfa_key=mfa_get_response.mfa_key, + mfa_secret=mfa_get_response.mfa_secret, + mfa_key_ldap=mfa_get_response.mfa_key_ldap, + mfa_secret_ldap=mfa_get_response.mfa_secret_ldap, + ) async def callback_mfa( self, diff --git a/app/api/auth/router_auth.py b/app/api/auth/router_auth.py index a5c98911f..004f8c0c0 100644 --- a/app/api/auth/router_auth.py +++ b/app/api/auth/router_auth.py @@ -13,6 +13,7 @@ from fastapi_error_map.rules import rule from api.auth.adapters import AuthFastAPIAdapter +from api.auth.schemas import MFAChallengeResponse, OAuth2Form, SetupRequest from api.auth.utils import get_ip_from_request, get_user_agent_from_request from api.error_routing import ( ERROR_MAP_TYPE, @@ -27,11 +28,6 @@ MFARequiredError, MissingMFACredentialsError, ) -from ldap_protocol.auth.schemas import ( - MFAChallengeResponse, - OAuth2Form, - SetupRequest, -) from ldap_protocol.dialogue import UserSchema from ldap_protocol.identity.exceptions import ( AlreadyConfiguredError, diff --git a/app/api/auth/router_mfa.py b/app/api/auth/router_mfa.py index 8e275b242..a003f7a52 100644 --- a/app/api/auth/router_mfa.py +++ b/app/api/auth/router_mfa.py @@ -14,6 +14,7 @@ from fastapi_error_map.rules import rule from api.auth.adapters import MFAFastAPIAdapter +from api.auth.schemas import MFACreateRequest, MFAGetResponse from api.auth.utils import ( get_ip_from_request, get_user_agent_from_request, @@ -35,7 +36,6 @@ NetworkPolicyError, NotFoundError, ) -from ldap_protocol.auth.schemas import MFACreateRequest, MFAGetResponse from ldap_protocol.multifactor import MFA_HTTP_Creds, MFA_LDAP_Creds translator = DomainErrorTranslator(DomainCodes.MFA) diff --git a/app/ldap_protocol/auth/schemas.py b/app/api/auth/schemas.py similarity index 71% rename from app/ldap_protocol/auth/schemas.py rename to app/api/auth/schemas.py index fe786189c..3102f2b37 100644 --- a/app/ldap_protocol/auth/schemas.py +++ b/app/api/auth/schemas.py @@ -1,25 +1,14 @@ -"""Schemas for auth module. +"""Auth schemas. Copyright (c) 2025 MultiFactor License: https://github.com/MultiDirectoryLab/MultiDirectory/blob/main/LICENSE """ import re -from dataclasses import dataclass -from datetime import datetime -from ipaddress import IPv4Address, IPv6Address -from typing import Literal from fastapi.param_functions import Form from fastapi.security import OAuth2PasswordRequestForm -from pydantic import ( - BaseModel, - ConfigDict, - Field, - SecretStr, - computed_field, - field_validator, -) +from pydantic import BaseModel, SecretStr, computed_field, field_validator from ldap_protocol.utils.const import EmailStr @@ -96,23 +85,3 @@ class MFAChallengeResponse(BaseModel): status: str message: str - - -@dataclass -class LoginDTO: - """Login Data Transfer Object.""" - - session_key: str | None - mfa_challenge: MFAChallengeResponse | None - - -class SessionContentSchema(BaseModel): - """Session content schema.""" - - model_config = ConfigDict(extra="allow") - - id: int - sign: str = Field("", description="Session signature") - issued: datetime - ip: IPv4Address | IPv6Address - protocol: Literal["ldap", "http"] = "http" diff --git a/app/api/dhcp/adapter.py b/app/api/dhcp/adapter.py index d063ad144..2a680e137 100644 --- a/app/api/dhcp/adapter.py +++ b/app/api/dhcp/adapter.py @@ -7,8 +7,7 @@ from ipaddress import IPv4Address from api.base_adapter import BaseAdapter -from ldap_protocol.dhcp import ( - AbstractDHCPManager, +from api.dhcp.schemas import ( DHCPChangeStateSchemaRequest, DHCPLeaseSchemaRequest, DHCPLeaseSchemaResponse, @@ -19,6 +18,7 @@ DHCPSubnetSchemaAddRequest, DHCPSubnetSchemaResponse, ) +from ldap_protocol.dhcp import AbstractDHCPManager from ldap_protocol.dhcp.dataclasses import ( DHCPLease, DHCPOptionData, diff --git a/app/api/dhcp/router.py b/app/api/dhcp/router.py index a41e806a0..d1eb9b77b 100644 --- a/app/api/dhcp/router.py +++ b/app/api/dhcp/router.py @@ -12,6 +12,17 @@ from fastapi_error_map.rules import rule from api.auth.utils import verify_auth +from api.dhcp.schemas import ( + DHCPChangeStateSchemaRequest, + DHCPLeaseSchemaRequest, + DHCPLeaseSchemaResponse, + DHCPLeaseToReservationErrorResponse, + DHCPReservationSchemaRequest, + DHCPReservationSchemaResponse, + DHCPStateSchemaResponse, + DHCPSubnetSchemaAddRequest, + DHCPSubnetSchemaResponse, +) from api.error_routing import ( ERROR_MAP_TYPE, DishkaErrorAwareRoute, @@ -27,17 +38,6 @@ DHCPOperationError, DHCPValidationError, ) -from ldap_protocol.dhcp.schemas import ( - DHCPChangeStateSchemaRequest, - DHCPLeaseSchemaRequest, - DHCPLeaseSchemaResponse, - DHCPLeaseToReservationErrorResponse, - DHCPReservationSchemaRequest, - DHCPReservationSchemaResponse, - DHCPStateSchemaResponse, - DHCPSubnetSchemaAddRequest, - DHCPSubnetSchemaResponse, -) from .adapter import DHCPAdapter diff --git a/app/ldap_protocol/dhcp/schemas.py b/app/api/dhcp/schemas.py similarity index 71% rename from app/ldap_protocol/dhcp/schemas.py rename to app/api/dhcp/schemas.py index 8f3b0a2c6..c3e10dde8 100644 --- a/app/ldap_protocol/dhcp/schemas.py +++ b/app/api/dhcp/schemas.py @@ -1,56 +1,15 @@ -"""Schemas for DHCP manager. +"""DHCP schemas. Copyright (c) 2025 MultiFactor License: https://github.com/MultiDirectoryLab/MultiDirectory/blob/main/LICENSE """ -from dataclasses import dataclass, field from datetime import datetime from ipaddress import IPv4Address, IPv4Network from pydantic import BaseModel, field_serializer -from .dataclasses import DHCPLease, DHCPReservation, DHCPSubnet -from .enums import DHCPManagerState, KeaDHCPCommands - - -@dataclass -class KeaDHCPCommandRequest: - """Single command request.""" - - command: KeaDHCPCommands - - -@dataclass -class KeaDHCPBaseAPIRequest(KeaDHCPCommandRequest): - """Base request for Kea DHCP API.""" - - arguments: list[int] | dict[str, str] | None = None - service: list[str] = field(default_factory=lambda: ["dhcp4"]) - - -@dataclass -class KeaDHCPAPISubnetRequest(KeaDHCPCommandRequest): - """Request for Kea DHCP API to manage subnets.""" - - subnet4: DHCPSubnet | list[DHCPSubnet] - service: list[str] = field(default_factory=lambda: ["dhcp4"]) - - -@dataclass -class KeaDHCPAPILeaseRequest(KeaDHCPCommandRequest): - """Request for Kea DHCP API to manage leases.""" - - lease: DHCPLease - service: list[str] = field(default_factory=lambda: ["dhcp4"]) - - -@dataclass -class KeaDHCPAPIReservationRequest(KeaDHCPCommandRequest): - """Request for Kea DHCP API to manage reservations.""" - - arguments: DHCPReservation - service: list[str] = field(default_factory=lambda: ["dhcp4"]) +from ldap_protocol.dhcp.enums import DHCPManagerState class DHCPSubnetSchemaAddRequest(BaseModel): diff --git a/app/ldap_protocol/auth/auth_manager.py b/app/ldap_protocol/auth/auth_manager.py index d2e9073fd..6dff8b69e 100644 --- a/app/ldap_protocol/auth/auth_manager.py +++ b/app/ldap_protocol/auth/auth_manager.py @@ -14,9 +14,8 @@ from config import Settings from entities import User from enums import AuthorizationRules, MFAFlags -from ldap_protocol.auth.dto import SetupDTO +from ldap_protocol.auth.dto import LoginRequestDTO, LoginResponseDTO, SetupDTO from ldap_protocol.auth.mfa_manager import MFAManager -from ldap_protocol.auth.schemas import LoginDTO, OAuth2Form from ldap_protocol.auth.use_cases import SetupUseCase from ldap_protocol.auth.utils import authenticate_user from ldap_protocol.dialogue import UserSchema @@ -100,11 +99,11 @@ def __getattribute__(self, name: str) -> object: async def login( self, - form: OAuth2Form, + form: LoginRequestDTO, url: URL, ip: IPv4Address | IPv6Address, user_agent: str, - ) -> LoginDTO: + ) -> LoginResponseDTO: """Log in a user. :param form: OAuth2Form with username and password @@ -169,8 +168,8 @@ async def login( ) if request_2fa: ( - mfa_challenge, - key, + mfa_challenge_dto, + session_key, ) = await self._mfa_manager.two_factor_protocol( user=user, network_policy=network_policy, @@ -178,7 +177,10 @@ async def login( ip=ip, user_agent=user_agent, ) - return LoginDTO(key, mfa_challenge) + return LoginResponseDTO( + session_key=session_key, + mfa_challenge=mfa_challenge_dto, + ) session_key = await self._repository.create_session_key( user, @@ -186,7 +188,10 @@ async def login( user_agent, self.key_ttl, ) - return LoginDTO(session_key, None) + return LoginResponseDTO( + session_key=session_key, + mfa_challenge=None, + ) async def _update_password( self, diff --git a/app/ldap_protocol/auth/dto.py b/app/ldap_protocol/auth/dto.py index 909c0f35e..80ac483d0 100644 --- a/app/ldap_protocol/auth/dto.py +++ b/app/ldap_protocol/auth/dto.py @@ -6,6 +6,16 @@ from dataclasses import dataclass +from enums import MFAChallengeStatuses + + +@dataclass +class LoginRequestDTO: + """Login request DTO.""" + + username: str + password: str + @dataclass class SetupDTO: @@ -17,3 +27,40 @@ class SetupDTO: display_name: str mail: str password: str + + +@dataclass +class MFAChallengeResponseDTO: + """MFA challenge response DTO.""" + + status: MFAChallengeStatuses + message: str + + +@dataclass +class LoginResponseDTO: + """Login response DTO.""" + + session_key: str | None + mfa_challenge: MFAChallengeResponseDTO | None + + +@dataclass +class MFACreateRequestDTO: + """MFA create request DTO.""" + + mfa_key: str + mfa_secret: str + is_ldap_scope: bool + secret_name: str + key_name: str + + +@dataclass +class MFAGetResponseDTO: + """MFA get response DTO.""" + + mfa_key: str | None + mfa_secret: str | None + mfa_key_ldap: str | None + mfa_secret_ldap: str | None diff --git a/app/ldap_protocol/auth/mfa_manager.py b/app/ldap_protocol/auth/mfa_manager.py index 334a66a44..0e24f2285 100644 --- a/app/ldap_protocol/auth/mfa_manager.py +++ b/app/ldap_protocol/auth/mfa_manager.py @@ -21,6 +21,11 @@ from config import Settings from entities import CatalogueSetting, NetworkPolicy, User from enums import AuthorizationRules, MFAChallengeStatuses, MFAFlags +from ldap_protocol.auth.dto import ( + MFAChallengeResponseDTO, + MFACreateRequestDTO, + MFAGetResponseDTO, +) from ldap_protocol.auth.exceptions.mfa import ( AuthenticationError, ForbiddenError, @@ -31,11 +36,6 @@ MissingMFACredentialsError, NetworkPolicyError, ) -from ldap_protocol.auth.schemas import ( - MFAChallengeResponse, - MFACreateRequest, - MFAGetResponse, -) from ldap_protocol.auth.utils import get_user from ldap_protocol.identity import IdentityProvider from ldap_protocol.multifactor import ( @@ -102,10 +102,10 @@ def __getattribute__(self, name: str) -> object: return self._monitor.wrap_proxy_request(attr) return attr - async def setup_mfa(self, mfa: MFACreateRequest) -> bool: + async def setup_mfa(self, mfa: MFACreateRequestDTO) -> bool: """Create or update MFA keys. - :param mfa: MFACreateRequest + :param mfa: MFACreateRequestDTO :return: bool """ async with self._session.begin_nested(): @@ -151,12 +151,12 @@ async def get_mfa( self, mfa_creds: MFA_HTTP_Creds | None, mfa_creds_ldap: MFA_LDAP_Creds | None, - ) -> MFAGetResponse: + ) -> MFAGetResponseDTO: """Get MFA keys for http and ldap. :param mfa_creds: MFA_HTTP_Creds or None :param mfa_creds_ldap: MFA_LDAP_Creds or None - :return: MFAGetResponse + :return: MFAGetResponseDTO """ if not mfa_creds: mfa_creds = MFA_HTTP_Creds(Creds(None, None)) @@ -164,7 +164,7 @@ async def get_mfa( if not mfa_creds_ldap: mfa_creds_ldap = MFA_LDAP_Creds(Creds(None, None)) - return MFAGetResponse( + return MFAGetResponseDTO( mfa_key=mfa_creds.key, mfa_secret=mfa_creds.secret, mfa_key_ldap=mfa_creds_ldap.key, @@ -219,14 +219,14 @@ async def _create_bypass_data( message: str, ip: IPv4Address | IPv6Address, user_agent: str, - ) -> tuple[MFAChallengeResponse, str | None]: + ) -> tuple[MFAChallengeResponseDTO, str | None]: """Create session key and response. :param user: User :param message: str :param ip: IPv4Address | IPv6Address :param user_agent: str - :return: tuple[MFAChallengeResponse, str | None] + :return: tuple[MFAChallengeResponseDTO, str | None] """ key = await self._repository.create_session_key( user, @@ -235,7 +235,7 @@ async def _create_bypass_data( self.key_ttl, ) return ( - MFAChallengeResponse( + MFAChallengeResponseDTO( status=MFAChallengeStatuses.BYPASS, message=message, ), @@ -249,7 +249,7 @@ async def two_factor_protocol( url: URL, ip: IPv4Address | IPv6Address, user_agent: str, - ) -> tuple[MFAChallengeResponse, str | None]: + ) -> tuple[MFAChallengeResponseDTO, str | None]: """Initiate two-factor protocol with application. :param user: User @@ -258,7 +258,7 @@ async def two_factor_protocol( :param ip: IP address :param user_agent: User-Agent string :return: - tuple[MFAChallengeResponse, str | None] (session key | None) + tuple[MFAChallengeResponseDTO, str | None] (session key | None) :raises MissingMFACredentialsError: if MFA is not initialized :raises InvalidCredentialsError: if credentials are invalid :raises NetworkPolicyError: if network policy is not passed @@ -300,7 +300,7 @@ async def two_factor_protocol( weakref.finalize(bypass_coro, bypass_coro.close) return ( - MFAChallengeResponse( + MFAChallengeResponseDTO( status=MFAChallengeStatuses.PENDING, message=redirect_url, ), diff --git a/app/ldap_protocol/dhcp/__init__.py b/app/ldap_protocol/dhcp/__init__.py index d9f4c277c..a813b3d03 100644 --- a/app/ldap_protocol/dhcp/__init__.py +++ b/app/ldap_protocol/dhcp/__init__.py @@ -12,17 +12,6 @@ ) from .kea_dhcp_manager import KeaDHCPManager from .kea_dhcp_repository import KeaDHCPAPIRepository -from .schemas import ( - DHCPChangeStateSchemaRequest, - DHCPLeaseSchemaRequest, - DHCPLeaseSchemaResponse, - DHCPLeaseToReservationErrorResponse, - DHCPReservationSchemaRequest, - DHCPReservationSchemaResponse, - DHCPStateSchemaResponse, - DHCPSubnetSchemaAddRequest, - DHCPSubnetSchemaResponse, -) from .stub import StubDHCPAPIRepository, StubDHCPManager @@ -58,13 +47,4 @@ def get_dhcp_api_repository_class( "DHCPOperationError", "DHCPAPIError", "DHCPSubnetSchemaRequest", - "DHCPSubnetSchemaAddRequest", - "DHCPReservationSchemaRequest", - "DHCPSubnetSchemaResponse", - "DHCPLeaseSchemaRequest", - "DHCPLeaseSchemaResponse", - "DHCPReservationSchemaResponse", - "DHCPChangeStateSchemaRequest", - "DHCPStateSchemaResponse", - "DHCPLeaseToReservationErrorResponse", ] diff --git a/app/ldap_protocol/dhcp/dtos.py b/app/ldap_protocol/dhcp/dtos.py new file mode 100644 index 000000000..2b7c097a0 --- /dev/null +++ b/app/ldap_protocol/dhcp/dtos.py @@ -0,0 +1,49 @@ +"""DTOs for DHCP manager. + +Copyright (c) 2025 MultiFactor +License: https://github.com/MultiDirectoryLab/MultiDirectory/blob/main/LICENSE +""" + +from dataclasses import dataclass, field + +from .dataclasses import DHCPLease, DHCPReservation, DHCPSubnet +from .enums import KeaDHCPCommands + + +@dataclass +class KeaDHCPCommandRequest: + """Single command request.""" + + command: KeaDHCPCommands + + +@dataclass +class KeaDHCPBaseAPIRequest(KeaDHCPCommandRequest): + """Base request for Kea DHCP API.""" + + arguments: list[int] | dict[str, str] | None = None + service: list[str] = field(default_factory=lambda: ["dhcp4"]) + + +@dataclass +class KeaDHCPAPISubnetRequest(KeaDHCPCommandRequest): + """Request for Kea DHCP API to manage subnets.""" + + subnet4: DHCPSubnet | list[DHCPSubnet] + service: list[str] = field(default_factory=lambda: ["dhcp4"]) + + +@dataclass +class KeaDHCPAPILeaseRequest(KeaDHCPCommandRequest): + """Request for Kea DHCP API to manage leases.""" + + lease: DHCPLease + service: list[str] = field(default_factory=lambda: ["dhcp4"]) + + +@dataclass +class KeaDHCPAPIReservationRequest(KeaDHCPCommandRequest): + """Request for Kea DHCP API to manage reservations.""" + + arguments: DHCPReservation + service: list[str] = field(default_factory=lambda: ["dhcp4"]) diff --git a/app/ldap_protocol/dhcp/kea_dhcp_repository.py b/app/ldap_protocol/dhcp/kea_dhcp_repository.py index a41c8e82c..849523bec 100644 --- a/app/ldap_protocol/dhcp/kea_dhcp_repository.py +++ b/app/ldap_protocol/dhcp/kea_dhcp_repository.py @@ -17,6 +17,12 @@ DHCPReservation, DHCPSubnet, ) +from .dtos import ( + KeaDHCPAPILeaseRequest, + KeaDHCPAPIReservationRequest, + KeaDHCPAPISubnetRequest, + KeaDHCPBaseAPIRequest, +) from .enums import KeaDHCPCommands, KeaDHCPResultCodes from .exceptions import ( DHCPAPIError, @@ -40,12 +46,6 @@ release_lease_retort, update_subnet_retort, ) -from .schemas import ( - KeaDHCPAPILeaseRequest, - KeaDHCPAPIReservationRequest, - KeaDHCPAPISubnetRequest, - KeaDHCPBaseAPIRequest, -) class KeaDHCPAPIRepository(DHCPAPIRepository): diff --git a/app/ldap_protocol/dhcp/retorts.py b/app/ldap_protocol/dhcp/retorts.py index 2a0d4c11d..023b800a1 100644 --- a/app/ldap_protocol/dhcp/retorts.py +++ b/app/ldap_protocol/dhcp/retorts.py @@ -7,7 +7,7 @@ from adaptix import Retort, name_mapping from .dataclasses import DHCPLease, DHCPReservation, DHCPSubnet -from .schemas import ( +from .dtos import ( KeaDHCPAPILeaseRequest, KeaDHCPAPISubnetRequest, KeaDHCPBaseAPIRequest, diff --git a/app/ldap_protocol/kerberos/schemas.py b/app/ldap_protocol/kerberos/dtos.py similarity index 85% rename from app/ldap_protocol/kerberos/schemas.py rename to app/ldap_protocol/kerberos/dtos.py index b3be3abb5..d01775aee 100644 --- a/app/ldap_protocol/kerberos/schemas.py +++ b/app/ldap_protocol/kerberos/dtos.py @@ -11,7 +11,7 @@ @dataclass -class KerberosAdminDnGroup: +class KerberosAdminDnGroupDTO: """Kerberos admin, services container, and admin group DNs.""" krbadmin_dn: str @@ -20,8 +20,8 @@ class KerberosAdminDnGroup: @dataclass -class AddRequests: - """AddRequests for Kerberos admin structure: group, services, krb_user.""" +class AddRequestsDTO: + """AddRequestsDTO for Kerberos admin structure.""" group: AddRequest services: AddRequest @@ -29,7 +29,7 @@ class AddRequests: @dataclass -class KDCContext: +class KDCContextDTO: """Kerberos KDC configuration context.""" base_dn: str @@ -43,7 +43,7 @@ class KDCContext: @dataclass -class TaskStruct: +class TaskStructDTO: """Structure for background task: function, args, kwargs.""" func: Callable[..., Any] diff --git a/app/ldap_protocol/kerberos/service.py b/app/ldap_protocol/kerberos/service.py index ec630f778..fa838abb9 100644 --- a/app/ldap_protocol/kerberos/service.py +++ b/app/ldap_protocol/kerberos/service.py @@ -30,6 +30,12 @@ from password_utils import PasswordUtils from .base import AbstractKadmin +from .dtos import ( + AddRequestsDTO, + KDCContextDTO, + KerberosAdminDnGroupDTO, + TaskStructDTO, +) from .exceptions import ( KRBAPIAddPrincipalError, KRBAPIConnectionError, @@ -42,7 +48,6 @@ KRBAPIStatusNotFoundError, ) from .ldap_structure import KRBLDAPStructureManager -from .schemas import AddRequests, KDCContext, KerberosAdminDnGroup, TaskStruct from .template_render import KRBTemplateRenderer from .utils import ( KerberosState, @@ -138,17 +143,20 @@ async def _get_base_dn(self) -> tuple[str, str]: ) return base_dn_list[0].path_dn, base_dn_list[0].name - def _build_kerberos_admin_dns(self, base_dn: str) -> KerberosAdminDnGroup: + def _build_kerberos_admin_dns( + self, + base_dn: str, + ) -> KerberosAdminDnGroupDTO: """Build DN strings for Kerberos admin, services, and group. :param str base_dn: Base DN. - :return KerberosAdminDnGroup: + :return KerberosAdminDnGroupDTO: dataclass with DN for krbadmin, services_container, krbadmin_group. """ krbadmin = f"cn=krbadmin,cn=Users,{base_dn}" services_container = get_system_container_dn(base_dn) krbgroup = f"cn=krbadmin,cn=Groups,{base_dn}" - return KerberosAdminDnGroup( + return KerberosAdminDnGroupDTO( krbadmin_dn=krbadmin, services_container_dn=services_container, krbadmin_group_dn=krbgroup, @@ -156,17 +164,17 @@ def _build_kerberos_admin_dns(self, base_dn: str) -> KerberosAdminDnGroup: def _build_add_requests( self, - dns: KerberosAdminDnGroup, + dns: KerberosAdminDnGroupDTO, mail: str, krbadmin_password: SecretStr, - ) -> AddRequests: + ) -> AddRequestsDTO: """Build AddRequest objects for group, services, and admin user. - :param KerberosAdminDnGroup dns: + :param KerberosAdminDnGroupDTO dns: DNs for krbadmin, services container, and group. :param str mail: Email for krbadmin. :param SecretStr krbadmin_password: Password for krbadmin. - :return AddRequests: + :return AddRequestsDTO: dataclass of AddRequest for group, services, and user. """ group = AddRequest.from_dict( @@ -219,7 +227,7 @@ def _build_add_requests( }, is_system=True, ) - return AddRequests( + return AddRequestsDTO( group=group, services=services, krb_user=krb_user, @@ -232,8 +240,8 @@ async def setup_kdc( stash_password: str, user: UserSchema, request: Request, - ) -> TaskStruct: - """Set up KDC, generate configs, and return TaskStruct. + ) -> TaskStructDTO: + """Set up KDC, generate configs, and return TaskStructDTO. Args: krbadmin_password (str): Password for krbadmin. @@ -289,17 +297,17 @@ async def setup_kdc( admin_password, ) - async def _get_kdc_context(self) -> KDCContext: + async def _get_kdc_context(self) -> KDCContextDTO: """Build and return context for KDC setup/config rendering. :raises Exception: If base DN cannot be retrieved. - :return KDCContext: dataclass with all required KDC context fields. + :return KDCContextDTO: dataclass with all required KDC context fields. """ base_dn, domain = await self._get_base_dn() krbadmin = f"cn=krbadmin,cn=users,{base_dn}" krbgroup = f"cn=krbadmin,cn=groups,{base_dn}" services_container = get_system_container_dn(base_dn) - return KDCContext( + return KDCContextDTO( base_dn=base_dn, domain=domain, krbadmin=krbadmin, @@ -335,7 +343,7 @@ async def _schedule_principal_task( request: Request, user: UserSchema, password: str, - ) -> TaskStruct: + ) -> TaskStructDTO: """Schedule background task for principal creation after KDC setup. :param Request request: FastAPI request (for DI container). @@ -356,7 +364,7 @@ async def _schedule_principal_task( user.user_principal_name.split("@")[0], password, ) - return TaskStruct(func=func, args=args) + return TaskStructDTO(func=func, args=args) async def add_principal( self, @@ -428,8 +436,8 @@ async def ktadd( self, names: list[str], is_rand_key: bool, - ) -> tuple[AsyncIterator[bytes], TaskStruct]: - """Generate keytab and return (aiter_bytes, TaskStruct). + ) -> tuple[AsyncIterator[bytes], TaskStructDTO]: + """Generate keytab and return (aiter_bytes, TaskStructDTO). :param list[str] names: List of principal names. :param bool is_rand_key: If True, generate new principal keys. @@ -442,7 +450,7 @@ async def ktadd( raise KerberosNotFoundError("Principal not found") aiter_bytes = response.aiter_bytes() func = response.aclose - return aiter_bytes, TaskStruct(func=func) + return aiter_bytes, TaskStructDTO(func=func) async def get_status(self) -> KerberosState: """Get Kerberos server state (db + actual server). diff --git a/app/ldap_protocol/kerberos/template_render.py b/app/ldap_protocol/kerberos/template_render.py index 0df7b36a7..d4428a596 100644 --- a/app/ldap_protocol/kerberos/template_render.py +++ b/app/ldap_protocol/kerberos/template_render.py @@ -6,7 +6,7 @@ import jinja2 -from .schemas import KDCContext +from .dtos import KDCContextDTO class KRBTemplateRenderer: @@ -23,11 +23,11 @@ def __init__(self, templates: jinja2.Environment) -> None: """ self._templates = templates - async def render_krb5(self, context: KDCContext) -> str: + async def render_krb5(self, context: KDCContextDTO) -> str: """Render the krb5.conf configuration file using the provided context. :param context: - KDCContext dataclass with Kerberos configuration parameters. + KDCContextDTO dataclass with Kerberos configuration parameters. :return: Rendered krb5.conf as a string. """ krb5_template = self._templates.get_template("krb5.conf") @@ -40,11 +40,11 @@ async def render_krb5(self, context: KDCContext) -> str: sync_password_url=context.sync_password_url, ) - async def render_kdc(self, context: KDCContext) -> str: + async def render_kdc(self, context: KDCContextDTO) -> str: """Render the kdc.conf configuration file using the provided context. :param context: - KDCContext dataclass with Kerberos configuration parameters. + KDCContextDTO dataclass with Kerberos configuration parameters. :return: Rendered kdc.conf as a string. """ kdc_template = self._templates.get_template("kdc.conf") diff --git a/app/ldap_protocol/policies/audit/monitor.py b/app/ldap_protocol/policies/audit/monitor.py index 5ce08d0ab..6258daf49 100644 --- a/app/ldap_protocol/policies/audit/monitor.py +++ b/app/ldap_protocol/policies/audit/monitor.py @@ -14,6 +14,7 @@ from config import Settings from entities import User +from ldap_protocol.auth.dto import LoginRequestDTO from ldap_protocol.auth.exceptions.mfa import ( AuthenticationError, ForbiddenError, @@ -22,7 +23,6 @@ MFATokenError, NetworkPolicyError, ) -from ldap_protocol.auth.schemas import OAuth2Form from ldap_protocol.identity.exceptions import ( AuthorizationError, AuthValidationError, @@ -224,7 +224,7 @@ async def wrapped_proxy_request( def wrap_login(self, attr: _T) -> _T: @wraps(attr) async def wrapped_login( - form: OAuth2Form, + form: LoginRequestDTO, url: URL, ip: IPv4Address | IPv6Address, user_agent: str, diff --git a/interface b/interface index e1ca5656a..3732b6958 160000 --- a/interface +++ b/interface @@ -1 +1 @@ -Subproject commit e1ca5656aeabc20a1862aeaf11ded72feaa97403 +Subproject commit 3732b695844e95e1692ae83e1b2e1de70e68b380 diff --git a/tests/test_api/test_audit/test_router.py b/tests/test_api/test_audit/test_router.py index 2abc682b6..bd201d23f 100644 --- a/tests/test_api/test_audit/test_router.py +++ b/tests/test_api/test_audit/test_router.py @@ -10,15 +10,15 @@ from fastapi import status from httpx import AsyncClient +from api.audit.schemas import ( + AuditDestinationSchemaRequest, + AuditPolicySchemaRequest, +) from enums import AuditDestinationProtocolType, AuditDestinationServiceType from ldap_protocol.policies.audit.dataclasses import ( AuditDestinationDTO, AuditPolicyDTO, ) -from ldap_protocol.policies.audit.schemas import ( - AuditDestinationSchemaRequest, - AuditPolicySchemaRequest, -) @pytest.mark.asyncio diff --git a/tests/test_api/test_dhcp/test_adapter.py b/tests/test_api/test_dhcp/test_adapter.py index 5d2dd4b26..f67b03016 100644 --- a/tests/test_api/test_dhcp/test_adapter.py +++ b/tests/test_api/test_dhcp/test_adapter.py @@ -10,6 +10,11 @@ import pytest from api.dhcp.adapter import DHCPAdapter +from api.dhcp.schemas import ( + DHCPLeaseSchemaRequest, + DHCPReservationSchemaRequest, + DHCPSubnetSchemaAddRequest, +) from authorization_provider_protocol import AuthorizationProviderProtocol from ldap_protocol.dhcp.dataclasses import ( DHCPLease, @@ -18,11 +23,6 @@ DHCPReservation, DHCPSubnet, ) -from ldap_protocol.dhcp.schemas import ( - DHCPLeaseSchemaRequest, - DHCPReservationSchemaRequest, - DHCPSubnetSchemaAddRequest, -) @pytest.fixture