diff --git a/.gitignore b/.gitignore index 0e8f587..9ab1037 100644 --- a/.gitignore +++ b/.gitignore @@ -1,21 +1,51 @@ -# Poetry specific files -.venv/ -/dist/ -/poetry.toml - -# Python bytecode and caches +``` +# Python __pycache__/ *.pyc *.pyo *.pyd +.Python +*.so +*.egg-info/ +.eggs/ + +# Virtual environments +venv/ +.venv/ +env/ +ENV/ +.ENV + +# Build artifacts +build/ +dist/ +*.egg + +# Testing +.coverage +htmlcov/ .pytest_cache/ .mypy_cache/ -.ruff_cache/ -.cache/ -# Environment variables (secret keys) +# Logs +*.log + +# Environment variables .env +.env.local +*.env.* -# IDE settings +# IDE .vscode/ .idea/ +*.swp +*.swo +*.tmp + +# OS +.DS_Store +Thumbs.db + +# Coverage +coverage/ +``` \ No newline at end of file diff --git a/src/modules/authorization/application/create_permission/command.py b/src/modules/authorization/application/create_permission/command.py new file mode 100644 index 0000000..9d2a79b --- /dev/null +++ b/src/modules/authorization/application/create_permission/command.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel + + +class CreatePermissionCommand(BaseModel): + resource: str + action: str + description: str | None = None diff --git a/src/modules/authorization/application/create_permission/handler.py b/src/modules/authorization/application/create_permission/handler.py new file mode 100644 index 0000000..569972c --- /dev/null +++ b/src/modules/authorization/application/create_permission/handler.py @@ -0,0 +1,31 @@ +from src.modules.authorization.application.create_permission.command import ( + CreatePermissionCommand, +) +from src.modules.authorization.domain.entities.permission import Permission +from src.modules.authorization.domain.permissions import permission_key +from src.modules.authorization.domain.repositories.casbin_policy_repository import ( + CasbinPolicyRepository, +) +from src.shared.unit_of_work import UnitOfWork + + +class CreatePermissionHandler: + def __init__( + self, + policy_repo: CasbinPolicyRepository, + unit_of_work: UnitOfWork, + ): + self._policy_repo = policy_repo + self._unit_of_work = unit_of_work + + async def execute(self, command: CreatePermissionCommand) -> Permission: + permission = Permission.create( + key=permission_key(command.resource, command.action), + resource=command.resource, + action=command.action, + description=command.description, + ) + async with self._unit_of_work: + created = await self._policy_repo.create_permission(permission) + await self._unit_of_work.commit() + return created diff --git a/src/modules/authorization/application/create_role/command.py b/src/modules/authorization/application/create_role/command.py new file mode 100644 index 0000000..e91f655 --- /dev/null +++ b/src/modules/authorization/application/create_role/command.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel + + +class CreateRoleCommand(BaseModel): + name: str + description: str | None = None diff --git a/src/modules/authorization/application/create_role/handler.py b/src/modules/authorization/application/create_role/handler.py new file mode 100644 index 0000000..5b29225 --- /dev/null +++ b/src/modules/authorization/application/create_role/handler.py @@ -0,0 +1,23 @@ +from src.modules.authorization.application.create_role.command import CreateRoleCommand +from src.modules.authorization.domain.entities.role import Role +from src.modules.authorization.domain.repositories.casbin_policy_repository import ( + CasbinPolicyRepository, +) +from src.shared.unit_of_work import UnitOfWork + + +class CreateRoleHandler: + def __init__( + self, + policy_repo: CasbinPolicyRepository, + unit_of_work: UnitOfWork, + ): + self._policy_repo = policy_repo + self._unit_of_work = unit_of_work + + async def execute(self, command: CreateRoleCommand) -> Role: + role = Role.create(name=command.name, description=command.description) + async with self._unit_of_work: + created = await self._policy_repo.create_role(role) + await self._unit_of_work.commit() + return created diff --git a/src/modules/authorization/application/delete_permission/command.py b/src/modules/authorization/application/delete_permission/command.py new file mode 100644 index 0000000..cf3306e --- /dev/null +++ b/src/modules/authorization/application/delete_permission/command.py @@ -0,0 +1,7 @@ +from uuid import UUID + +from pydantic import BaseModel + + +class DeletePermissionCommand(BaseModel): + permission_id: UUID diff --git a/src/modules/authorization/application/delete_permission/handler.py b/src/modules/authorization/application/delete_permission/handler.py new file mode 100644 index 0000000..00f0c6f --- /dev/null +++ b/src/modules/authorization/application/delete_permission/handler.py @@ -0,0 +1,22 @@ +from src.modules.authorization.application.delete_permission.command import ( + DeletePermissionCommand, +) +from src.modules.authorization.domain.repositories.casbin_policy_repository import ( + CasbinPolicyRepository, +) +from src.shared.unit_of_work import UnitOfWork + + +class DeletePermissionHandler: + def __init__( + self, + policy_repo: CasbinPolicyRepository, + unit_of_work: UnitOfWork, + ): + self._policy_repo = policy_repo + self._unit_of_work = unit_of_work + + async def execute(self, command: DeletePermissionCommand) -> None: + async with self._unit_of_work: + await self._policy_repo.delete_permission(command.permission_id) + await self._unit_of_work.commit() diff --git a/src/modules/authorization/application/delete_role/command.py b/src/modules/authorization/application/delete_role/command.py new file mode 100644 index 0000000..26a2906 --- /dev/null +++ b/src/modules/authorization/application/delete_role/command.py @@ -0,0 +1,7 @@ +from uuid import UUID + +from pydantic import BaseModel + + +class DeleteRoleCommand(BaseModel): + role_id: UUID diff --git a/src/modules/authorization/application/delete_role/handler.py b/src/modules/authorization/application/delete_role/handler.py new file mode 100644 index 0000000..5f8c61b --- /dev/null +++ b/src/modules/authorization/application/delete_role/handler.py @@ -0,0 +1,20 @@ +from src.modules.authorization.application.delete_role.command import DeleteRoleCommand +from src.modules.authorization.domain.repositories.casbin_policy_repository import ( + CasbinPolicyRepository, +) +from src.shared.unit_of_work import UnitOfWork + + +class DeleteRoleHandler: + def __init__( + self, + policy_repo: CasbinPolicyRepository, + unit_of_work: UnitOfWork, + ): + self._policy_repo = policy_repo + self._unit_of_work = unit_of_work + + async def execute(self, command: DeleteRoleCommand) -> None: + async with self._unit_of_work: + await self._policy_repo.delete_role(command.role_id) + await self._unit_of_work.commit() diff --git a/src/modules/authorization/application/get_permission/handler.py b/src/modules/authorization/application/get_permission/handler.py new file mode 100644 index 0000000..a10944c --- /dev/null +++ b/src/modules/authorization/application/get_permission/handler.py @@ -0,0 +1,13 @@ +from src.modules.authorization.application.get_permission.query import GetPermissionQuery +from src.modules.authorization.domain.entities.permission import Permission +from src.modules.authorization.domain.repositories.casbin_policy_repository import ( + CasbinPolicyRepository, +) + + +class GetPermissionQueryHandler: + def __init__(self, policy_repo: CasbinPolicyRepository): + self._policy_repo = policy_repo + + async def execute(self, query: GetPermissionQuery) -> Permission | None: + return await self._policy_repo.get_permission(query.permission_id) diff --git a/src/modules/authorization/application/get_permission/query.py b/src/modules/authorization/application/get_permission/query.py new file mode 100644 index 0000000..b1f2a6b --- /dev/null +++ b/src/modules/authorization/application/get_permission/query.py @@ -0,0 +1,7 @@ +from uuid import UUID + +from pydantic import BaseModel + + +class GetPermissionQuery(BaseModel): + permission_id: UUID diff --git a/src/modules/authorization/application/get_role/handler.py b/src/modules/authorization/application/get_role/handler.py new file mode 100644 index 0000000..1ebd1f1 --- /dev/null +++ b/src/modules/authorization/application/get_role/handler.py @@ -0,0 +1,15 @@ +from uuid import UUID + +from src.modules.authorization.application.get_role.query import GetRoleQuery +from src.modules.authorization.domain.entities.role import Role +from src.modules.authorization.domain.repositories.casbin_policy_repository import ( + CasbinPolicyRepository, +) + + +class GetRoleQueryHandler: + def __init__(self, policy_repo: CasbinPolicyRepository): + self._policy_repo = policy_repo + + async def execute(self, query: GetRoleQuery) -> Role | None: + return await self._policy_repo.get_role(query.role_id) diff --git a/src/modules/authorization/application/get_role/query.py b/src/modules/authorization/application/get_role/query.py new file mode 100644 index 0000000..6111fe5 --- /dev/null +++ b/src/modules/authorization/application/get_role/query.py @@ -0,0 +1,7 @@ +from uuid import UUID + +from pydantic import BaseModel + + +class GetRoleQuery(BaseModel): + role_id: UUID diff --git a/src/modules/authorization/application/list_permissions/query.py b/src/modules/authorization/application/list_permissions/query.py new file mode 100644 index 0000000..7892285 --- /dev/null +++ b/src/modules/authorization/application/list_permissions/query.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class ListPermissionsQuery(BaseModel): + pass diff --git a/src/modules/authorization/application/list_roles/handler.py b/src/modules/authorization/application/list_roles/handler.py new file mode 100644 index 0000000..5fe5825 --- /dev/null +++ b/src/modules/authorization/application/list_roles/handler.py @@ -0,0 +1,13 @@ +from src.modules.authorization.application.list_roles.query import ListRolesQuery +from src.modules.authorization.domain.entities.role import Role +from src.modules.authorization.domain.repositories.casbin_policy_repository import ( + CasbinPolicyRepository, +) + + +class ListRolesQueryHandler: + def __init__(self, policy_repo: CasbinPolicyRepository): + self._policy_repo = policy_repo + + async def execute(self, query: ListRolesQuery) -> list[Role]: + return await self._policy_repo.list_roles() diff --git a/src/modules/authorization/application/list_roles/query.py b/src/modules/authorization/application/list_roles/query.py new file mode 100644 index 0000000..234a8f1 --- /dev/null +++ b/src/modules/authorization/application/list_roles/query.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class ListRolesQuery(BaseModel): + pass diff --git a/src/modules/authorization/application/update_permission/command.py b/src/modules/authorization/application/update_permission/command.py new file mode 100644 index 0000000..d18b2a0 --- /dev/null +++ b/src/modules/authorization/application/update_permission/command.py @@ -0,0 +1,10 @@ +from uuid import UUID + +from pydantic import BaseModel + + +class UpdatePermissionCommand(BaseModel): + permission_id: UUID + resource: str | None = None + action: str | None = None + description: str | None = None diff --git a/src/modules/authorization/application/update_permission/handler.py b/src/modules/authorization/application/update_permission/handler.py new file mode 100644 index 0000000..229f523 --- /dev/null +++ b/src/modules/authorization/application/update_permission/handler.py @@ -0,0 +1,44 @@ +from src.modules.authorization.application.update_permission.command import ( + UpdatePermissionCommand, +) +from src.modules.authorization.domain.entities.permission import Permission +from src.modules.authorization.domain.permissions import permission_key +from src.modules.authorization.domain.repositories.casbin_policy_repository import ( + CasbinPolicyRepository, +) +from src.shared.unit_of_work import UnitOfWork + + +class UpdatePermissionHandler: + def __init__( + self, + policy_repo: CasbinPolicyRepository, + unit_of_work: UnitOfWork, + ): + self._policy_repo = policy_repo + self._unit_of_work = unit_of_work + + async def execute(self, command: UpdatePermissionCommand) -> Permission | None: + existing = await self._policy_repo.get_permission(command.permission_id) + if existing is None: + return None + + resource = ( + command.resource if command.resource is not None else existing.resource + ) + action = command.action if command.action is not None else existing.action + permission = Permission( + id=command.permission_id, + key=permission_key(resource, action), + resource=resource, + action=action, + description=( + command.description + if command.description is not None + else existing.description + ), + ) + async with self._unit_of_work: + updated = await self._policy_repo.update_permission(permission) + await self._unit_of_work.commit() + return updated diff --git a/src/modules/authorization/application/update_role/command.py b/src/modules/authorization/application/update_role/command.py new file mode 100644 index 0000000..0d1459f --- /dev/null +++ b/src/modules/authorization/application/update_role/command.py @@ -0,0 +1,9 @@ +from uuid import UUID + +from pydantic import BaseModel + + +class UpdateRoleCommand(BaseModel): + role_id: UUID + name: str | None = None + description: str | None = None diff --git a/src/modules/authorization/application/update_role/handler.py b/src/modules/authorization/application/update_role/handler.py new file mode 100644 index 0000000..a232ba7 --- /dev/null +++ b/src/modules/authorization/application/update_role/handler.py @@ -0,0 +1,35 @@ +from src.modules.authorization.application.update_role.command import UpdateRoleCommand +from src.modules.authorization.domain.entities.role import Role +from src.modules.authorization.domain.repositories.casbin_policy_repository import ( + CasbinPolicyRepository, +) +from src.shared.unit_of_work import UnitOfWork + + +class UpdateRoleHandler: + def __init__( + self, + policy_repo: CasbinPolicyRepository, + unit_of_work: UnitOfWork, + ): + self._policy_repo = policy_repo + self._unit_of_work = unit_of_work + + async def execute(self, command: UpdateRoleCommand) -> Role | None: + existing = await self._policy_repo.get_role(command.role_id) + if existing is None: + return None + + role = Role( + id=command.role_id, + name=command.name if command.name is not None else existing.name, + description=( + command.description + if command.description is not None + else existing.description + ), + ) + async with self._unit_of_work: + updated = await self._policy_repo.update_role(role) + await self._unit_of_work.commit() + return updated diff --git a/src/core/authorization/permissions.py b/src/modules/authorization/domain/permissions.py similarity index 100% rename from src/core/authorization/permissions.py rename to src/modules/authorization/domain/permissions.py diff --git a/src/core/authorization/domain/service.py b/src/modules/authorization/domain/services/authorization_service.py similarity index 100% rename from src/core/authorization/domain/service.py rename to src/modules/authorization/domain/services/authorization_service.py diff --git a/src/core/authorization/infrastructure/models/casbin_rule_model.py b/src/modules/authorization/infrastructure/models/casbin_rule_model.py similarity index 100% rename from src/core/authorization/infrastructure/models/casbin_rule_model.py rename to src/modules/authorization/infrastructure/models/casbin_rule_model.py diff --git a/src/core/authorization/infrastructure/models/permission_model.py b/src/modules/authorization/infrastructure/models/permission_model.py similarity index 100% rename from src/core/authorization/infrastructure/models/permission_model.py rename to src/modules/authorization/infrastructure/models/permission_model.py diff --git a/src/core/authorization/infrastructure/models/resource_model.py b/src/modules/authorization/infrastructure/models/resource_model.py similarity index 100% rename from src/core/authorization/infrastructure/models/resource_model.py rename to src/modules/authorization/infrastructure/models/resource_model.py diff --git a/src/core/authorization/infrastructure/models/role_model.py b/src/modules/authorization/infrastructure/models/role_model.py similarity index 100% rename from src/core/authorization/infrastructure/models/role_model.py rename to src/modules/authorization/infrastructure/models/role_model.py diff --git a/src/core/authorization/infrastructure/models/role_permission_model.py b/src/modules/authorization/infrastructure/models/role_permission_model.py similarity index 100% rename from src/core/authorization/infrastructure/models/role_permission_model.py rename to src/modules/authorization/infrastructure/models/role_permission_model.py diff --git a/src/core/authorization/infrastructure/models/user_has_role_model.py b/src/modules/authorization/infrastructure/models/user_has_role_model.py similarity index 100% rename from src/core/authorization/infrastructure/models/user_has_role_model.py rename to src/modules/authorization/infrastructure/models/user_has_role_model.py diff --git a/src/core/authorization/infrastructure/repositories/casbin_policy_repository.py b/src/modules/authorization/infrastructure/repositories/casbin_policy_repository.py similarity index 100% rename from src/core/authorization/infrastructure/repositories/casbin_policy_repository.py rename to src/modules/authorization/infrastructure/repositories/casbin_policy_repository.py diff --git a/src/core/authorization/infrastructure/services/casbin_authorization_service.py b/src/modules/authorization/infrastructure/services/casbin_authorization_service.py similarity index 91% rename from src/core/authorization/infrastructure/services/casbin_authorization_service.py rename to src/modules/authorization/infrastructure/services/casbin_authorization_service.py index 2d191d8..ca52751 100644 --- a/src/core/authorization/infrastructure/services/casbin_authorization_service.py +++ b/src/modules/authorization/infrastructure/services/casbin_authorization_service.py @@ -1,12 +1,15 @@ from datetime import datetime from uuid import UUID -from src.core.authorization.domain.service import AuthorizationService -from src.core.authorization.infrastructure.repositories.casbin_policy_repository import ( +from src.modules.authorization.domain.permissions import permission_key +from src.modules.authorization.domain.services.authorization_service import ( + AuthorizationService, +) +from src.modules.authorization.infrastructure.repositories.casbin_policy_repository import ( SQLAlchemyCasbinPolicyRepository, ) -from src.core.authorization.permissions import permission_key -from src.modules.authorization import Permission, Role +from src.modules.authorization.domain.entities.permission import Permission +from src.modules.authorization.domain.entities.role import Role from src.shared.utils.cursor import CursorDirection CASBIN_MODEL_TEXT = """ diff --git a/src/core/authorization/dependencies.py b/src/modules/authorization/presentation/dependency.py similarity index 79% rename from src/core/authorization/dependencies.py rename to src/modules/authorization/presentation/dependency.py index 39b43f5..c5f91e0 100644 --- a/src/core/authorization/dependencies.py +++ b/src/modules/authorization/presentation/dependency.py @@ -3,15 +3,17 @@ from fastapi import Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession -from src.core.authorization.domain.service import AuthorizationService -from src.core.authorization.infrastructure.repositories.casbin_policy_repository import ( +from src.core.database.postgres.session import get_db +from src.core.dependency.auth import get_current_user +from src.modules.authorization.domain.services.authorization_service import ( + AuthorizationService, +) +from src.modules.authorization.infrastructure.repositories.casbin_policy_repository import ( SQLAlchemyCasbinPolicyRepository, ) -from src.core.authorization.infrastructure.services.casbin_authorization_service import ( +from src.modules.authorization.infrastructure.services.casbin_authorization_service import ( CasbinAuthorizationService, ) -from src.core.database.postgres.session import get_db -from src.core.dependency.auth import get_current_user def get_authorization_service( diff --git a/src/modules/authorization/presenter/routers/permission_router.py b/src/modules/authorization/presentation/routers/permission_router.py similarity index 94% rename from src/modules/authorization/presenter/routers/permission_router.py rename to src/modules/authorization/presentation/routers/permission_router.py index 6ab327c..a238f0c 100644 --- a/src/modules/authorization/presenter/routers/permission_router.py +++ b/src/modules/authorization/presentation/routers/permission_router.py @@ -4,11 +4,14 @@ from fastapi import APIRouter, Depends, HTTPException, Query, status -from src.core.authorization.dependencies import require_permission -from src.core.authorization.infrastructure.services.casbin_authorization_service import ( - CasbinAuthorizationService, +from src.core.database.postgres.session import get_unit_of_work +from src.core.schemas.response import ( + CursorMeta, + CursorPaginatedResponse, + SuccessResponse, ) -from src.core.authorization.permissions import ( +from src.modules.authorization.domain.entities.permission import Permission +from src.modules.authorization.domain.permissions import ( CREATE_ACTION, DELETE_ACTION, PERMISSION_RESOURCE, @@ -16,21 +19,18 @@ UPDATE_ACTION, permission_key, ) -from src.core.database.postgres.session import get_unit_of_work -from src.core.schemas.response import ( - CursorMeta, - CursorPaginatedResponse, - SuccessResponse, +from src.modules.authorization.infrastructure.services.casbin_authorization_service import ( + CasbinAuthorizationService, ) -from src.modules.authorization.domain.entities.permission import Permission -from src.modules.authorization.presenter.dependency import ( +from src.modules.authorization.presentation.dependency import ( get_casbin_authorization_service, + require_permission, ) -from src.modules.authorization.presenter.schema.request import ( +from src.modules.authorization.presentation.schema.request import ( CreatePermissionRequest, UpdatePermissionRequest, ) -from src.modules.authorization.presenter.schema.response import PermissionResponse +from src.modules.authorization.presentation.schema.response import PermissionResponse from src.shared.unit_of_work import UnitOfWork from src.shared.utils.cursor import CursorDirection, decode_cursor, encode_cursor diff --git a/src/modules/authorization/presenter/routers/role_router.py b/src/modules/authorization/presentation/routers/role_router.py similarity index 94% rename from src/modules/authorization/presenter/routers/role_router.py rename to src/modules/authorization/presentation/routers/role_router.py index 80b4581..7a24913 100644 --- a/src/modules/authorization/presenter/routers/role_router.py +++ b/src/modules/authorization/presentation/routers/role_router.py @@ -4,32 +4,32 @@ from fastapi import APIRouter, Depends, HTTPException, Query, status -from src.core.authorization.dependencies import require_permission -from src.core.authorization.infrastructure.services.casbin_authorization_service import ( - CasbinAuthorizationService, +from src.core.database.postgres.session import get_unit_of_work +from src.core.schemas.response import ( + CursorMeta, + CursorPaginatedResponse, + SuccessResponse, ) -from src.core.authorization.permissions import ( +from src.modules.authorization.domain.permissions import ( CREATE_ACTION, DELETE_ACTION, READ_ACTION, ROLE_RESOURCE, UPDATE_ACTION, ) -from src.core.database.postgres.session import get_unit_of_work -from src.core.schemas.response import ( - CursorMeta, - CursorPaginatedResponse, - SuccessResponse, -) from src.modules.authorization.domain.entities.role import Role -from src.modules.authorization.presenter.dependency import ( +from src.modules.authorization.infrastructure.services.casbin_authorization_service import ( + CasbinAuthorizationService, +) +from src.modules.authorization.presentation.dependency import ( get_casbin_authorization_service, + require_permission, ) -from src.modules.authorization.presenter.schema.request import ( +from src.modules.authorization.presentation.schema.request import ( CreateRoleRequest, UpdateRoleRequest, ) -from src.modules.authorization.presenter.schema.response import RoleResponse +from src.modules.authorization.presentation.schema.response import RoleResponse from src.shared.unit_of_work import UnitOfWork from src.shared.utils.cursor import CursorDirection, decode_cursor, encode_cursor diff --git a/src/modules/authorization/presenter/schema/request.py b/src/modules/authorization/presentation/schema/request.py similarity index 100% rename from src/modules/authorization/presenter/schema/request.py rename to src/modules/authorization/presentation/schema/request.py diff --git a/src/modules/authorization/presenter/schema/response.py b/src/modules/authorization/presentation/schema/response.py similarity index 100% rename from src/modules/authorization/presenter/schema/response.py rename to src/modules/authorization/presentation/schema/response.py diff --git a/src/modules/authorization/presenter/dependency.py b/src/modules/authorization/presenter/dependency.py deleted file mode 100644 index d05665f..0000000 --- a/src/modules/authorization/presenter/dependency.py +++ /dev/null @@ -1,24 +0,0 @@ -from fastapi import Depends -from sqlalchemy.ext.asyncio import AsyncSession - -from src.core.authorization.infrastructure.repositories.casbin_policy_repository import ( - SQLAlchemyCasbinPolicyRepository, -) -from src.core.authorization.infrastructure.services.casbin_authorization_service import ( - CasbinAuthorizationService, -) -from src.core.database.postgres.session import get_db - - -def get_casbin_policy_repository( - db: AsyncSession = Depends(get_db), -) -> SQLAlchemyCasbinPolicyRepository: - return SQLAlchemyCasbinPolicyRepository(db=db) - - -def get_casbin_authorization_service( - repository: SQLAlchemyCasbinPolicyRepository = Depends( - get_casbin_policy_repository - ), -) -> CasbinAuthorizationService: - return CasbinAuthorizationService(repository)