diff --git a/Makefile b/Makefile index 932fe2643..1e5b5f2cf 100644 --- a/Makefile +++ b/Makefile @@ -31,10 +31,10 @@ launch: ## run standalone app without tty container docker compose run sh -c "python multidirectory.py --migrate && python ." rerun_last_migration: - docker exec -it multidirectory_api sh -c "alembic downgrade -1; python multidirectory.py --migrate;" + docker exec -it multidirectory_api sh -c "python multidirectory.py --downgrade -1; python multidirectory.py --migrate;" rerun_all_migrations: - docker exec -it multidirectory_api sh -c "alembic downgrade base; python multidirectory.py --migrate;" + docker exec -it multidirectory_api sh -c "python multidirectory.py --downgrade base; python multidirectory.py --migrate;" down: ## shutdown services docker compose -f docker-compose.test.yml down --remove-orphans diff --git a/app/alembic/versions/21a957c18dce_create_directory_name_indexes.py b/app/alembic/versions/21a957c18dce_create_directory_name_indexes.py new file mode 100644 index 000000000..32f03077f --- /dev/null +++ b/app/alembic/versions/21a957c18dce_create_directory_name_indexes.py @@ -0,0 +1,48 @@ +"""Create Directory.name indexes. + +Revision ID: 21a957c18dce +Revises: 1b71cafba681 +Create Date: 2026-04-10 11:15:22.133564 + +""" + +import sqlalchemy as sa +from alembic import op +from dishka import AsyncContainer + +# revision identifiers, used by Alembic. +revision: None | str = "21a957c18dce" +down_revision: None | str = "1b71cafba681" +branch_labels: None | list[str] = None +depends_on: None | list[str] = None + + +def upgrade(container: AsyncContainer) -> None: # noqa: ARG001 + """Upgrade.""" + op.execute("CREATE EXTENSION IF NOT EXISTS pg_trgm") + op.create_index( + "idx_Directory_name_hash", + "Directory", + ["name"], + unique=False, + postgresql_using="hash", + ) + op.create_index( + "idx_Directory_name_gin_trgm", + "Directory", + [sa.literal_column("name gin_trgm_ops")], + unique=False, + postgresql_using="gin", + ) + + +def downgrade(container: AsyncContainer) -> None: # noqa: ARG001 + """Downgrade.""" + op.drop_index( + "idx_Directory_name_gin_trgm", + table_name="Directory", + ) + op.drop_index( + "idx_Directory_name_hash", + table_name="Directory", + ) diff --git a/app/api/ldap_schema/__init__.py b/app/api/ldap_schema/__init__.py index a0314c320..5b303a117 100644 --- a/app/api/ldap_schema/__init__.py +++ b/app/api/ldap_schema/__init__.py @@ -18,6 +18,7 @@ DomainErrorTranslator, ) from enums import DomainCodes +from ldap_protocol.identity.exceptions import UnauthorizedError from ldap_protocol.ldap_schema.exceptions import ( AttributeTypeAlreadyExistsError, AttributeTypeCantModifyError, @@ -40,6 +41,10 @@ error_map: ERROR_MAP_TYPE = { + UnauthorizedError: rule( + status=status.HTTP_401_UNAUTHORIZED, + translator=translator, + ), AttributeTypeAlreadyExistsError: rule( status=status.HTTP_400_BAD_REQUEST, translator=translator, diff --git a/app/ldap_protocol/ldap_schema/_legacy/attribute_type/attribute_type_dao.py b/app/ldap_protocol/ldap_schema/_legacy/attribute_type/attribute_type_dao.py index 823df60fd..e0c7be24f 100644 --- a/app/ldap_protocol/ldap_schema/_legacy/attribute_type/attribute_type_dao.py +++ b/app/ldap_protocol/ldap_schema/_legacy/attribute_type/attribute_type_dao.py @@ -30,7 +30,7 @@ def _convert_model_to_dto( ) return AttributeTypeDTO[int]( oid=attr_type.oid, - name=ldap_display_name, + name=attr_type.name, ldap_display_name=ldap_display_name, syntax=attr_type.syntax, single_value=attr_type.single_value, diff --git a/app/ldap_protocol/ldap_schema/attribute_type/attribute_type_dao.py b/app/ldap_protocol/ldap_schema/attribute_type/attribute_type_dao.py index fc5bf344f..32e8f8855 100644 --- a/app/ldap_protocol/ldap_schema/attribute_type/attribute_type_dao.py +++ b/app/ldap_protocol/ldap_schema/attribute_type/attribute_type_dao.py @@ -144,7 +144,7 @@ async def get_paginator( filters = [qa(EntityType.name) == EntityTypeNames.ATTRIBUTE_TYPE] if params.query: - filters.append(qa(Directory.name).like(f"%{params.query}%")) + filters.append(qa(Directory.name).ilike(f"%{params.query}%")) query = ( select(Directory) diff --git a/app/ldap_protocol/ldap_schema/object_class/object_class_dao.py b/app/ldap_protocol/ldap_schema/object_class/object_class_dao.py index 8baab32f8..49c204bdf 100644 --- a/app/ldap_protocol/ldap_schema/object_class/object_class_dao.py +++ b/app/ldap_protocol/ldap_schema/object_class/object_class_dao.py @@ -116,7 +116,7 @@ async def get_paginator( filters = [qa(EntityType.name) == EntityTypeNames.OBJECT_CLASS] if params.query: - filters.append(qa(Directory.name).like(f"%{params.query}%")) + filters.append(qa(Directory.name).ilike(f"%{params.query}%")) query = ( select(Directory) diff --git a/app/multidirectory.py b/app/multidirectory.py index 7e7c58862..7c2e67a8b 100644 --- a/app/multidirectory.py +++ b/app/multidirectory.py @@ -350,6 +350,11 @@ async def migrate_dns_factory(settings: Settings) -> None: action="store_true", help="Make migrations", ) + group.add_argument( + "--downgrade", + metavar="REV", + help="Downgrade database to revision", + ) group.add_argument( "--migrate_dns", action="store_true", @@ -397,5 +402,7 @@ async def migrate_dns_factory(settings: Settings) -> None: dump_acme_cert() elif args.migrate: command.upgrade(Config("alembic.ini"), "head") + elif args.downgrade: + command.downgrade(Config("alembic.ini"), args.downgrade) elif args.migrate_dns: dns_migration(settings=settings) diff --git a/app/repo/pg/tables.py b/app/repo/pg/tables.py index be17cef6f..135b40992 100644 --- a/app/repo/pg/tables.py +++ b/app/repo/pg/tables.py @@ -169,6 +169,8 @@ def _compile_create_uc( text("array_lowercase(path)"), postgresql_using="hash", ), + Index("idx_Directory_name_hash", "name", postgresql_using="hash"), + Index("idx_Directory_name_gin_trgm", "name", postgresql_using="gin"), ) groups_table = Table( diff --git a/interface b/interface index 046449cdd..9e7aa8355 160000 --- a/interface +++ b/interface @@ -1 +1 @@ -Subproject commit 046449cdd568919cca12a7939366dcee7a54fdfa +Subproject commit 9e7aa8355dc40c8d14d16ba9511e07d0e995d13a diff --git a/tests/test_api/test_ldap_schema/test_attribute_type_router.py b/tests/test_api/test_ldap_schema/test_attribute_type_router.py index 18ae827d1..bf366bbd0 100644 --- a/tests/test_api/test_ldap_schema/test_attribute_type_router.py +++ b/tests/test_api/test_ldap_schema/test_attribute_type_router.py @@ -105,6 +105,20 @@ async def test_get_list_attribute_types_with_pagination( assert len(response.json().get("items")) == page_size +@pytest.mark.asyncio +async def test_attribute_type_pagination_search_is_case_insensitive( + http_client: AsyncClient, +) -> None: + """Test case-insensitive search for attribute type pagination.""" + response = await http_client.get( + "/schema/attribute_types", + params={"page_number": 1, "page_size": 50, "query": "PoSiXeMaIl"}, + ) + assert response.status_code == status.HTTP_200_OK + items = response.json().get("items", []) + assert any(item.get("name") == "posixEmail" for item in items) + + @pytest.mark.asyncio async def test_modify_one_attribute_type_raise_404( http_client: AsyncClient, diff --git a/tests/test_api/test_ldap_schema/test_object_class_router.py b/tests/test_api/test_ldap_schema/test_object_class_router.py index 1359c7d4e..0531f80ef 100644 --- a/tests/test_api/test_ldap_schema/test_object_class_router.py +++ b/tests/test_api/test_ldap_schema/test_object_class_router.py @@ -133,6 +133,20 @@ async def test_get_list_object_classes_with_pagination( assert len(response.json().get("items")) == page_size +@pytest.mark.asyncio +async def test_object_class_pagination_search_is_case_insensitive( + http_client: AsyncClient, +) -> None: + """Test case-insensitive search for object class pagination.""" + response = await http_client.get( + "/schema/object_classes", + params={"page_number": 1, "page_size": 50, "query": "InEtOrGpErSoN"}, + ) + assert response.status_code == status.HTTP_200_OK + items = response.json().get("items", []) + assert any(item.get("name") == "inetOrgPerson" for item in items) + + @pytest.mark.parametrize( "dataset", test_modify_one_object_class_dataset,