-
Notifications
You must be signed in to change notification settings - Fork 0
Add: Services to system #883
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
0def0a5
297d5e5
d40dcc0
3db3fd8
a384c87
4109966
17f3d28
4fe2154
60ae9bd
25a8715
8cd59a7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,179 @@ | ||||||
| """Rename services container to System for AD compatibility. | ||||||
| Revision ID: a1b2c3d4e5f6 | ||||||
| Revises: 6c858cc05da7 | ||||||
| Create Date: 2026-01-13 12:00:00.000000 | ||||||
| """ | ||||||
|
|
||||||
| from alembic import op | ||||||
| from dishka import AsyncContainer, Scope | ||||||
| from sqlalchemy import and_, exists, select | ||||||
| from sqlalchemy.ext.asyncio import AsyncConnection, AsyncSession | ||||||
|
|
||||||
| from entities import Attribute, 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 = "a1b2c3d4e5f6" | ||||||
| down_revision: None | str = "71e642808369" | ||||||
| branch_labels: None | list[str] = None | ||||||
| depends_on: None | list[str] = None | ||||||
|
|
||||||
|
|
||||||
| def upgrade(container: AsyncContainer) -> None: | ||||||
| """Upgrade: Rename 'services' container to 'System'.""" | ||||||
|
|
||||||
| async def _update_descendants( | ||||||
| session: AsyncSession, | ||||||
| parent_id: int, | ||||||
| ) -> None: | ||||||
| """Recursively update paths of all descendants.""" | ||||||
| child_dirs = await session.scalars( | ||||||
| select(Directory) | ||||||
| .where(qa(Directory.parent_id) == parent_id), | ||||||
| ) # fmt: skip | ||||||
|
|
||||||
| for child_dir in child_dirs: | ||||||
| child_dir.path = [ | ||||||
| "ou=System" if p == "ou=services" else p | ||||||
| for p in child_dir.path | ||||||
| ] | ||||||
| await session.flush() | ||||||
| await _update_descendants(session, child_dir.id) | ||||||
|
|
||||||
| async def _update_attributes( | ||||||
| session: AsyncSession, | ||||||
| old_value: str, | ||||||
| new_value: str, | ||||||
| ) -> None: | ||||||
| """Update attribute values containing old DN.""" | ||||||
| result = await session.execute( | ||||||
| select(Attribute) | ||||||
| .where( | ||||||
| Attribute.value.ilike(f"%{old_value}%"), # type: ignore | ||||||
| ), | ||||||
| ) # fmt: skip | ||||||
| attributes = result.scalars().all() | ||||||
|
|
||||||
| for attr in attributes: | ||||||
| if attr.value and old_value in attr.value: | ||||||
| attr.value = attr.value.replace(old_value, new_value) | ||||||
|
|
||||||
| await session.flush() | ||||||
|
|
||||||
| async def _rename_services_to_system(connection: AsyncConnection) -> None: # noqa: ARG001 | ||||||
| async with container(scope=Scope.REQUEST) as cnt: | ||||||
| session = await cnt.get(AsyncSession) | ||||||
|
|
||||||
| base_directories = await get_base_directories(session) | ||||||
| if not base_directories: | ||||||
| await session.commit() | ||||||
| return | ||||||
|
|
||||||
| service_dirs = await session.scalars( | ||||||
| select(Directory).where(qa(Directory.name) == "services"), | ||||||
| ) | ||||||
|
|
||||||
| for service_dir in service_dirs: | ||||||
| system_exists = await session.scalar( | ||||||
| select(exists(Directory)) | ||||||
| .where( | ||||||
| and_( | ||||||
| qa(Directory.name) == "System", | ||||||
| qa(Directory.parent_id) == service_dir.parent_id, | ||||||
| ), | ||||||
| ), | ||||||
| ) # fmt: skip | ||||||
|
|
||||||
| if system_exists: | ||||||
| continue | ||||||
|
|
||||||
| service_dir.name = "System" | ||||||
| service_dir.path = [ | ||||||
| "ou=System" if p == "ou=services" else p | ||||||
| for p in service_dir.path | ||||||
| ] | ||||||
|
|
||||||
| await session.flush() | ||||||
| await _update_descendants(session, service_dir.id) | ||||||
|
|
||||||
| await _update_attributes(session, "ou=services", "ou=System") | ||||||
| await session.commit() | ||||||
|
|
||||||
| op.run_async(_rename_services_to_system) | ||||||
|
|
||||||
|
|
||||||
| def downgrade(container: AsyncContainer) -> None: | ||||||
| """Downgrade: Rename 'System' container back to 'services'.""" | ||||||
|
|
||||||
| async def _update_descendants_downgrade( | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. а функции эти не одинаковые с upgrade? очень прям похожие, лучше вынеси |
||||||
| session: AsyncSession, | ||||||
| parent_id: int, | ||||||
| ) -> None: | ||||||
| """Recursively update paths of all descendants.""" | ||||||
| child_dirs = await session.scalars( | ||||||
| select(Directory) | ||||||
| .where(qa(Directory.parent_id) == parent_id), | ||||||
| ) # fmt: skip | ||||||
|
|
||||||
| for child_dir in child_dirs: | ||||||
| child_dir.path = [ | ||||||
| "ou=services" if p == "ou=System" else p | ||||||
| for p in child_dir.path | ||||||
| ] | ||||||
| await session.flush() | ||||||
| await _update_descendants_downgrade(session, child_dir.id) | ||||||
|
|
||||||
| async def _update_attributes_downgrade( | ||||||
| session: AsyncSession, | ||||||
| old_value: str, | ||||||
| new_value: str, | ||||||
| ) -> None: | ||||||
| """Update attribute values during downgrade.""" | ||||||
| result = await session.execute( | ||||||
| select(Attribute) | ||||||
| .where( | ||||||
| Attribute.value.ilike(f"%{old_value}%"), # type: ignore | ||||||
| ), | ||||||
| ) # fmt: skip | ||||||
| attributes = result.scalars().all() | ||||||
|
|
||||||
| for attr in attributes: | ||||||
| if attr.value and old_value in attr.value: | ||||||
| attr.value = attr.value.replace(old_value, new_value) | ||||||
|
|
||||||
| await session.flush() | ||||||
|
|
||||||
| async def _rename_system_to_services(connection: AsyncConnection) -> None: # noqa ARG001 | ||||||
|
||||||
| async def _rename_system_to_services(connection: AsyncConnection) -> None: # noqa ARG001 | |
| async def _rename_system_to_services(connection: AsyncConnection) -> None: # noqa: ARG001 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,46 +1,89 @@ | ||
| """Kerberos update config. | ||
| """Kerberos configuration update script. | ||
|
|
||
| Copyright (c) 2025 MultiFactor | ||
| License: https://github.com/MultiDirectoryLab/MultiDirectory/blob/main/LICENSE | ||
| """ | ||
|
|
||
| from pathlib import Path | ||
|
|
||
| from loguru import logger | ||
| from sqlalchemy.ext.asyncio import AsyncSession | ||
|
|
||
| from config import Settings | ||
| from ldap_protocol.kerberos import AbstractKadmin | ||
| from ldap_protocol.kerberos.utils import get_system_container_dn | ||
| from ldap_protocol.utils.queries import get_base_directories | ||
|
|
||
| KRB5_CONF_PATH = Path("/etc/krb5kdc/krb5.conf") | ||
| KDC_CONF_PATH = Path("/etc/krb5kdc/kdc.conf") | ||
| STASH_FILE_PATH = Path("/etc/krb5kdc/krb5.d/stash.keyfile") | ||
|
|
||
|
|
||
| def _migrate_legacy_dns(content: str) -> str: | ||
| """Replace legacy DN formats with current ones. | ||
|
|
||
| :param content: File content to migrate. | ||
| :return: Migrated content. | ||
| """ | ||
| return content.replace("ou=services", "ou=System").replace( | ||
| "ou=users", | ||
| "cn=users", | ||
| ) | ||
|
|
||
|
|
||
| async def update_krb5_config( | ||
| kadmin: AbstractKadmin, | ||
| session: AsyncSession, | ||
| settings: Settings, | ||
| ) -> None: | ||
| """Update kerberos config.""" | ||
| if not (await kadmin.get_status(wait_for_positive=True)): | ||
| logger.error("kadmin_api is not running") | ||
| return | ||
| """Update Kerberos configuration files via direct write to shared volume. | ||
|
|
||
| base_dn_list = await get_base_directories(session) | ||
| base_dn = base_dn_list[0].path_dn | ||
| domain: str = base_dn_list[0].name | ||
| Renders krb5.conf and kdc.conf from templates and writes them directly | ||
| to the shared volume. Also migrates legacy DN formats in stash.keyfile | ||
| if present (ou=services -> ou=System, ou=users -> cn=users). | ||
|
|
||
| krbadmin = "cn=krbadmin,cn=users," + base_dn | ||
| services_container = "ou=services," + base_dn | ||
| :param session: Database session for fetching base directories. | ||
| :param settings: Application settings with template environment. | ||
| :raises Exception: If config rendering or writing fails. | ||
| """ | ||
| if not KRB5_CONF_PATH.parent.exists(): | ||
| logger.error( | ||
| f"Config directory {KRB5_CONF_PATH.parent} not found, " | ||
| "kerberos volume not mounted", | ||
| ) | ||
| return | ||
|
|
||
| krb5_template = settings.TEMPLATES.get_template("krb5.conf") | ||
| kdc_template = settings.TEMPLATES.get_template("kdc.conf") | ||
| base_dn_list = await get_base_directories(session) | ||
| if not base_dn_list: | ||
| logger.error("No base directories found") | ||
| return | ||
|
|
||
| kdc_config = await kdc_template.render_async(domain=domain) | ||
| base_dn = base_dn_list[0].path_dn | ||
| domain = base_dn_list[0].name | ||
| krbadmin = f"cn=krbadmin,cn=users,{base_dn}" | ||
| services_container = get_system_container_dn(base_dn) | ||
|
|
||
| krb5_config = await krb5_template.render_async( | ||
| krb5_config = await settings.TEMPLATES.get_template( | ||
| "krb5.conf", | ||
| ).render_async( | ||
| domain=domain, | ||
| krbadmin=krbadmin, | ||
| services_container=services_container, | ||
| ldap_uri=settings.KRB5_LDAP_URI, | ||
| mfa_push_url=settings.KRB5_MFA_PUSH_URL, | ||
| sync_password_url=settings.KRB5_SYNC_PASSWORD_URL, | ||
| ) | ||
| kdc_config = await settings.TEMPLATES.get_template( | ||
| "kdc.conf", | ||
| ).render_async( | ||
| domain=domain, | ||
| ) | ||
|
|
||
| KRB5_CONF_PATH.write_text(krb5_config, encoding="utf-8") | ||
| KDC_CONF_PATH.write_text(kdc_config, encoding="utf-8") | ||
|
|
||
| await kadmin.setup_configs(krb5_config, kdc_config) | ||
| if STASH_FILE_PATH.exists(): | ||
| stash_content = STASH_FILE_PATH.read_text(encoding="utf-8") | ||
| if "ou=services" in stash_content or "ou=users" in stash_content: | ||
| STASH_FILE_PATH.write_text( | ||
| _migrate_legacy_dns(stash_content), | ||
| encoding="utf-8", | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -424,6 +424,7 @@ services: | |
| - ./certs:/certs | ||
| - ./app:/app | ||
| - ldap_keytab:/LDAP_keytab/ | ||
| - kdc:/etc/krb5kdc/ | ||
milov-dmitriy marked this conversation as resolved.
Show resolved
Hide resolved
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. в другие композы продублировать, особенно в .package, там продовый валяется |
||
| env_file: local.env | ||
| command: python multidirectory.py --scheduler | ||
| tty: true | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.