From cd531438977163281daa849fab52e4ecf21924e7 Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Sun, 9 Aug 2026 15:09:12 +0300 Subject: [PATCH 1/3] fix(infrastructure): company name toctou --- .../a3029168e306_unique_company_name.py | 32 +++++++++++++++++++ .../infrastructure/persistence/registry.py | 2 +- 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 src/conode/infrastructure/persistence/migrations/versions/a3029168e306_unique_company_name.py diff --git a/src/conode/infrastructure/persistence/migrations/versions/a3029168e306_unique_company_name.py b/src/conode/infrastructure/persistence/migrations/versions/a3029168e306_unique_company_name.py new file mode 100644 index 0000000..4340dec --- /dev/null +++ b/src/conode/infrastructure/persistence/migrations/versions/a3029168e306_unique_company_name.py @@ -0,0 +1,32 @@ +"""unique-company-name + +Revision ID: a3029168e306 +Revises: 74a91a825965 +Create Date: 2026-08-09 12:07:58.394174 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'a3029168e306' +down_revision: Union[str, Sequence[str], None] = '74a91a825965' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_unique_constraint(None, 'company_record', ['name']) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'company_record', type_='unique') + # ### end Alembic commands ### diff --git a/src/conode/infrastructure/persistence/registry.py b/src/conode/infrastructure/persistence/registry.py index 829f1fa..e9d341c 100644 --- a/src/conode/infrastructure/persistence/registry.py +++ b/src/conode/infrastructure/persistence/registry.py @@ -74,7 +74,7 @@ ForeignKey("user_record.id", ondelete="CASCADE"), nullable=False, ), - Column("name", CompanyNameType, nullable=False), + Column("name", CompanyNameType, nullable=False, unique=True), Column("description", CompanyDescriptionType, nullable=False), Column("verified", Boolean, nullable=False), Column("created_at", DateTime(timezone=True), nullable=False), From 55ef736908cf5979c95199ebd462562ec9c6ae8d Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Sun, 9 Aug 2026 15:09:20 +0300 Subject: [PATCH 2/3] fix(application): company name toctou --- .../application/register_company/register_company.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/conode/application/register_company/register_company.py b/src/conode/application/register_company/register_company.py index 783d32b..c794966 100644 --- a/src/conode/application/register_company/register_company.py +++ b/src/conode/application/register_company/register_company.py @@ -38,15 +38,6 @@ async def execute(self, request: RegisterCompanyRequestDTO) -> Company: async with self.transaction_manager: user = await self.access_control_service.get_authorized_user() - company = await self.company_repository.get_by_name( - CompanyName(request.name), - ) - if company is not None: - raise CompanyAlreadyExistsError( - "Company with this name already exists", - [{"key": "name", "value": request.name}], - ) - company = Company.new( company_id=CompanyId(uuid4()), name=request.name, From 3fea0f0dcb42822926920f6059a94ebd182e7c56 Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Sun, 9 Aug 2026 15:16:28 +0300 Subject: [PATCH 3/3] chore(infrastructure): add exception handler --- .../register_company/register_company.py | 3 +- .../infrastructure/repositories/company.py | 31 ++++++++++++------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/conode/application/register_company/register_company.py b/src/conode/application/register_company/register_company.py index c794966..d5cbfdd 100644 --- a/src/conode/application/register_company/register_company.py +++ b/src/conode/application/register_company/register_company.py @@ -1,7 +1,6 @@ from dataclasses import dataclass from uuid import uuid4 -from conode.application.errors import CompanyAlreadyExistsError from conode.application.interfaces.repositories import ( CompanyRepository, RolePermissionsRepository, @@ -10,7 +9,7 @@ ) from conode.application.interfaces.transaction_manager import TransactionManager from conode.application.services import AccessControlService, RoleManagmentService -from conode.domain.company import Company, CompanyId, CompanyName +from conode.domain.company import Company, CompanyId from conode.domain.grant import UserGrant, UserGrantId from conode.domain.role import ( EntityType, diff --git a/src/conode/infrastructure/repositories/company.py b/src/conode/infrastructure/repositories/company.py index 10e3bdf..3bbe13b 100644 --- a/src/conode/infrastructure/repositories/company.py +++ b/src/conode/infrastructure/repositories/company.py @@ -2,9 +2,10 @@ import structlog from sqlalchemy import insert, select, update +from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession -from conode.application.errors import CompanyNotFoundError +from conode.application.errors import CompanyAlreadyExistsError, CompanyNotFoundError from conode.application.interfaces.repositories import CompanyRepository from conode.domain.company.model import Company, CompanyId, CompanyName from conode.domain.user import UserId @@ -18,17 +19,23 @@ class CompanyRepositoryImpl(CompanyRepository): async def create(self, company: Company) -> None: logger.debug("Repository create company", company_id=company.id) - await self.session.execute( - insert(Company).values( - id=company.id, - name=company.name, - description=company.description, - verified=company.verified, - owner_id=company.owner_id, - created_at=company.created_at, - updated_at=company.updated_at, - ), - ) + try: + await self.session.execute( + insert(Company).values( + id=company.id, + name=company.name, + description=company.description, + verified=company.verified, + owner_id=company.owner_id, + created_at=company.created_at, + updated_at=company.updated_at, + ), + ) + except IntegrityError as e: + raise CompanyAlreadyExistsError( + "Company with this name already exists", + [{"key": "name", "value": company.name.value}], + ) from e async def update(self, company: Company) -> None: logger.debug("Repository update company", company_id=company.id)