diff --git a/app/alembic/versions/01f3f05a5b11_add_primary_group_id.py b/app/alembic/versions/01f3f05a5b11_add_primary_group_id.py index bfa82dbfc..73be02c1d 100644 --- a/app/alembic/versions/01f3f05a5b11_add_primary_group_id.py +++ b/app/alembic/versions/01f3f05a5b11_add_primary_group_id.py @@ -1,11 +1,12 @@ """Add primaryGroupId attribute and domain computers group. Revision ID: 01f3f05a5b11 -Revises: 8164b4a9e1f1 +Revises: c007129b7973 Create Date: 2025-09-26 12:36:05.974255 """ +import sqlalchemy as sa from alembic import op from dishka import AsyncContainer, Scope from sqlalchemy import delete, exists, select @@ -13,8 +14,10 @@ from sqlalchemy.ext.asyncio import AsyncConnection, AsyncSession from sqlalchemy.orm import Session, selectinload +from constants import DOMAIN_COMPUTERS_GROUP_NAME from entities import Attribute, Directory, EntityType, Group from enums import EntityTypeNames +from extra.alembic_utils import temporary_stub_column from ldap_protocol.ldap_schema.attribute_value_validator import ( AttributeValueValidator, ) @@ -35,6 +38,7 @@ depends_on: None = None +@temporary_stub_column("is_system", sa.Boolean()) def upgrade(container: AsyncContainer) -> None: """Upgrade.""" @@ -51,7 +55,7 @@ async def _add_domain_computers_group(connection: AsyncConnection) -> None: # n try: group_dir_query = select( exists(Directory) - .where(qa(Directory.name) == "domain computers"), + .where(qa(Directory.name) == DOMAIN_COMPUTERS_GROUP_NAME), ) # fmt: skip group_dir = (await session.scalars(group_dir_query)).one() @@ -59,7 +63,7 @@ async def _add_domain_computers_group(connection: AsyncConnection) -> None: # n return dir_, group_ = await create_group( - name="domain computers", + name=DOMAIN_COMPUTERS_GROUP_NAME, sid=515, attribute_value_validator=AttributeValueValidator(), session=session, @@ -165,6 +169,7 @@ async def _add_primary_group_id(connection: AsyncConnection) -> None: # noqa: A op.run_async(_add_primary_group_id) +@temporary_stub_column("is_system", sa.Boolean()) def downgrade(container: AsyncContainer) -> None: """Downgrade.""" bind = op.get_bind() diff --git a/app/alembic/versions/05ddc0bd562a_add_roles.py b/app/alembic/versions/05ddc0bd562a_add_roles.py index 203e6b387..aff773460 100644 --- a/app/alembic/versions/05ddc0bd562a_add_roles.py +++ b/app/alembic/versions/05ddc0bd562a_add_roles.py @@ -12,6 +12,7 @@ from sqlalchemy.ext.asyncio import AsyncConnection, AsyncSession from entities import Directory, Group +from extra.alembic_utils import temporary_stub_column from ldap_protocol.roles.role_use_case import RoleUseCase from ldap_protocol.utils.queries import get_base_directories from repo.pg.tables import queryable_attr as qa @@ -23,6 +24,7 @@ depends_on: None = None +@temporary_stub_column("is_system", sa.Boolean()) def upgrade(container: AsyncContainer) -> None: """Upgrade.""" op.create_table( diff --git a/app/alembic/versions/16a9fa2c1f1e_rename_readonly_group.py b/app/alembic/versions/16a9fa2c1f1e_rename_readonly_group.py index 0f9ba47e0..b331dddd5 100644 --- a/app/alembic/versions/16a9fa2c1f1e_rename_readonly_group.py +++ b/app/alembic/versions/16a9fa2c1f1e_rename_readonly_group.py @@ -6,13 +6,16 @@ """ +import sqlalchemy as sa from alembic import op from dishka import AsyncContainer from sqlalchemy import select, update from sqlalchemy.exc import DBAPIError, IntegrityError from sqlalchemy.orm import Session, selectinload +from constants import READ_ONLY_GROUP_NAME from entities import Attribute, Directory +from extra.alembic_utils import temporary_stub_column from repo.pg.tables import queryable_attr as qa # revision identifiers, used by Alembic. @@ -22,6 +25,7 @@ depends_on: None | list[str] = None +@temporary_stub_column("is_system", sa.Boolean()) def upgrade(container: AsyncContainer) -> None: # noqa: ARG001 """Upgrade.""" bind = op.get_bind() @@ -30,19 +34,15 @@ def upgrade(container: AsyncContainer) -> None: # noqa: ARG001 try: ro_dir_query = ( select(Directory) - .options( - selectinload(qa(Directory.parent)), - ) - .where( - qa(Directory.name) == "readonly domain controllers", - ) - ) # fmt: skip + .options(selectinload(qa(Directory.parent))) + .where(qa(Directory.name) == "readonly domain controllers") + ) ro_dir = session.scalar(ro_dir_query) if not ro_dir: return - ro_dir.name = "read-only" + ro_dir.name = READ_ONLY_GROUP_NAME ro_dir.create_path(ro_dir.parent, ro_dir.get_dn_prefix()) @@ -73,6 +73,7 @@ def upgrade(container: AsyncContainer) -> None: # noqa: ARG001 session.close() +@temporary_stub_column("is_system", sa.Boolean()) def downgrade(container: AsyncContainer) -> None: # noqa: ARG001 """Downgrade.""" bind = op.get_bind() @@ -81,13 +82,9 @@ def downgrade(container: AsyncContainer) -> None: # noqa: ARG001 try: ro_dir_query = ( select(Directory) - .options( - selectinload(qa(Directory.parent)), - ) - .where( - qa(Directory.name) == "read-only", - ) - ) # fmt: skip + .options(selectinload(qa(Directory.parent))) + .where(qa(Directory.name) == READ_ONLY_GROUP_NAME) + ) ro_dir = session.scalar(ro_dir_query) if not ro_dir: @@ -102,7 +99,7 @@ def downgrade(container: AsyncContainer) -> None: # noqa: ARG001 .filter_by( name="sAMAccountName", directory=ro_dir, - value="read-only", + value=READ_ONLY_GROUP_NAME, ) .values({"value": ro_dir.name}), ) @@ -112,7 +109,7 @@ def downgrade(container: AsyncContainer) -> None: # noqa: ARG001 .filter_by( name="cn", directory=ro_dir, - value="read-only", + value=READ_ONLY_GROUP_NAME, ) .values({"value": ro_dir.name}), ) diff --git a/app/alembic/versions/275222846605_initial_ldap_schema.py b/app/alembic/versions/275222846605_initial_ldap_schema.py index 27efc5e94..226c9270b 100644 --- a/app/alembic/versions/275222846605_initial_ldap_schema.py +++ b/app/alembic/versions/275222846605_initial_ldap_schema.py @@ -17,7 +17,7 @@ from sqlalchemy.orm import Session, selectinload from entities import Attribute, AttributeType, ObjectClass -from extra.alembic_utils import temporary_stub_entity_type_name +from extra.alembic_utils import temporary_stub_column from ldap_protocol.ldap_schema.attribute_type_dao import AttributeTypeDAO from ldap_protocol.ldap_schema.dto import AttributeTypeDTO from ldap_protocol.utils.raw_definition_parser import ( @@ -35,7 +35,7 @@ ad_2012_r2_schema_json = json.loads(ad_2012_r2_schema) -@temporary_stub_entity_type_name +@temporary_stub_column("entity_type_id", sa.Integer()) def upgrade(container: AsyncContainer) -> None: """Upgrade.""" bind = op.get_bind() diff --git a/app/alembic/versions/4442d1d982a4_remove_krb_policy.py b/app/alembic/versions/4442d1d982a4_remove_krb_policy.py index 919b0e104..5673da6a8 100644 --- a/app/alembic/versions/4442d1d982a4_remove_krb_policy.py +++ b/app/alembic/versions/4442d1d982a4_remove_krb_policy.py @@ -6,13 +6,14 @@ """ +import sqlalchemy as sa from alembic import op from dishka import AsyncContainer from sqlalchemy import delete from sqlalchemy.orm import Session from entities import Attribute, Directory -from extra.alembic_utils import temporary_stub_entity_type_name +from extra.alembic_utils import temporary_stub_column # revision identifiers, used by Alembic. revision = "4442d1d982a4" @@ -21,7 +22,7 @@ depends_on: None | str = None -@temporary_stub_entity_type_name +@temporary_stub_column("entity_type_id", sa.Integer()) def upgrade(container: AsyncContainer) -> None: # noqa: ARG001 """Upgrade.""" bind = op.get_bind() diff --git a/app/alembic/versions/6303f5c706ec_update_krbadmin_useraccountcontrol_.py b/app/alembic/versions/6303f5c706ec_update_krbadmin_useraccountcontrol_.py index ec927c733..6d101e95a 100644 --- a/app/alembic/versions/6303f5c706ec_update_krbadmin_useraccountcontrol_.py +++ b/app/alembic/versions/6303f5c706ec_update_krbadmin_useraccountcontrol_.py @@ -6,6 +6,7 @@ """ +import sqlalchemy as sa from alembic import op from dishka import AsyncContainer, Scope from sqlalchemy import select, update @@ -13,6 +14,7 @@ from sqlalchemy.orm import joinedload from entities import Attribute, Directory +from extra.alembic_utils import temporary_stub_column from ldap_protocol.objects import UserAccountControlFlag from ldap_protocol.utils.helpers import create_integer_hash from repo.pg.tables import queryable_attr as qa @@ -24,6 +26,7 @@ depends_on: None | list[str] = None +@temporary_stub_column("is_system", sa.Boolean()) def upgrade(container: AsyncContainer) -> None: """Upgrade.""" @@ -90,6 +93,7 @@ async def _change_uid_admin(connection: AsyncConnection) -> None: # noqa: ARG00 op.run_async(_change_uid_admin) +@temporary_stub_column("is_system", sa.Boolean()) def downgrade(container: AsyncContainer) -> None: """Downgrade.""" diff --git a/app/alembic/versions/6c858cc05da7_add_default_admin_name.py b/app/alembic/versions/6c858cc05da7_add_default_admin_name.py index 64cbd6a88..4d608e134 100644 --- a/app/alembic/versions/6c858cc05da7_add_default_admin_name.py +++ b/app/alembic/versions/6c858cc05da7_add_default_admin_name.py @@ -12,6 +12,7 @@ from sqlalchemy.orm import Session from entities import Attribute, User +from extra.alembic_utils import temporary_stub_column from repo.pg.tables import queryable_attr as qa # revision identifiers, used by Alembic. @@ -21,6 +22,7 @@ depends_on: None | list[str] = None +@temporary_stub_column("is_system", sa.Boolean()) def upgrade(container: AsyncContainer) -> None: # noqa: ARG001 """Upgrade.""" bind = op.get_bind() diff --git a/app/alembic/versions/6f8fe2548893_fix_read_only.py b/app/alembic/versions/6f8fe2548893_fix_read_only.py index 1717c0124..8d0f87874 100644 --- a/app/alembic/versions/6f8fe2548893_fix_read_only.py +++ b/app/alembic/versions/6f8fe2548893_fix_read_only.py @@ -6,13 +6,15 @@ """ +import sqlalchemy as sa from alembic import op from dishka import AsyncContainer from sqlalchemy import delete, select, update from sqlalchemy.orm import Session +from constants import DOMAIN_USERS_GROUP_NAME from entities import Attribute, Directory -from extra.alembic_utils import temporary_stub_entity_type_name +from extra.alembic_utils import temporary_stub_column from ldap_protocol.utils.helpers import create_integer_hash # revision identifiers, used by Alembic. @@ -22,7 +24,8 @@ depends_on: None = None -@temporary_stub_entity_type_name +@temporary_stub_column("entity_type_id", sa.Integer()) +@temporary_stub_column("is_system", sa.Boolean()) def upgrade(container: AsyncContainer) -> None: # noqa: ARG001 """Upgrade.""" bind = op.get_bind() @@ -43,7 +46,7 @@ def upgrade(container: AsyncContainer) -> None: # noqa: ARG001 .filter_by( name="sAMAccountName", directory=ro_dir, - value="domain users", + value=DOMAIN_USERS_GROUP_NAME, ) .values({"value": ro_dir.name}), ) diff --git a/app/alembic/versions/71e642808369_add_directory_is_system.py b/app/alembic/versions/71e642808369_add_directory_is_system.py new file mode 100644 index 000000000..2526190e4 --- /dev/null +++ b/app/alembic/versions/71e642808369_add_directory_is_system.py @@ -0,0 +1,104 @@ +"""Add directory is_system column. + +Revision ID: 71e642808369 +Revises: a99f866a7e3a +Create Date: 2026-01-15 09:08:12.866533 + +""" + +import sqlalchemy as sa +from alembic import op +from dishka import AsyncContainer, Scope +from sqlalchemy import update +from sqlalchemy.ext.asyncio import AsyncConnection, AsyncSession +from sqlalchemy.orm import Session + +from constants import ( + COMPUTERS_CONTAINER_NAME, + DOMAIN_ADMIN_GROUP_NAME, + DOMAIN_COMPUTERS_GROUP_NAME, + DOMAIN_USERS_GROUP_NAME, + GROUPS_CONTAINER_NAME, + READ_ONLY_GROUP_NAME, + USERS_CONTAINER_NAME, +) +from entities import Directory +from ldap_protocol.utils.queries import get_base_directories +from repo.pg.tables import queryable_attr as qa + +# revision identifiers, used by Alembic. +revision: None | str = "71e642808369" +down_revision: None | str = "a99f866a7e3a" +branch_labels: None | list[str] = None +depends_on: None | list[str] = None + + +def upgrade(container: AsyncContainer) -> None: + """Upgrade.""" + bind = op.get_bind() + session = Session(bind=bind) + + op.add_column( + "Directory", + sa.Column("is_system", sa.Boolean(), nullable=True), + ) + # NOTE: If instances of Directories exists, set default value + session.execute(update(Directory).values({"is_system": False})) + op.alter_column("Directory", "is_system", nullable=False) + + async def _indicate_system_directories( + connection: AsyncConnection, # noqa: ARG001 + ) -> None: + async with container(scope=Scope.REQUEST) as cnt: + session = await cnt.get(AsyncSession) + + base_dn_list = await get_base_directories(session) + if not base_dn_list: + return + + for base_dn in base_dn_list: + base_dn.is_system = True + + await session.flush() + + await session.execute( + update(Directory) + .where( + qa(Directory.is_system).is_(False), + qa(Directory.name).in_( + ( + GROUPS_CONTAINER_NAME, + DOMAIN_ADMIN_GROUP_NAME, + DOMAIN_USERS_GROUP_NAME, + READ_ONLY_GROUP_NAME, + DOMAIN_COMPUTERS_GROUP_NAME, + COMPUTERS_CONTAINER_NAME, + USERS_CONTAINER_NAME, + "services", + "krbadmin", + "kerberos", + ), + ), + ) + .values(is_system=True), + ) + await session.flush() + + # NOTE: It's required to mark only administrator users as system. + # Because only main administrator has object_class=='user'. + await session.execute( + update(Directory) + .where( + qa(Directory.is_system).is_(False), + qa(Directory.object_class) == "user", + ) + .values(is_system=True), + ) + await session.flush() + + op.run_async(_indicate_system_directories) + + +def downgrade(container: AsyncContainer) -> None: # noqa: ARG001 + """Downgrade.""" + op.drop_column("Directory", "is_system") diff --git a/app/alembic/versions/8164b4a9e1f1_add_ou_computers.py b/app/alembic/versions/8164b4a9e1f1_add_ou_computers.py index 344e9797a..5f8608a4a 100644 --- a/app/alembic/versions/8164b4a9e1f1_add_ou_computers.py +++ b/app/alembic/versions/8164b4a9e1f1_add_ou_computers.py @@ -6,12 +6,15 @@ """ +import sqlalchemy as sa from alembic import op from dishka import AsyncContainer, Scope from sqlalchemy import delete, exists, select from sqlalchemy.ext.asyncio import AsyncConnection, AsyncSession +from constants import COMPUTERS_CONTAINER_NAME from entities import Directory +from extra.alembic_utils import temporary_stub_column from ldap_protocol.roles.role_use_case import RoleUseCase from ldap_protocol.utils.queries import get_base_directories from repo.pg.tables import queryable_attr as qa @@ -24,13 +27,14 @@ _OU_COMPUTERS_DATA = { - "name": "computers", + "name": COMPUTERS_CONTAINER_NAME, "object_class": "organizationalUnit", "attributes": {"objectClass": ["top", "container"]}, "children": [], } +@temporary_stub_column("is_system", sa.Boolean()) def upgrade(container: AsyncContainer) -> None: """Upgrade.""" from ldap_protocol.auth.setup_gateway import SetupGateway @@ -49,7 +53,7 @@ async def _create_ou_computers(connection: AsyncConnection) -> None: # noqa: AR exists_ou_computers = await session.scalar( select( exists(Directory) - .where(qa(Directory.name) == "computers"), + .where(qa(Directory.name) == COMPUTERS_CONTAINER_NAME), ), ) # fmt: skip if exists_ou_computers: @@ -57,13 +61,14 @@ async def _create_ou_computers(connection: AsyncConnection) -> None: # noqa: AR await setup_gateway.create_dir( _OU_COMPUTERS_DATA, - domain_dir, - domain_dir, + is_system=True, + domain=domain_dir, + parent=domain_dir, ) ou_computers_dir = await session.scalar( select(Directory) - .where(qa(Directory.name) == "computers"), + .where(qa(Directory.name) == COMPUTERS_CONTAINER_NAME), ) # fmt: skip if not ou_computers_dir: raise Exception("Directory 'ou=computers' not found.") @@ -78,6 +83,7 @@ async def _create_ou_computers(connection: AsyncConnection) -> None: # noqa: AR op.run_async(_create_ou_computers) +@temporary_stub_column("is_system", sa.Boolean()) def downgrade(container: AsyncContainer) -> None: """Downgrade.""" @@ -91,7 +97,7 @@ async def _delete_ou_computers(connection: AsyncConnection) -> None: # noqa: AR await session.execute( delete(Directory) - .where(qa(Directory.name) == "computers"), + .where(qa(Directory.name) == COMPUTERS_CONTAINER_NAME), ) # fmt: skip await session.commit() diff --git a/app/alembic/versions/ba78cef9700a_initial_entity_type.py b/app/alembic/versions/ba78cef9700a_initial_entity_type.py index 3e9852b9c..0e6744919 100644 --- a/app/alembic/versions/ba78cef9700a_initial_entity_type.py +++ b/app/alembic/versions/ba78cef9700a_initial_entity_type.py @@ -15,7 +15,7 @@ from constants import ENTITY_TYPE_DATAS from entities import Attribute, Directory, User -from extra.alembic_utils import temporary_stub_entity_type_name +from extra.alembic_utils import temporary_stub_column from ldap_protocol.ldap_schema.dto import EntityTypeDTO from ldap_protocol.ldap_schema.entity_type_dao import EntityTypeDAO from ldap_protocol.ldap_schema.entity_type_use_case import EntityTypeUseCase @@ -29,7 +29,8 @@ depends_on: None | str = None -@temporary_stub_entity_type_name +@temporary_stub_column("entity_type_id", sa.Integer()) +@temporary_stub_column("is_system", sa.Boolean()) def upgrade(container: AsyncContainer) -> None: """Upgrade database schema and data, creating Entity Types.""" op.create_table( diff --git a/app/alembic/versions/bf435bbd95ff_add_rdn_attr_name.py b/app/alembic/versions/bf435bbd95ff_add_rdn_attr_name.py index 3ba82ea7d..88eaf4581 100644 --- a/app/alembic/versions/bf435bbd95ff_add_rdn_attr_name.py +++ b/app/alembic/versions/bf435bbd95ff_add_rdn_attr_name.py @@ -12,7 +12,7 @@ from sqlalchemy.orm import Session from entities import Attribute, Directory -from extra.alembic_utils import temporary_stub_entity_type_name +from extra.alembic_utils import temporary_stub_column from repo.pg.tables import queryable_attr as qa # revision identifiers, used by Alembic. @@ -22,7 +22,8 @@ depends_on: None | str = None -@temporary_stub_entity_type_name +@temporary_stub_column("entity_type_id", sa.Integer()) +@temporary_stub_column("is_system", sa.Boolean()) def upgrade(container: AsyncContainer) -> None: # noqa: ARG001 """Upgrade.""" op.add_column("Directory", sa.Column("rdname", sa.String(length=64))) @@ -57,7 +58,8 @@ def upgrade(container: AsyncContainer) -> None: # noqa: ARG001 op.alter_column("Directory", "rdname", nullable=False) -@temporary_stub_entity_type_name +@temporary_stub_column("entity_type_id", sa.Integer()) +@temporary_stub_column("is_system", sa.Boolean()) def downgrade(container: AsyncContainer) -> None: # noqa: ARG001 """Downgrade.""" bind = op.get_bind() diff --git a/app/alembic/versions/bv546ccd35fa_fix_krbadmin_attrs.py b/app/alembic/versions/bv546ccd35fa_fix_krbadmin_attrs.py index 53979d701..dfaa36aa0 100644 --- a/app/alembic/versions/bv546ccd35fa_fix_krbadmin_attrs.py +++ b/app/alembic/versions/bv546ccd35fa_fix_krbadmin_attrs.py @@ -12,7 +12,7 @@ from sqlalchemy.orm import Session from entities import Attribute, Directory -from extra.alembic_utils import temporary_stub_entity_type_name +from extra.alembic_utils import temporary_stub_column from repo.pg.tables import queryable_attr as qa # revision identifiers, used by Alembic. @@ -22,7 +22,8 @@ depends_on: None | str = None -@temporary_stub_entity_type_name +@temporary_stub_column("entity_type_id", sa.Integer()) +@temporary_stub_column("is_system", sa.Boolean()) def upgrade(container: AsyncContainer) -> None: # noqa: ARG001 """Upgrade.""" bind = op.get_bind() diff --git a/app/alembic/versions/c4888c68e221_fix_admin_attr_and_policy.py b/app/alembic/versions/c4888c68e221_fix_admin_attr_and_policy.py index 3bc7020b8..dbaa321be 100644 --- a/app/alembic/versions/c4888c68e221_fix_admin_attr_and_policy.py +++ b/app/alembic/versions/c4888c68e221_fix_admin_attr_and_policy.py @@ -13,6 +13,7 @@ from sqlalchemy.orm import joinedload from entities import Attribute, Directory, NetworkPolicy +from extra.alembic_utils import temporary_stub_column from ldap_protocol.ldap_schema.entity_type_dao import EntityTypeDAO from ldap_protocol.utils.helpers import create_integer_hash from ldap_protocol.utils.queries import get_base_directories @@ -25,6 +26,7 @@ depends_on: None | list[str] = None +@temporary_stub_column("is_system", sa.Boolean()) def upgrade(container: AsyncContainer) -> None: """Upgrade.""" diff --git a/app/alembic/versions/dafg3a4b22ab_add_preauth_princ.py b/app/alembic/versions/dafg3a4b22ab_add_preauth_princ.py index e7b426043..38d982694 100644 --- a/app/alembic/versions/dafg3a4b22ab_add_preauth_princ.py +++ b/app/alembic/versions/dafg3a4b22ab_add_preauth_princ.py @@ -12,7 +12,7 @@ from sqlalchemy.orm import Session from entities import Attribute, CatalogueSetting, User -from extra.alembic_utils import temporary_stub_entity_type_name +from extra.alembic_utils import temporary_stub_column from ldap_protocol.kerberos import KERBEROS_STATE_NAME from repo.pg.tables import queryable_attr as qa @@ -23,7 +23,8 @@ depends_on: None | str = None -@temporary_stub_entity_type_name +@temporary_stub_column("entity_type_id", sa.Integer()) +@temporary_stub_column("is_system", sa.Boolean()) def upgrade(container: AsyncContainer) -> None: # noqa: ARG001 """Upgrade.""" bind = op.get_bind() diff --git a/app/alembic/versions/e4d6d99d32bd_add_audit_policies.py b/app/alembic/versions/e4d6d99d32bd_add_audit_policies.py index d2530bf09..0a64e7fb7 100644 --- a/app/alembic/versions/e4d6d99d32bd_add_audit_policies.py +++ b/app/alembic/versions/e4d6d99d32bd_add_audit_policies.py @@ -14,6 +14,7 @@ from sqlalchemy.dialects import postgresql from sqlalchemy.ext.asyncio import AsyncConnection, AsyncSession +from extra.alembic_utils import temporary_stub_column from ldap_protocol.policies.audit.audit_use_case import AuditUseCase from ldap_protocol.policies.audit.destination_dao import AuditDestinationDAO from ldap_protocol.policies.audit.events.managers import RawAuditManager @@ -27,6 +28,7 @@ depends_on: None | str = None +@temporary_stub_column("is_system", sa.Boolean()) def upgrade(container: AsyncContainer) -> None: """Upgrade.""" diff --git a/app/alembic/versions/f1abf7ef2443_add_container_object_class.py b/app/alembic/versions/f1abf7ef2443_add_container_object_class.py index 99b2a4774..cf1c80e1b 100644 --- a/app/alembic/versions/f1abf7ef2443_add_container_object_class.py +++ b/app/alembic/versions/f1abf7ef2443_add_container_object_class.py @@ -6,6 +6,7 @@ """ +import sqlalchemy as sa from alembic import op from dishka import AsyncContainer, Scope from sqlalchemy import delete, func, insert, select, update @@ -13,6 +14,7 @@ from entities import Attribute, Directory, EntityType from enums import EntityTypeNames +from extra.alembic_utils import temporary_stub_column from repo.pg.tables import queryable_attr as qa # revision identifiers, used by Alembic. @@ -22,6 +24,7 @@ depends_on: None | str = None +@temporary_stub_column("is_system", sa.Boolean()) def upgrade(container: AsyncContainer) -> None: """Upgrade.""" @@ -107,6 +110,7 @@ async def _migrate_ou_to_cn_containers( op.run_async(_migrate_ou_to_cn_containers) +@temporary_stub_column("is_system", sa.Boolean()) def downgrade(container: AsyncContainer) -> None: """Downgrade.""" diff --git a/app/alembic/versions/fafc3d0b11ec_.py b/app/alembic/versions/fafc3d0b11ec_.py index b6df06c4f..1ab09b6b3 100644 --- a/app/alembic/versions/fafc3d0b11ec_.py +++ b/app/alembic/versions/fafc3d0b11ec_.py @@ -6,6 +6,7 @@ """ +import sqlalchemy as sa from alembic import op from dishka import AsyncContainer, Scope from sqlalchemy import delete, exists, select @@ -13,7 +14,7 @@ from sqlalchemy.ext.asyncio import AsyncConnection, AsyncSession from entities import Directory -from extra.alembic_utils import temporary_stub_entity_type_name +from extra.alembic_utils import temporary_stub_column from ldap_protocol.ldap_schema.attribute_value_validator import ( AttributeValueValidator, ) @@ -31,7 +32,8 @@ depends_on: None | str = None -@temporary_stub_entity_type_name +@temporary_stub_column("entity_type_id", sa.Integer()) +@temporary_stub_column("is_system", sa.Boolean()) def upgrade(container: AsyncContainer) -> None: """Upgrade.""" @@ -74,7 +76,8 @@ async def _create_readonly_grp_and_plcy( op.run_async(_create_readonly_grp_and_plcy) -@temporary_stub_entity_type_name +@temporary_stub_column("entity_type_id", sa.Integer()) +@temporary_stub_column("is_system", sa.Boolean()) def downgrade(container: AsyncContainer) -> None: """Downgrade.""" diff --git a/app/constants.py b/app/constants.py index 077d35ec1..fe4cdf6da 100644 --- a/app/constants.py +++ b/app/constants.py @@ -8,6 +8,17 @@ from enums import EntityTypeNames +GROUPS_CONTAINER_NAME = "groups" +COMPUTERS_CONTAINER_NAME = "computers" +USERS_CONTAINER_NAME = "users" + +READ_ONLY_GROUP_NAME = "read-only" + +DOMAIN_ADMIN_GROUP_NAME = "domain admins" +DOMAIN_USERS_GROUP_NAME = "domain users" +DOMAIN_COMPUTERS_GROUP_NAME = "domain computers" + + group_attrs = { "objectClass": ["top"], "groupType": ["-2147483646"], @@ -121,7 +132,7 @@ }, }, { - "name": "users", + "name": USERS_CONTAINER_NAME, "object_class": "container", "attributes": {"objectClass": ["top"]}, "children": [ @@ -272,7 +283,7 @@ class EntityTypeData(TypedDict): FIRST_SETUP_DATA = [ { - "name": "groups", + "name": GROUPS_CONTAINER_NAME, "object_class": "container", "attributes": { "objectClass": ["top"], @@ -280,52 +291,52 @@ class EntityTypeData(TypedDict): }, "children": [ { - "name": "domain admins", + "name": DOMAIN_ADMIN_GROUP_NAME, "object_class": "group", "attributes": { "objectClass": ["top", "posixGroup"], "groupType": ["-2147483646"], "instanceType": ["4"], - "sAMAccountName": ["domain admins"], + "sAMAccountName": [DOMAIN_ADMIN_GROUP_NAME], "sAMAccountType": ["268435456"], "gidNumber": ["512"], }, "objectSid": 512, }, { - "name": "domain users", + "name": DOMAIN_USERS_GROUP_NAME, "object_class": "group", "attributes": { "objectClass": ["top", "posixGroup"], "groupType": ["-2147483646"], "instanceType": ["4"], - "sAMAccountName": ["domain users"], + "sAMAccountName": [DOMAIN_USERS_GROUP_NAME], "sAMAccountType": ["268435456"], "gidNumber": ["513"], }, "objectSid": 513, }, { - "name": "read-only", + "name": READ_ONLY_GROUP_NAME, "object_class": "group", "attributes": { "objectClass": ["top", "posixGroup"], "groupType": ["-2147483646"], "instanceType": ["4"], - "sAMAccountName": ["read-only"], + "sAMAccountName": [READ_ONLY_GROUP_NAME], "sAMAccountType": ["268435456"], "gidNumber": ["521"], }, "objectSid": 521, }, { - "name": "domain computers", + "name": DOMAIN_COMPUTERS_GROUP_NAME, "object_class": "group", "attributes": { "objectClass": ["top", "posixGroup"], "groupType": ["-2147483646"], "instanceType": ["4"], - "sAMAccountName": ["domain computers"], + "sAMAccountName": [DOMAIN_COMPUTERS_GROUP_NAME], "sAMAccountType": ["268435456"], "gidNumber": ["515"], }, @@ -334,12 +345,13 @@ class EntityTypeData(TypedDict): ], }, { - "name": "computers", + "name": COMPUTERS_CONTAINER_NAME, "object_class": "container", "attributes": {"objectClass": ["top"]}, "children": [], }, ] + DEFAULT_DC_POSTFIX = "DC1" UNC_PREFIX = "\\\\" diff --git a/app/entities.py b/app/entities.py index 417c18c93..53f5c95e9 100644 --- a/app/entities.py +++ b/app/entities.py @@ -190,6 +190,7 @@ class Directory: id: int = field(init=False) name: str + is_system: bool = field(default=False) object_sid: str = field(default="") object_guid: uuid.UUID = field(default_factory=uuid.uuid4) parent_id: int | None = None diff --git a/app/extra/alembic_utils.py b/app/extra/alembic_utils.py index 2afc558ab..ac8cfffd8 100644 --- a/app/extra/alembic_utils.py +++ b/app/extra/alembic_utils.py @@ -1,37 +1,39 @@ """Alembic utils.""" -from typing import Callable +from typing import Any, Callable import sqlalchemy as sa from alembic import op -def temporary_stub_entity_type_name(func: Callable) -> Callable: - """Add and drop the 'entity_type_name' column in the 'Directory' table. +def temporary_stub_column(column_name: str, type_: Any) -> Callable: + """Add and drop a temporary column in the 'Directory' table. State of the database at the time of migration - doesn't contain 'entity_type_name' column in the 'Directory' table, + doesn't contain the specified column in the 'Directory' table, but 'Directory' model has the column. - Before starting the migration, add 'entity_type_name' column. - Then migration completed, delete 'entity_type_name' column. + Before starting the migration, add the specified column. + Then migration completed, delete the column. Don`t like excluding columns with Deferred(), because you will need to refactor SQL queries - that precede the 'ba78cef9700a_initial_entity_type.py' migration - and include working with the Directory. + that precede migrations and include working with the Directory. - :param Callable func: any function - :return Callable: any function + :param str column_name: column name to temporarily add + :return Callable: decorator function """ - def wrapper(*args: tuple, **kwargs: dict) -> None: - op.add_column( - "Directory", - sa.Column("entity_type_id", sa.Integer(), nullable=True), - ) - func(*args, **kwargs) - op.drop_column("Directory", "entity_type_id") - return None + def decorator(func: Callable) -> Callable: + def wrapper(*args: tuple, **kwargs: dict) -> None: + op.add_column( + "Directory", + sa.Column(column_name, type_, nullable=True), + ) + func(*args, **kwargs) + op.drop_column("Directory", column_name) + return None - return wrapper + return wrapper + + return decorator diff --git a/app/ldap_protocol/auth/setup_gateway.py b/app/ldap_protocol/auth/setup_gateway.py index d57b6a95f..b5bfe580a 100644 --- a/app/ldap_protocol/auth/setup_gateway.py +++ b/app/ldap_protocol/auth/setup_gateway.py @@ -59,6 +59,7 @@ async def setup_enviroment( self, *, data: list, + is_system: bool, dn: str = "multifactor.dev", ) -> None: """Create directories and users for enviroment.""" @@ -68,6 +69,7 @@ async def setup_enviroment( return domain = Directory(name=dn, object_class="domain") + domain.is_system = True domain.object_sid = generate_domain_sid() domain.path = [f"dc={path}" for path in reversed(dn.split("."))] domain.depth = len(domain.path) @@ -107,8 +109,9 @@ async def setup_enviroment( for unit in data: await self.create_dir( unit, - domain, - domain, + is_system=is_system, + domain=domain, + parent=domain, ) except Exception: @@ -120,11 +123,13 @@ async def setup_enviroment( async def create_dir( self, data: dict, + is_system: bool, domain: Directory, parent: Directory | None = None, ) -> None: """Create data recursively.""" dir_ = Directory( + is_system=is_system, object_class=data["object_class"], name=data["name"], parent=parent, @@ -221,8 +226,9 @@ async def create_dir( for n_data in data["children"]: await self.create_dir( n_data, - domain, - dir_, + is_system=is_system, + domain=domain, + parent=dir_, ) async def _get_group(self, name: str) -> Group: diff --git a/app/ldap_protocol/auth/use_cases.py b/app/ldap_protocol/auth/use_cases.py index 7d0df286e..b9a53414e 100644 --- a/app/ldap_protocol/auth/use_cases.py +++ b/app/ldap_protocol/auth/use_cases.py @@ -9,7 +9,11 @@ from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession -from constants import FIRST_SETUP_DATA +from constants import ( + DOMAIN_ADMIN_GROUP_NAME, + FIRST_SETUP_DATA, + USERS_CONTAINER_NAME, +) from ldap_protocol.auth.dto import SetupDTO from ldap_protocol.auth.setup_gateway import SetupGateway from ldap_protocol.identity.exceptions import ( @@ -79,7 +83,7 @@ def _create_user_data(self, dto: SetupDTO) -> dict: :return: dict with user data """ return { - "name": "users", + "name": USERS_CONTAINER_NAME, "object_class": "container", "attributes": {"objectClass": ["top"]}, "children": [ @@ -92,7 +96,7 @@ def _create_user_data(self, dto: SetupDTO) -> dict: "mail": dto.mail, "display_name": dto.display_name, "password": dto.password, - "groups": ["domain admins"], + "groups": [DOMAIN_ADMIN_GROUP_NAME], }, "attributes": { "objectClass": [ @@ -127,6 +131,7 @@ async def _create(self, dto: SetupDTO, data: list) -> None: await self._setup_gateway.setup_enviroment( data=data, dn=dto.domain, + is_system=True, ) await self._password_use_cases.create_default_domain_policy() diff --git a/app/ldap_protocol/kerberos/service.py b/app/ldap_protocol/kerberos/service.py index aea808d22..21ec03c84 100644 --- a/app/ldap_protocol/kerberos/service.py +++ b/app/ldap_protocol/kerberos/service.py @@ -172,10 +172,12 @@ def _build_add_requests( "description": ["Kerberos administrator's group."], "gidNumber": ["800"], }, + is_system=True, ) services = AddRequest.from_dict( dns.services_container_dn, {"objectClass": ["organizationalUnit", "top", "container"]}, + is_system=True, ) krb_user = AddRequest.from_dict( dns.krbadmin_dn, @@ -209,6 +211,7 @@ def _build_add_requests( ), ], }, + is_system=True, ) return AddRequests( group=group, diff --git a/app/ldap_protocol/ldap_requests/add.py b/app/ldap_protocol/ldap_requests/add.py index aa5d3b3ee..6f29fe9af 100644 --- a/app/ldap_protocol/ldap_requests/add.py +++ b/app/ldap_protocol/ldap_requests/add.py @@ -10,6 +10,7 @@ from sqlalchemy import select from sqlalchemy.exc import IntegrityError +from constants import DOMAIN_COMPUTERS_GROUP_NAME, DOMAIN_USERS_GROUP_NAME from entities import Attribute, Directory, Group, User from enums import AceType, EntityTypeNames from ldap_protocol.asn1parser import ASN1Row @@ -24,10 +25,6 @@ ProtocolRequests, UserAccountControlFlag, ) -from ldap_protocol.utils.const import ( - DOMAIN_COMPUTERS_GROUP_NAME, - DOMAIN_USERS_GROUP_NAME, -) from ldap_protocol.utils.helpers import ( create_integer_hash, create_user_name, @@ -68,6 +65,11 @@ class AddRequest(BaseRequest): CONTEXT_TYPE: ClassVar[type] = LDAPAddRequestContext entry: str = Field(..., description="Any `DistinguishedName`") + is_system: bool = Field( + False, + description="Mark as system directory (cannot be modified)", + ) + attributes: list[PartialAttribute] password: SecretStr | None = Field(None, examples=["password"]) @@ -205,6 +207,7 @@ async def handle( # noqa: C901 new_dir = Directory( object_class="", name=name, + is_system=self.is_system or bool(name == "kerberos"), parent=parent, ) @@ -481,6 +484,7 @@ def from_dict( entry: str, attributes: dict[str, list[str]], password: str | None = None, + is_system: bool = False, ) -> "AddRequest": """Create AddRequest from dict. @@ -490,6 +494,7 @@ def from_dict( """ return AddRequest( entry=entry, + is_system=is_system, password=password, attributes=[ PartialAttribute(type=name, vals=vals) diff --git a/app/ldap_protocol/ldap_requests/delete.py b/app/ldap_protocol/ldap_requests/delete.py index 334df621a..e2b127331 100644 --- a/app/ldap_protocol/ldap_requests/delete.py +++ b/app/ldap_protocol/ldap_requests/delete.py @@ -98,6 +98,12 @@ async def handle( # noqa: C901 yield DeleteResponse(result_code=LDAPCodes.NO_SUCH_OBJECT) return + if directory.is_system: + yield DeleteResponse( + result_code=LDAPCodes.UNWILLING_TO_PERFORM, + ) + return + self.set_event_data( {"before_attrs": self.get_directory_attrs(directory)}, ) diff --git a/app/ldap_protocol/ldap_requests/modify.py b/app/ldap_protocol/ldap_requests/modify.py index 8ba60b963..8b192671f 100644 --- a/app/ldap_protocol/ldap_requests/modify.py +++ b/app/ldap_protocol/ldap_requests/modify.py @@ -14,6 +14,7 @@ from sqlalchemy.orm import joinedload, selectinload from config import Settings +from constants import DOMAIN_ADMIN_GROUP_NAME from entities import Attribute, Directory, Group, User from enums import AceType, EntityTypeNames from ldap_protocol.asn1parser import ASN1Row @@ -80,8 +81,6 @@ class ModifyForbiddenError(Exception): KRBAPIForcePasswordChangeError, ) -_DOMAIN_ADMIN_NAME = "domain admins" - class ModifyRequest(BaseRequest): """Modify request. @@ -150,7 +149,7 @@ async def _update_password_expiration( now = datetime.now(timezone.utc) + timedelta(days=max_age_days) change.modification.vals[0] = now.strftime("%Y%m%d%H%M%SZ") - async def handle( + async def handle( # noqa: C901 self, ctx: LDAPModifyRequestContext, ) -> AsyncGenerator[ModifyResponse, None]: @@ -185,6 +184,12 @@ async def handle( yield ModifyResponse(result_code=LDAPCodes.NO_SUCH_OBJECT) return + if directory.is_system: + yield ModifyResponse( + result_code=LDAPCodes.UNWILLING_TO_PERFORM, + ) + return + can_modify = ctx.access_manager.check_modify_access( changes=self.changes, aces=directory.access_control_entries, @@ -448,7 +453,7 @@ async def _can_delete_group_from_directory( if operation == Operation.REPLACE: for group in directory.groups: if ( - group.directory.name == _DOMAIN_ADMIN_NAME + group.directory.name == DOMAIN_ADMIN_GROUP_NAME and directory.path_dn == user.dn and group not in groups ): @@ -459,7 +464,7 @@ async def _can_delete_group_from_directory( elif operation == Operation.DELETE: for group in groups: if ( - group.directory.name == _DOMAIN_ADMIN_NAME + group.directory.name == DOMAIN_ADMIN_GROUP_NAME and directory.path_dn == user.dn ): raise ModifyForbiddenError( @@ -493,7 +498,7 @@ async def _can_delete_member_from_directory( operation == Operation.DELETE and user.dn in modified_members_dns ) - if directory.name == _DOMAIN_ADMIN_NAME and ( + if directory.name == DOMAIN_ADMIN_GROUP_NAME and ( is_user_in_deleted or is_user_not_in_replaced ): raise ModifyForbiddenError("Can't delete yourself from group.") diff --git a/app/ldap_protocol/ldap_requests/modify_dn.py b/app/ldap_protocol/ldap_requests/modify_dn.py index 0cf547833..7c315eadd 100644 --- a/app/ldap_protocol/ldap_requests/modify_dn.py +++ b/app/ldap_protocol/ldap_requests/modify_dn.py @@ -136,6 +136,12 @@ async def handle( yield ModifyDNResponse(result_code=LDAPCodes.UNWILLING_TO_PERFORM) return + if directory.is_system: + yield ModifyDNResponse( + result_code=LDAPCodes.UNWILLING_TO_PERFORM, + ) + return + old_name = directory.name new_dn, new_name = self.newrdn.split("=") directory.name = new_name diff --git a/app/ldap_protocol/utils/const.py b/app/ldap_protocol/utils/const.py index e2afe48f7..c8a44a03f 100644 --- a/app/ldap_protocol/utils/const.py +++ b/app/ldap_protocol/utils/const.py @@ -31,5 +31,3 @@ def _type_validate_email(email: str) -> str: GRANT_DN_STRING = Annotated[str, AfterValidator(_type_validate_entry)] EmailStr = Annotated[str, AfterValidator(_type_validate_email)] -DOMAIN_USERS_GROUP_NAME = "domain users" -DOMAIN_COMPUTERS_GROUP_NAME = "domain computers" diff --git a/app/repo/pg/tables.py b/app/repo/pg/tables.py index f59d1e758..7b463a597 100644 --- a/app/repo/pg/tables.py +++ b/app/repo/pg/tables.py @@ -112,6 +112,7 @@ def _compile_create_uc( "Directory", metadata, Column("id", Integer, primary_key=True), + Column("is_system", Boolean, nullable=False, default=False), Column( "parentId", Integer, diff --git a/interface b/interface index f31962020..97bbc08dd 160000 --- a/interface +++ b/interface @@ -1 +1 @@ -Subproject commit f31962020a6689e6a4c61fb3349db5b5c7895f92 +Subproject commit 97bbc08dda7584f579f756d8b09abe60db67b47b diff --git a/tests/conftest.py b/tests/conftest.py index 418020032..12ba86e40 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -154,9 +154,9 @@ from ldap_protocol.server import PoolClientHandler from ldap_protocol.session_storage import RedisSessionStorage, SessionStorage from ldap_protocol.session_storage.repository import SessionRepository -from ldap_protocol.utils.queries import get_user +from ldap_protocol.utils.queries import get_base_directories, get_user from password_utils import PasswordUtils -from tests.constants import TEST_DATA +from tests.constants import TEST_DATA, TEST_SYSTEM_ADMIN_DATA class TestProvider(Provider): @@ -983,7 +983,19 @@ async def setup_session( attribute_value_validator=attribute_value_validator, ) await audit_use_case.create_policies() - await setup_gateway.setup_enviroment(dn="md.test", data=TEST_DATA) + await setup_gateway.setup_enviroment( + dn="md.test", + data=TEST_DATA, + is_system=False, + ) + + domain = (await get_base_directories(session))[0] + await setup_gateway.create_dir( + data=TEST_SYSTEM_ADMIN_DATA, + is_system=True, + domain=domain, + parent=domain, + ) # NOTE: after setup environment we need base DN to be created await password_use_cases.create_default_domain_policy() diff --git a/tests/constants.py b/tests/constants.py index 548e681ec..5542e0742 100644 --- a/tests/constants.py +++ b/tests/constants.py @@ -4,11 +4,18 @@ License: https://github.com/MultiDirectoryLab/MultiDirectory/blob/main/LICENSE """ +from constants import ( + DOMAIN_ADMIN_GROUP_NAME, + DOMAIN_COMPUTERS_GROUP_NAME, + DOMAIN_USERS_GROUP_NAME, + GROUPS_CONTAINER_NAME, + USERS_CONTAINER_NAME, +) from ldap_protocol.objects import UserAccountControlFlag TEST_DATA = [ { - "name": "groups", + "name": GROUPS_CONTAINER_NAME, "object_class": "container", "attributes": { "objectClass": ["top"], @@ -16,20 +23,20 @@ }, "children": [ { - "name": "domain admins", + "name": DOMAIN_ADMIN_GROUP_NAME, "object_class": "group", "attributes": { "objectClass": ["top", "posixGroup"], "groupType": ["-2147483646"], "instanceType": ["4"], - "sAMAccountName": ["domain admins"], + "sAMAccountName": [DOMAIN_ADMIN_GROUP_NAME], "sAMAccountType": ["268435456"], }, }, { "name": "developers", "object_class": "group", - "groups": ["domain admins"], + "groups": [DOMAIN_ADMIN_GROUP_NAME], "attributes": { "objectClass": ["top", "posixGroup"], "groupType": ["-2147483646"], @@ -50,31 +57,31 @@ }, }, { - "name": "domain users", + "name": DOMAIN_USERS_GROUP_NAME, "object_class": "group", "attributes": { "objectClass": ["top", "posixGroup"], "groupType": ["-2147483646"], "instanceType": ["4"], - "sAMAccountName": ["domain users"], + "sAMAccountName": [DOMAIN_USERS_GROUP_NAME], "sAMAccountType": ["268435456"], }, }, { - "name": "domain computers", + "name": DOMAIN_COMPUTERS_GROUP_NAME, "object_class": "group", "attributes": { "objectClass": ["top", "posixGroup"], "groupType": ["-2147483646"], "instanceType": ["4"], - "sAMAccountName": ["domain computers"], + "sAMAccountName": [DOMAIN_COMPUTERS_GROUP_NAME], "sAMAccountType": ["268435456"], }, }, ], }, { - "name": "users", + "name": USERS_CONTAINER_NAME, "object_class": "container", "attributes": {"objectClass": ["top"]}, "children": [ @@ -87,9 +94,7 @@ "mail": "user0@mail.com", "display_name": "user0", "password": "password", - "groups": [ - "domain admins", - ], + "groups": [DOMAIN_ADMIN_GROUP_NAME], }, "attributes": { "givenName": ["John"], @@ -119,9 +124,7 @@ "mail": "user_admin@mail.com", "display_name": "user_admin", "password": "password", - "groups": [ - "domain admins", - ], + "groups": [DOMAIN_ADMIN_GROUP_NAME], }, "attributes": { "objectClass": [ @@ -148,9 +151,7 @@ "mail": "user_admin_for_roles@mail.com", "display_name": "user_admin_for_roles", "password": "password", - "groups": [ - "admin login only", - ], + "groups": ["admin login only"], }, "attributes": { "objectClass": [ @@ -177,7 +178,7 @@ "mail": "user_non_admin@mail.com", "display_name": "user_non_admin", "password": "password", - "groups": ["domain users"], + "groups": [DOMAIN_USERS_GROUP_NAME], }, "attributes": { "objectClass": [ @@ -407,3 +408,29 @@ ], }, ] + +TEST_SYSTEM_ADMIN_DATA = { + "name": "System Administrator", + "object_class": "user", + "organizationalPerson": { + "sam_account_name": "system_admin", + "user_principal_name": "system_admin", + "mail": "system_admin@mail.com", + "display_name": "system_admin", + "password": "password", + "groups": [DOMAIN_ADMIN_GROUP_NAME], + }, + "attributes": { + "objectClass": [ + "top", + "person", + "organizationalPerson", + "posixAccount", + "inetOrgPerson", + "shadowAccount", + ], + "posixEmail": ["abctest@mail.com"], + "attr_with_bvalue": [b"any"], + "userAccountControl": [str(UserAccountControlFlag.NORMAL_ACCOUNT)], + }, +} diff --git a/tests/search_request_datasets.py b/tests/search_request_datasets.py index 77557bae9..fa36f135c 100644 --- a/tests/search_request_datasets.py +++ b/tests/search_request_datasets.py @@ -50,6 +50,7 @@ { "filter": f"(useraccountcontrol:1.2.840.113556.1.4.803:={UserAccountControlFlag.NORMAL_ACCOUNT})", # noqa: E501 "objects": [ + "cn=System Administrator,dc=md,dc=test", "cn=user0,cn=users,dc=md,dc=test", "cn=user_admin,cn=users,dc=md,dc=test", "cn=user_admin_for_roles,cn=users,dc=md,dc=test", @@ -83,6 +84,7 @@ { "filter": f"(!(userAccountControl:1.2.840.113556.1.4.803:={UserAccountControlFlag.ACCOUNTDISABLE}))", # noqa: E501 "objects": [ + "cn=System Administrator,dc=md,dc=test", "cn=user0,cn=users,dc=md,dc=test", "cn=user_admin,cn=users,dc=md,dc=test", "cn=user_admin_for_roles,cn=users,dc=md,dc=test", @@ -104,6 +106,7 @@ + UserAccountControlFlag.NORMAL_ACCOUNT })", "objects": [ + "cn=System Administrator,dc=md,dc=test", "cn=user0,cn=users,dc=md,dc=test", "cn=user_admin,cn=users,dc=md,dc=test", "cn=user_admin_for_roles,cn=users,dc=md,dc=test", @@ -124,6 +127,7 @@ { "filter": f"(!(userAccountControl:1.2.840.113556.1.4.804:={UserAccountControlFlag.ACCOUNTDISABLE}))", # noqa: E501 "objects": [ + "cn=System Administrator,dc=md,dc=test", "cn=user0,cn=users,dc=md,dc=test", "cn=user_admin,cn=users,dc=md,dc=test", "cn=user_admin_for_roles,cn=users,dc=md,dc=test", diff --git a/tests/test_api/test_main/test_router/test_delete.py b/tests/test_api/test_main/test_router/test_delete.py index 59b714c95..6b359237c 100644 --- a/tests/test_api/test_main/test_router/test_delete.py +++ b/tests/test_api/test_main/test_router/test_delete.py @@ -29,7 +29,25 @@ async def test_api_correct_delete(http_client: AsyncClient) -> None: @pytest.mark.asyncio -@pytest.mark.usefixtures("adding_test_user") +@pytest.mark.usefixtures("setup_session") +@pytest.mark.usefixtures("session") +async def test_api_cant_delete_system_directory( + http_client: AsyncClient, +) -> None: + """Test API for delete system directory.""" + response = await http_client.request( + "delete", + "/entry/delete", + json={"entry": "cn=System Administrator,dc=md,dc=test"}, + ) + + data = response.json() + + assert isinstance(data, dict) + assert data.get("resultCode") == LDAPCodes.UNWILLING_TO_PERFORM + + +@pytest.mark.asyncio @pytest.mark.usefixtures("setup_session") @pytest.mark.usefixtures("session") async def test_api_delete_with_incorrect_dn(http_client: AsyncClient) -> None: @@ -49,7 +67,6 @@ async def test_api_delete_with_incorrect_dn(http_client: AsyncClient) -> None: @pytest.mark.asyncio -@pytest.mark.usefixtures("adding_test_user") @pytest.mark.usefixtures("setup_session") @pytest.mark.usefixtures("session") async def test_api_delete_non_exist_object(http_client: AsyncClient) -> None: diff --git a/tests/test_api/test_main/test_router/test_modify.py b/tests/test_api/test_main/test_router/test_modify.py index b5359f51a..9cb9d4eef 100644 --- a/tests/test_api/test_main/test_router/test_modify.py +++ b/tests/test_api/test_main/test_router/test_modify.py @@ -67,7 +67,6 @@ async def test_api_correct_modify(http_client: AsyncClient) -> None: @pytest.mark.asyncio -@pytest.mark.usefixtures("adding_test_user") @pytest.mark.usefixtures("setup_session") @pytest.mark.usefixtures("session") async def test_api_duplicate_with_spaces_modify( @@ -204,7 +203,6 @@ async def test_api_modify_many(http_client: AsyncClient) -> None: @pytest.mark.asyncio -@pytest.mark.usefixtures("adding_test_user") @pytest.mark.usefixtures("setup_session") @pytest.mark.usefixtures("session") async def test_api_modify_with_incorrect_dn(http_client: AsyncClient) -> None: @@ -258,7 +256,34 @@ async def test_api_modify_non_exist_object(http_client: AsyncClient) -> None: @pytest.mark.asyncio -@pytest.mark.usefixtures("adding_test_user") +@pytest.mark.usefixtures("session") +async def test_api_cant_modify_system_directory( + http_client: AsyncClient, +) -> None: + """Test API for modify system directory.""" + response = await http_client.patch( + "/entry/update", + json={ + "object": "cn=System Administrator,dc=md,dc=test", + "changes": [ + { + "operation": Operation.REPLACE, + "modification": { + "type": "name", + "vals": ["new_test"], + }, + }, + ], + }, + ) + + data = response.json() + + assert isinstance(data, dict) + assert data.get("resultCode") == LDAPCodes.UNWILLING_TO_PERFORM + + +@pytest.mark.asyncio @pytest.mark.usefixtures("setup_session") @pytest.mark.usefixtures("session") async def test_api_correct_modify_replace_memberof( @@ -398,7 +423,6 @@ async def test_api_modify_replace_loop_detect_member( @pytest.mark.asyncio -@pytest.mark.usefixtures("adding_test_user") @pytest.mark.usefixtures("setup_session") @pytest.mark.usefixtures("session") async def test_api_modify_replace_loop_detect_memberof( @@ -429,7 +453,6 @@ async def test_api_modify_replace_loop_detect_memberof( @pytest.mark.asyncio -@pytest.mark.usefixtures("adding_test_user") @pytest.mark.usefixtures("session") async def test_api_modify_incorrect_uac(http_client: AsyncClient) -> None: """Test API for modify object attribute.""" @@ -454,7 +477,6 @@ async def test_api_modify_incorrect_uac(http_client: AsyncClient) -> None: @pytest.mark.asyncio -@pytest.mark.usefixtures("adding_test_user") @pytest.mark.usefixtures("setup_session") @pytest.mark.usefixtures("session") async def test_qpi_modify_primary_object_classes( 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 6e5d71cfc..d511fbd20 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 @@ -11,7 +11,6 @@ @pytest.mark.asyncio -@pytest.mark.usefixtures("adding_test_user") @pytest.mark.usefixtures("setup_session") @pytest.mark.usefixtures("session") async def test_api_modify_dn_without_level_change( @@ -80,7 +79,6 @@ async def test_api_modify_dn_without_level_change( @pytest.mark.asyncio -@pytest.mark.usefixtures("adding_test_user") @pytest.mark.usefixtures("setup_session") @pytest.mark.usefixtures("session") async def test_api_modify_dn_with_level_down( @@ -149,7 +147,6 @@ async def test_api_modify_dn_with_level_down( @pytest.mark.asyncio -@pytest.mark.usefixtures("adding_test_user") @pytest.mark.usefixtures("setup_session") @pytest.mark.usefixtures("session") async def test_api_modify_dn_with_level_up( @@ -218,7 +215,6 @@ async def test_api_modify_dn_with_level_up( @pytest.mark.asyncio -@pytest.mark.usefixtures("adding_test_user") @pytest.mark.usefixtures("setup_session") @pytest.mark.usefixtures("session") async def test_api_correct_update_dn(http_client: AsyncClient) -> None: @@ -338,7 +334,6 @@ async def test_api_correct_update_dn(http_client: AsyncClient) -> None: @pytest.mark.asyncio -@pytest.mark.usefixtures("adding_test_user") @pytest.mark.usefixtures("setup_session") @pytest.mark.usefixtures("session") async def test_api_update_dn_with_parent(http_client: AsyncClient) -> None: @@ -436,7 +431,6 @@ async def test_api_update_dn_non_auth_user(http_client: AsyncClient) -> None: @pytest.mark.asyncio -@pytest.mark.usefixtures("adding_test_user") @pytest.mark.usefixtures("setup_session") @pytest.mark.usefixtures("session") async def test_api_update_dn_non_exist_superior( @@ -460,7 +454,29 @@ async def test_api_update_dn_non_exist_superior( @pytest.mark.asyncio -@pytest.mark.usefixtures("adding_test_user") +@pytest.mark.usefixtures("setup_session") +@pytest.mark.usefixtures("session") +async def test_api_cant_update_system_directory( + http_client: AsyncClient, +) -> None: + """Test API for update DN of system directory.""" + response = await http_client.put( + "/entry/update/dn", + json={ + "entry": "cn=System Administrator,dc=md,dc=test", + "newrdn": "cn=New System Administrator", + "deleteoldrdn": True, + "new_superior": "dc=non-exist,dc=test", + }, + ) + + data = response.json() + + assert isinstance(data, dict) + assert data.get("resultCode") == LDAPCodes.UNWILLING_TO_PERFORM + + +@pytest.mark.asyncio @pytest.mark.usefixtures("setup_session") @pytest.mark.usefixtures("session") async def test_api_update_dn_non_exist_entry(http_client: AsyncClient) -> None: @@ -482,7 +498,6 @@ async def test_api_update_dn_non_exist_entry(http_client: AsyncClient) -> None: @pytest.mark.asyncio -@pytest.mark.usefixtures("adding_test_user") @pytest.mark.usefixtures("setup_session") @pytest.mark.usefixtures("session") async def test_api_update_dn_invalid_entry(http_client: AsyncClient) -> None: @@ -504,7 +519,6 @@ async def test_api_update_dn_invalid_entry(http_client: AsyncClient) -> None: @pytest.mark.asyncio -@pytest.mark.usefixtures("adding_test_user") @pytest.mark.usefixtures("setup_session") @pytest.mark.usefixtures("session") async def test_api_update_dn_invalid_new_superior( diff --git a/tests/test_api/test_main/test_router/test_search.py b/tests/test_api/test_main/test_router/test_search.py index 9df3898c5..911e4b344 100644 --- a/tests/test_api/test_main/test_router/test_search.py +++ b/tests/test_api/test_main/test_router/test_search.py @@ -95,15 +95,16 @@ async def test_api_search(http_client: AsyncClient) -> None: assert response["resultCode"] == LDAPCodes.SUCCESS - sub_dirs = [ + sub_dirs = { + "cn=System Administrator,dc=md,dc=test", "cn=groups,dc=md,dc=test", "cn=users,dc=md,dc=test", "ou=testModifyDn1,dc=md,dc=test", "ou=testModifyDn3,dc=md,dc=test", "ou=test_bit_rules,dc=md,dc=test", - ] - assert all( - obj["object_name"] in sub_dirs for obj in response["search_result"] + } + assert sub_dirs == set( + obj["object_name"] for obj in response["search_result"] ) @@ -280,6 +281,7 @@ async def test_api_search_recursive_memberof(http_client: AsyncClient) -> None: """Test api search.""" group = "cn=domain admins,cn=groups,dc=md,dc=test" members = [ + "cn=System Administrator,dc=md,dc=test", "cn=developers,cn=groups,dc=md,dc=test", "cn=user0,cn=users,dc=md,dc=test", "cn=user_admin,cn=users,dc=md,dc=test", diff --git a/tests/test_ldap/test_roles/test_search.py b/tests/test_ldap/test_roles/test_search.py index a20e8f0dd..7965a3fe0 100644 --- a/tests/test_ldap/test_roles/test_search.py +++ b/tests/test_ldap/test_roles/test_search.py @@ -102,6 +102,7 @@ async def test_role_search_3( creds=creds, search_base=BASE_DN, expected_dn=[ + "dn: cn=System Administrator,dc=md,dc=test", "dn: cn=groups,dc=md,dc=test", "dn: cn=users,dc=md,dc=test", "dn: cn=user_non_admin,cn=users,dc=md,dc=test", @@ -189,6 +190,7 @@ async def test_role_search_5( creds=creds, search_base=BASE_DN, expected_dn=[ + "dn: cn=System Administrator,dc=md,dc=test", "dn: cn=user1,cn=moscow,cn=russia,cn=users,dc=md,dc=test", "dn: cn=user_non_admin,cn=users,dc=md,dc=test", "dn: cn=user_admin_for_roles,cn=users,dc=md,dc=test",