功能:新增仓库与客户现场数据权限
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
from sqlalchemy import delete, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from module_business.entity.do.master_do import BizUserSite, BizUserWarehouse
|
||||
|
||||
|
||||
class AccessDao:
|
||||
"""仓库与客户现场数据权限数据库操作层。"""
|
||||
|
||||
@classmethod
|
||||
async def user_has_warehouse(cls, db: AsyncSession, user_id: int, warehouse_id: int) -> bool:
|
||||
"""判断用户是否关联指定仓库。"""
|
||||
mapped_user_id = (
|
||||
await db.execute(
|
||||
select(BizUserWarehouse.user_id).where(
|
||||
BizUserWarehouse.user_id == user_id,
|
||||
BizUserWarehouse.warehouse_id == warehouse_id,
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
|
||||
return mapped_user_id is not None
|
||||
|
||||
@classmethod
|
||||
async def user_has_site(cls, db: AsyncSession, user_id: int, site_id: int) -> bool:
|
||||
"""判断用户是否关联指定客户现场。"""
|
||||
mapped_user_id = (
|
||||
await db.execute(
|
||||
select(BizUserSite.user_id).where(
|
||||
BizUserSite.user_id == user_id,
|
||||
BizUserSite.site_id == site_id,
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
|
||||
return mapped_user_id is not None
|
||||
|
||||
@classmethod
|
||||
async def list_user_warehouse_ids(cls, db: AsyncSession, user_id: int) -> list[int]:
|
||||
"""获取用户关联的仓库 ID 列表。"""
|
||||
result = await db.execute(select(BizUserWarehouse.warehouse_id).where(BizUserWarehouse.user_id == user_id))
|
||||
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
async def list_user_site_ids(cls, db: AsyncSession, user_id: int) -> list[int]:
|
||||
"""获取用户关联的客户现场 ID 列表。"""
|
||||
result = await db.execute(select(BizUserSite.site_id).where(BizUserSite.user_id == user_id))
|
||||
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
async def replace_warehouse_users(cls, db: AsyncSession, warehouse_id: int, user_ids: list[int]) -> None:
|
||||
"""完整替换指定仓库的用户关联,仅刷新当前事务。"""
|
||||
await db.execute(delete(BizUserWarehouse).where(BizUserWarehouse.warehouse_id == warehouse_id))
|
||||
db.add_all([BizUserWarehouse(user_id=user_id, warehouse_id=warehouse_id) for user_id in user_ids])
|
||||
await db.flush()
|
||||
|
||||
@classmethod
|
||||
async def replace_site_users(cls, db: AsyncSession, site_id: int, user_ids: list[int]) -> None:
|
||||
"""完整替换指定客户现场的用户关联,仅刷新当前事务。"""
|
||||
await db.execute(delete(BizUserSite).where(BizUserSite.site_id == site_id))
|
||||
db.add_all([BizUserSite(user_id=user_id, site_id=site_id) for user_id in user_ids])
|
||||
await db.flush()
|
||||
@@ -0,0 +1,35 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from exceptions.exception import PermissionException
|
||||
from module_admin.entity.vo.user_vo import CurrentUserModel
|
||||
from module_business.dao.access_dao import AccessDao
|
||||
|
||||
|
||||
class AccessService:
|
||||
"""仓库与客户现场数据权限服务层。"""
|
||||
|
||||
@classmethod
|
||||
async def assert_warehouse_access(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
current_user: CurrentUserModel,
|
||||
warehouse_id: int,
|
||||
) -> None:
|
||||
"""校验当前用户是否有权访问指定仓库。"""
|
||||
if current_user.user.admin:
|
||||
return
|
||||
if not await AccessDao.user_has_warehouse(db, current_user.user.user_id, warehouse_id):
|
||||
raise PermissionException(data='', message='无权访问该仓库')
|
||||
|
||||
@classmethod
|
||||
async def assert_site_access(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
current_user: CurrentUserModel,
|
||||
site_id: int,
|
||||
) -> None:
|
||||
"""校验当前用户是否有权访问指定客户现场。"""
|
||||
if current_user.user.admin:
|
||||
return
|
||||
if not await AccessDao.user_has_site(db, current_user.user.user_id, site_id):
|
||||
raise PermissionException(data='', message='无权访问该客户现场')
|
||||
@@ -0,0 +1,197 @@
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import InstrumentedAttribute
|
||||
from sqlalchemy.sql import Select
|
||||
|
||||
from exceptions.exception import PermissionException
|
||||
from module_business.dao.access_dao import AccessDao
|
||||
from module_business.entity.do.master_do import BizUserSite, BizUserWarehouse
|
||||
from module_business.service.access_service import AccessService
|
||||
|
||||
|
||||
class AccessServiceTest(unittest.IsolatedAsyncioTestCase):
|
||||
@patch('module_business.service.access_service.AccessDao.user_has_warehouse', new_callable=AsyncMock)
|
||||
@patch('module_business.service.access_service.AccessDao.user_has_site', new_callable=AsyncMock)
|
||||
async def test_admin_bypasses_mapping(self, user_has_site: AsyncMock, user_has_warehouse: AsyncMock) -> None:
|
||||
user = SimpleNamespace(user=SimpleNamespace(user_id=1, admin=True))
|
||||
db = AsyncMock(spec=AsyncSession)
|
||||
|
||||
await AccessService.assert_warehouse_access(db, user, 99)
|
||||
await AccessService.assert_site_access(db, user, 7)
|
||||
|
||||
user_has_warehouse.assert_not_awaited()
|
||||
user_has_site.assert_not_awaited()
|
||||
|
||||
@patch('module_business.service.access_service.AccessDao.user_has_warehouse', new_callable=AsyncMock)
|
||||
async def test_mapped_warehouse_is_allowed(self, user_has_warehouse: AsyncMock) -> None:
|
||||
user_has_warehouse.return_value = True
|
||||
user = SimpleNamespace(user=SimpleNamespace(user_id=8, admin=False))
|
||||
db = AsyncMock(spec=AsyncSession)
|
||||
|
||||
await AccessService.assert_warehouse_access(db, user, 99)
|
||||
|
||||
user_has_warehouse.assert_awaited_once_with(db, 8, 99)
|
||||
|
||||
@patch('module_business.service.access_service.AccessDao.user_has_warehouse', new_callable=AsyncMock)
|
||||
async def test_unmapped_warehouse_is_forbidden(self, user_has_warehouse: AsyncMock) -> None:
|
||||
user_has_warehouse.return_value = False
|
||||
user = SimpleNamespace(user=SimpleNamespace(user_id=8, admin=False))
|
||||
|
||||
with self.assertRaises(PermissionException) as raised:
|
||||
await AccessService.assert_warehouse_access(AsyncMock(spec=AsyncSession), user, 99)
|
||||
|
||||
self.assertEqual('', raised.exception.data)
|
||||
self.assertEqual('无权访问该仓库', raised.exception.message)
|
||||
|
||||
@patch('module_business.service.access_service.AccessDao.user_has_site', new_callable=AsyncMock)
|
||||
async def test_mapped_site_is_allowed(self, user_has_site: AsyncMock) -> None:
|
||||
user_has_site.return_value = True
|
||||
user = SimpleNamespace(user=SimpleNamespace(user_id=8, admin=False))
|
||||
db = AsyncMock(spec=AsyncSession)
|
||||
|
||||
await AccessService.assert_site_access(db, user, 7)
|
||||
|
||||
user_has_site.assert_awaited_once_with(db, 8, 7)
|
||||
|
||||
@patch('module_business.service.access_service.AccessDao.user_has_site', new_callable=AsyncMock)
|
||||
async def test_unmapped_site_is_forbidden(self, user_has_site: AsyncMock) -> None:
|
||||
user_has_site.return_value = False
|
||||
user = SimpleNamespace(user=SimpleNamespace(user_id=8, admin=False))
|
||||
|
||||
with self.assertRaises(PermissionException) as raised:
|
||||
await AccessService.assert_site_access(AsyncMock(spec=AsyncSession), user, 7)
|
||||
|
||||
self.assertEqual('', raised.exception.data)
|
||||
self.assertEqual('无权访问该客户现场', raised.exception.message)
|
||||
|
||||
|
||||
class AccessDaoTest(unittest.IsolatedAsyncioTestCase):
|
||||
@staticmethod
|
||||
def _db_with_result(result: MagicMock) -> AsyncMock:
|
||||
db = AsyncMock(spec=AsyncSession)
|
||||
db.execute.return_value = result
|
||||
return db
|
||||
|
||||
def _assert_statement_targets(
|
||||
self,
|
||||
statement: Select[tuple[int]],
|
||||
model: type[BizUserWarehouse] | type[BizUserSite],
|
||||
selected_column: InstrumentedAttribute[int],
|
||||
expected_params: set[int],
|
||||
) -> None:
|
||||
self.assertTrue(statement.selected_columns[0].compare(selected_column.property.columns[0]))
|
||||
self.assertTrue(statement.get_final_froms()[0].compare(model.__table__))
|
||||
self.assertEqual(expected_params, set(statement.compile().params.values()))
|
||||
|
||||
async def test_user_has_warehouse_returns_mapping_existence(self) -> None:
|
||||
for scalar, expected in ((8, True), (None, False)):
|
||||
with self.subTest(scalar=scalar):
|
||||
result = MagicMock()
|
||||
result.scalar_one_or_none.return_value = scalar
|
||||
db = self._db_with_result(result)
|
||||
|
||||
actual = await AccessDao.user_has_warehouse(db, user_id=8, warehouse_id=99)
|
||||
|
||||
self.assertIs(expected, actual)
|
||||
statement = db.execute.await_args.args[0]
|
||||
self._assert_statement_targets(
|
||||
statement,
|
||||
BizUserWarehouse,
|
||||
BizUserWarehouse.user_id,
|
||||
{8, 99},
|
||||
)
|
||||
|
||||
async def test_user_has_site_returns_mapping_existence(self) -> None:
|
||||
for scalar, expected in ((8, True), (None, False)):
|
||||
with self.subTest(scalar=scalar):
|
||||
result = MagicMock()
|
||||
result.scalar_one_or_none.return_value = scalar
|
||||
db = self._db_with_result(result)
|
||||
|
||||
actual = await AccessDao.user_has_site(db, user_id=8, site_id=7)
|
||||
|
||||
self.assertIs(expected, actual)
|
||||
statement = db.execute.await_args.args[0]
|
||||
self._assert_statement_targets(statement, BizUserSite, BizUserSite.user_id, {7, 8})
|
||||
|
||||
async def test_list_user_warehouse_ids_returns_scalar_ids(self) -> None:
|
||||
result = MagicMock()
|
||||
result.scalars.return_value.all.return_value = (11, 13)
|
||||
db = self._db_with_result(result)
|
||||
|
||||
warehouse_ids = await AccessDao.list_user_warehouse_ids(db, user_id=8)
|
||||
|
||||
self.assertEqual([11, 13], warehouse_ids)
|
||||
statement = db.execute.await_args.args[0]
|
||||
self._assert_statement_targets(statement, BizUserWarehouse, BizUserWarehouse.warehouse_id, {8})
|
||||
|
||||
async def test_list_user_site_ids_returns_scalar_ids(self) -> None:
|
||||
result = MagicMock()
|
||||
result.scalars.return_value.all.return_value = (3, 5)
|
||||
db = self._db_with_result(result)
|
||||
|
||||
site_ids = await AccessDao.list_user_site_ids(db, user_id=8)
|
||||
|
||||
self.assertEqual([3, 5], site_ids)
|
||||
statement = db.execute.await_args.args[0]
|
||||
self._assert_statement_targets(statement, BizUserSite, BizUserSite.site_id, {8})
|
||||
|
||||
async def test_replace_warehouse_users_replaces_complete_mapping(self) -> None:
|
||||
db = AsyncMock(spec=AsyncSession)
|
||||
|
||||
await AccessDao.replace_warehouse_users(db, warehouse_id=99, user_ids=[8, 9])
|
||||
|
||||
delete_statement = db.execute.await_args.args[0]
|
||||
self.assertTrue(delete_statement.table.compare(BizUserWarehouse.__table__))
|
||||
self.assertTrue(delete_statement.whereclause.left.compare(BizUserWarehouse.warehouse_id.property.columns[0]))
|
||||
self.assertEqual({99}, set(delete_statement.compile().params.values()))
|
||||
mappings = db.add_all.call_args.args[0]
|
||||
self.assertEqual([(8, 99), (9, 99)], [(item.user_id, item.warehouse_id) for item in mappings])
|
||||
self.assertTrue(all(isinstance(item, BizUserWarehouse) for item in mappings))
|
||||
self._assert_flush_without_transaction_completion(db)
|
||||
|
||||
async def test_replace_warehouse_users_supports_empty_list(self) -> None:
|
||||
db = AsyncMock(spec=AsyncSession)
|
||||
|
||||
await AccessDao.replace_warehouse_users(db, warehouse_id=99, user_ids=[])
|
||||
|
||||
delete_statement = db.execute.await_args.args[0]
|
||||
self.assertTrue(delete_statement.table.compare(BizUserWarehouse.__table__))
|
||||
self.assertTrue(delete_statement.whereclause.left.compare(BizUserWarehouse.warehouse_id.property.columns[0]))
|
||||
self.assertEqual({99}, set(delete_statement.compile().params.values()))
|
||||
db.add_all.assert_called_once_with([])
|
||||
self._assert_flush_without_transaction_completion(db)
|
||||
|
||||
async def test_replace_site_users_replaces_complete_mapping(self) -> None:
|
||||
db = AsyncMock(spec=AsyncSession)
|
||||
|
||||
await AccessDao.replace_site_users(db, site_id=7, user_ids=[8, 9])
|
||||
|
||||
delete_statement = db.execute.await_args.args[0]
|
||||
self.assertTrue(delete_statement.table.compare(BizUserSite.__table__))
|
||||
self.assertTrue(delete_statement.whereclause.left.compare(BizUserSite.site_id.property.columns[0]))
|
||||
self.assertEqual({7}, set(delete_statement.compile().params.values()))
|
||||
mappings = db.add_all.call_args.args[0]
|
||||
self.assertEqual([(8, 7), (9, 7)], [(item.user_id, item.site_id) for item in mappings])
|
||||
self.assertTrue(all(isinstance(item, BizUserSite) for item in mappings))
|
||||
self._assert_flush_without_transaction_completion(db)
|
||||
|
||||
async def test_replace_site_users_supports_empty_list(self) -> None:
|
||||
db = AsyncMock(spec=AsyncSession)
|
||||
|
||||
await AccessDao.replace_site_users(db, site_id=7, user_ids=[])
|
||||
|
||||
delete_statement = db.execute.await_args.args[0]
|
||||
self.assertTrue(delete_statement.table.compare(BizUserSite.__table__))
|
||||
self.assertTrue(delete_statement.whereclause.left.compare(BizUserSite.site_id.property.columns[0]))
|
||||
self.assertEqual({7}, set(delete_statement.compile().params.values()))
|
||||
db.add_all.assert_called_once_with([])
|
||||
self._assert_flush_without_transaction_completion(db)
|
||||
|
||||
def _assert_flush_without_transaction_completion(self, db: AsyncMock) -> None:
|
||||
db.flush.assert_awaited_once_with()
|
||||
db.commit.assert_not_awaited()
|
||||
db.rollback.assert_not_awaited()
|
||||
Reference in New Issue
Block a user