diff --git a/.docker/lint.Dockerfile b/.docker/lint.Dockerfile index 8b11ab301..0daa07beb 100644 --- a/.docker/lint.Dockerfile +++ b/.docker/lint.Dockerfile @@ -29,5 +29,4 @@ ENV VIRTUAL_ENV=/venvs/.venv \ COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV} -COPY app /app -COPY pyproject.toml ./ +COPY . /app diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 7b347b266..b21f307bf 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -41,7 +41,7 @@ jobs: - name: Load image run: docker load -i /tmp/linter_image.tar - name: Run ruff check - run: docker run linter:latest ruff check --output-format=github . + run: docker run linter:latest ruff check --output-format=github run_ruff_format: runs-on: ubuntu-latest diff --git a/Makefile b/Makefile index 87f2b8857..efc0edf41 100644 --- a/Makefile +++ b/Makefile @@ -2,10 +2,10 @@ help: ## show help message @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[$$()% a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) -before_pr: - ruff format ./app - ruff check ./app --fix --unsafe-fixes - mypy ./app +before_pr: ## format, lint and type-check code + ruff format + ruff check --fix --unsafe-fixes + mypy . build: ## build app and manually generate self-signed cert make down @@ -15,29 +15,35 @@ build_test: docker compose -f docker-compose.test.yml build up: ## run tty container with related services, use with run command - make down; docker compose up + make down + docker compose up test: ## run tests docker compose -f docker-compose.test.yml down --remove-orphans - make down; + make down docker compose -f docker-compose.test.yml up --no-log-prefix --attach test --exit-code-from test run: ## runs server 386/636 port clear;docker exec -it multidirectory sh -c "python ." launch: ## run standalone app without tty container - docker compose down; - docker compose run sh -c "alembic upgrade head && python ." + docker compose down + docker compose run sh -c "python multidirectory.py --migrate && python ." -downgrade: ## re-run migration - docker exec -it multidirectory_api sh -c\ - "alembic downgrade -1; alembic upgrade head;" +rerun_last_migration: ## re-run migration + docker exec -it multidirectory_api sh -c "alembic downgrade -1; python multidirectory.py --migrate;" down: ## shutdown services docker compose -f docker-compose.test.yml down --remove-orphans docker compose down --remove-orphans docker volume prune -f +migrations: ## generate migration file + docker compose run ldap_server alembic revision --autogenerate + +migrate: ## upgrade db + docker compose run ldap_server python multidirectory.py --migrate + # server stage/development commands stage_gen_cert: ## generate self-signed cert @@ -48,28 +54,21 @@ stage_build: ## build stage server docker compose -f docker-compose.dev.yml build stage_up: ## run app and detach - make stage_down; + make stage_down docker compose -f docker-compose.dev.yml up -d stage_down: ## stop all services docker compose -f docker-compose.dev.yml down --remove-orphans stage_update: ## update service - make stage_down; - make stage_build; - docker compose -f docker-compose.dev.yml pull; - make stage_up; - docker exec -it multidirectory-ldap sh -c\ - "alembic downgrade -1; alembic upgrade head; python -m extra.setup_dev" + make stage_down + make stage_build + docker compose -f docker-compose.dev.yml pull + make stage_up + docker exec -it multidirectory-ldap sh -c "alembic downgrade -1; python multidirectory.py --migrate; python -m extra.setup_dev" krb_client_build: ## build krb client service docker build -f integration_tests/kerberos/Dockerfile . -t krbclient:runtime krb_client: ## run krb client bash docker run --rm --init -it --name krbclient --network multidirectory_default krbclient:runtime bash - -migrations: ## generate migration file - docker compose run ldap_server alembic revision --autogenerate - -migrate: ## upgrade db - docker compose run ldap_server alembic upgrade head diff --git a/app/alembic/versions/6c858cc05da7_add_default_admin_name.py b/app/alembic/versions/6c858cc05da7_add_default_admin_name.py index 4d608e134..7b1a59f1e 100644 --- a/app/alembic/versions/6c858cc05da7_add_default_admin_name.py +++ b/app/alembic/versions/6c858cc05da7_add_default_admin_name.py @@ -55,4 +55,3 @@ def upgrade(container: AsyncContainer) -> None: # noqa: ARG001 def downgrade(container: AsyncContainer) -> None: """Downgrade.""" - # Откатывать не нужно diff --git a/app/alembic/versions/ec45e3e8aa0f_drop_password_policy_id_column.py b/app/alembic/versions/ec45e3e8aa0f_drop_password_policy_id_column.py new file mode 100644 index 000000000..a01bd542d --- /dev/null +++ b/app/alembic/versions/ec45e3e8aa0f_drop_password_policy_id_column.py @@ -0,0 +1,47 @@ +"""Drop unused Directory.password_policy_id column. + +Revision ID: ec45e3e8aa0f +Revises: a1b2c3d4e5f6 +Create Date: 2026-01-20 14:33:36.236135 + +""" + +import sqlalchemy as sa +from alembic import op +from dishka import AsyncContainer + +# revision identifiers, used by Alembic. +revision: None | str = "ec45e3e8aa0f" +down_revision: None | str = "a1b2c3d4e5f6" +branch_labels: None | list[str] = None +depends_on: None | list[str] = None + + +def upgrade(container: AsyncContainer) -> None: # noqa: ARG001 + """Upgrade.""" + op.drop_constraint( + op.f("Directory_password_policy_id_fkey"), + "Directory", + type_="foreignkey", + ) + op.drop_column("Directory", "password_policy_id") + + +def downgrade(container: AsyncContainer) -> None: # noqa: ARG001 + """Downgrade.""" + op.add_column( + "Directory", + sa.Column( + "password_policy_id", + sa.INTEGER(), + autoincrement=False, + nullable=True, + ), + ) + op.create_foreign_key( + op.f("Directory_password_policy_id_fkey"), + "Directory", + "PasswordPolicies", + ["password_policy_id"], + ["id"], + ) diff --git a/app/api/exception_handlers.py b/app/api/exception_handlers.py index eabda8d4b..edf5ce363 100644 --- a/app/api/exception_handlers.py +++ b/app/api/exception_handlers.py @@ -29,6 +29,6 @@ async def handle_auth_error( exc: Exception, ) -> NoReturn: """Handle Auth error.""" - # fastapi-error-map doesn't handle exceptions from dependencie, - # (get_ldap_session) потому ловим так + # fastapi-error-map doesn't handle exceptions from dependencies + # (get_ldap_session), so we catch them manually here raise HTTPException(status.HTTP_401_UNAUTHORIZED, detail=str(exc)) diff --git a/app/entities.py b/app/entities.py index 53f5c95e9..535da02f6 100644 --- a/app/entities.py +++ b/app/entities.py @@ -202,7 +202,6 @@ class Directory: ) updated_at: datetime | None = field(default=None) depth: int = field(default=0) - password_policy_id: int | None = None path: list[str] = field(default_factory=list) parent: Directory | None = field(default=None, repr=False, compare=False) diff --git a/app/errors/base.py b/app/errors/base.py index 435aa339c..18419ccba 100644 --- a/app/errors/base.py +++ b/app/errors/base.py @@ -7,7 +7,7 @@ from enum import IntEnum -class BaseDomainException(Exception): # noqa N818 +class BaseDomainException(Exception): # noqa: N818 """Base exception.""" code: IntEnum diff --git a/app/ldap_protocol/utils/pagination.py b/app/ldap_protocol/utils/pagination.py index f82bb7c96..5e4ef6e4b 100644 --- a/app/ldap_protocol/utils/pagination.py +++ b/app/ldap_protocol/utils/pagination.py @@ -104,7 +104,7 @@ async def get( session: AsyncSession, ) -> Self: """Get paginator.""" - if query._order_by_clause is None or len(query._order_by_clause) == 0: # noqa SLF001 + if query._order_by_clause is None or len(query._order_by_clause) == 0: # noqa: SLF001 raise ValueError("Select query must have an order_by clause.") metadata = PaginationMetadata( diff --git a/app/repo/pg/tables.py b/app/repo/pg/tables.py index 7b463a597..5391c95d5 100644 --- a/app/repo/pg/tables.py +++ b/app/repo/pg/tables.py @@ -147,12 +147,6 @@ def _compile_create_uc( ), Column("depth", Integer, nullable=True), Column("objectSid", String, nullable=True, key="object_sid"), - Column( - "password_policy_id", - Integer, - ForeignKey("PasswordPolicies.id"), - nullable=True, - ), Column( "objectGUID", PG_UUID(as_uuid=True), diff --git a/tests/api_datasets.py b/tests/api_datasets.py index 975199bd6..a9f430ef4 100644 --- a/tests/api_datasets.py +++ b/tests/api_datasets.py @@ -20,7 +20,7 @@ "contains:colon", "contains+plus", "contains*asterisk", - "contains\"doublequotes", # noqa: Q003 + 'contains"doublequotes', "multiple#forbidden=chars<>here", "#starts_with_hash", "ends_with_semicolon;", @@ -39,8 +39,8 @@ ":", "+", "*", - "\"", # noqa: Q003 - "#=<>\\;:+*\"", # noqa: Q003 + '"', + '#=<>\\;:+*"', "", " ", " ", diff --git a/tests/test_api/test_auth/test_router.py b/tests/test_api/test_auth/test_router.py index 26c0e4523..c13c0a5a6 100644 --- a/tests/test_api/test_auth/test_router.py +++ b/tests/test_api/test_auth/test_router.py @@ -13,7 +13,6 @@ from fastapi import status from httpx import AsyncClient from jose import jwt -from password_utils import PasswordUtils from sqlalchemy import select, update from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import joinedload @@ -26,6 +25,7 @@ from ldap_protocol.ldap_requests.modify import Operation from ldap_protocol.session_storage import SessionStorage from ldap_protocol.utils.queries import get_search_path +from password_utils import PasswordUtils from repo.pg.tables import queryable_attr as qa from tests.conftest import TestCreds diff --git a/tests/test_api/test_dhcp/test_adapter.py b/tests/test_api/test_dhcp/test_adapter.py index d8816d887..5d2dd4b26 100644 --- a/tests/test_api/test_dhcp/test_adapter.py +++ b/tests/test_api/test_dhcp/test_adapter.py @@ -8,9 +8,9 @@ from unittest.mock import Mock import pytest -from authorization_provider_protocol import AuthorizationProviderProtocol from api.dhcp.adapter import DHCPAdapter +from authorization_provider_protocol import AuthorizationProviderProtocol from ldap_protocol.dhcp.dataclasses import ( DHCPLease, DHCPOptionData, diff --git a/tests/test_api/test_main/test_kadmin.py b/tests/test_api/test_main/test_kadmin.py index aabfcf14f..0ffbd6ebe 100644 --- a/tests/test_api/test_main/test_kadmin.py +++ b/tests/test_api/test_main/test_kadmin.py @@ -60,7 +60,7 @@ def _create_test_user_data( async def test_tree_creation( http_client: AsyncClient, ctx_bind: LDAPBindRequestContext, - password_utils: PasswordUtils, + password_utils: PasswordUtils, # noqa: ARG001 ) -> None: """Test tree creation.""" krbadmin_pw = "Password123" diff --git a/tests/test_api/test_main/test_router/test_modify_dn.py b/tests/test_api/test_main/test_router/test_modify_dn.py index fe27de21c..b27360dae 100644 --- a/tests/test_api/test_main/test_router/test_modify_dn.py +++ b/tests/test_api/test_main/test_router/test_modify_dn.py @@ -91,7 +91,7 @@ async def test_api_modify_dn_with_level_down( response = await http_client.post( "entry/search", json={ - "base_object": "cn=testGroup1,ou=testModifyDn2,ou=testModifyDn1,dc=md,dc=test", + "base_object": "cn=testGroup1,ou=testModifyDn2,ou=testModifyDn1,dc=md,dc=test", # noqa: E501 "scope": 0, "deref_aliases": 0, "size_limit": 1000, diff --git a/tests/test_ldap/test_access_manager/test_search_access.py b/tests/test_ldap/test_access_manager/test_search_access.py index dd6f20b67..c586e2fc4 100644 --- a/tests/test_ldap/test_access_manager/test_search_access.py +++ b/tests/test_ldap/test_access_manager/test_search_access.py @@ -123,11 +123,11 @@ def test_check_search_access( expected_result: tuple[bool, set[str], set[str]], ) -> None: """Test the check_search_access method of AccessManager.""" - filtered_aces = AccessManager._filter_aces_by_entity_type( + filtered_aces = AccessManager._filter_aces_by_entity_type( # noqa: SLF001 aces, entity_type_id, ) - result = AccessManager._check_search_access(filtered_aces) + result = AccessManager._check_search_access(filtered_aces) # noqa: SLF001 assert result == expected_result diff --git a/tests/test_ldap/test_bind.py b/tests/test_ldap/test_bind.py index b2aa0a0e5..e31353880 100644 --- a/tests/test_ldap/test_bind.py +++ b/tests/test_ldap/test_bind.py @@ -79,12 +79,12 @@ async def mock_init_security_context( session: AsyncSession, # noqa: ARG001 settings: Settings, # noqa: ARG001 ) -> None: - auth_choice._ldap_session.gssapi_security_context = ( + auth_choice._ldap_session.gssapi_security_context = ( # noqa: SLF001 mock_security_context ) auth_choice = SaslGSSAPIAuthentication(ticket=b"ticket") - auth_choice._init_security_context = mock_init_security_context # type: ignore + auth_choice._init_security_context = mock_init_security_context # type: ignore # noqa: SLF001 bind = BindRequest( version=0, @@ -150,12 +150,12 @@ async def mock_init_security_context( session: AsyncSession, # noqa: ARG001 settings: Settings, # noqa: ARG001 ) -> None: - auth_choice._ldap_session.gssapi_security_context = ( + auth_choice._ldap_session.gssapi_security_context = ( # noqa: SLF001 mock_security_context ) auth_choice = SaslGSSAPIAuthentication(ticket=b"client_ticket") - auth_choice._init_security_context = mock_init_security_context # type: ignore + auth_choice._init_security_context = mock_init_security_context # type: ignore # noqa: SLF001 first_bind = BindRequest( version=0, @@ -218,12 +218,12 @@ async def mock_init_security_context( session: AsyncSession, # noqa: ARG001 settings: Settings, # noqa: ARG001 ) -> None: - auth_choice._ldap_session.gssapi_security_context = ( + auth_choice._ldap_session.gssapi_security_context = ( # noqa: SLF001 mock_security_context ) auth_choice = SaslSPNEGOAuthentication(ticket=b"client_ticket") - auth_choice._init_security_context = mock_init_security_context # type: ignore + auth_choice._init_security_context = mock_init_security_context # type: ignore # noqa: SLF001 first_bind = BindRequest( version=0,