feat: add tenant-scoped rule and permission management
This commit is contained in:
@@ -13,16 +13,90 @@ from fastapi_common.fastapi_common_web.exception.LeauditException import Leaudit
|
||||
|
||||
from fastapi_modules.fastapi_leaudit.domian.vo.auth.loginTokenVo import LoginTokenVO
|
||||
from fastapi_modules.fastapi_leaudit.services.authService import IAuthService
|
||||
from fastapi_modules.fastapi_leaudit.services.impl.tenantResolver import TenantResolver
|
||||
|
||||
|
||||
class AuthServiceImpl(IAuthService):
|
||||
"""认证服务实现。"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.TenantResolver = TenantResolver()
|
||||
self._sso_user_columns_cache: set[str] | None = None
|
||||
|
||||
@staticmethod
|
||||
def _naive_utcnow() -> datetime:
|
||||
"""返回适配 timestamp without time zone 的 UTC 时间。"""
|
||||
return datetime.utcnow()
|
||||
|
||||
async def _get_sso_user_columns(self, session) -> set[str]:
|
||||
"""读取 `sso_users` 实际列,兼容部分环境尚未完成租户字段迁移。"""
|
||||
if self._sso_user_columns_cache is not None:
|
||||
return self._sso_user_columns_cache
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
rows = await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = current_schema()
|
||||
AND table_name = 'sso_users'
|
||||
"""
|
||||
)
|
||||
)
|
||||
self._sso_user_columns_cache = {str(row[0]) for row in rows.fetchall()}
|
||||
return self._sso_user_columns_cache
|
||||
|
||||
@staticmethod
|
||||
def _optional_sso_user_column(columns: set[str], column: str, pg_type: str = "varchar") -> str:
|
||||
"""列存在则直接查询,不存在时回退为同名 NULL 别名。"""
|
||||
if column in columns:
|
||||
return column
|
||||
return f"NULL::{pg_type} AS {column}"
|
||||
|
||||
async def _build_sso_user_select_fields(
|
||||
self,
|
||||
session,
|
||||
*,
|
||||
include_password: bool = False,
|
||||
include_status: bool = False,
|
||||
include_deleted_at: bool = False,
|
||||
include_try_fields: bool = False,
|
||||
) -> str:
|
||||
"""构造兼容旧库结构的 `sso_users` SELECT 字段列表。"""
|
||||
columns = await self._get_sso_user_columns(session)
|
||||
fields = [
|
||||
"id",
|
||||
"sub",
|
||||
"username",
|
||||
"nick_name",
|
||||
"phone_number",
|
||||
"email",
|
||||
"ou_id",
|
||||
"ou_name",
|
||||
"is_leader",
|
||||
]
|
||||
if include_password:
|
||||
fields.append(self._optional_sso_user_column(columns, "password"))
|
||||
if include_status:
|
||||
fields.append(self._optional_sso_user_column(columns, "status", "integer"))
|
||||
if include_deleted_at:
|
||||
fields.append(self._optional_sso_user_column(columns, "deleted_at", "timestamp"))
|
||||
if include_try_fields:
|
||||
fields.append(self._optional_sso_user_column(columns, "try_count", "integer"))
|
||||
fields.append(self._optional_sso_user_column(columns, "try_login_time", "timestamp"))
|
||||
fields.extend(
|
||||
[
|
||||
self._optional_sso_user_column(columns, "area"),
|
||||
self._optional_sso_user_column(columns, "tenant_code"),
|
||||
self._optional_sso_user_column(columns, "tenant_name"),
|
||||
self._optional_sso_user_column(columns, "dep_name"),
|
||||
self._optional_sso_user_column(columns, "dep_short_name"),
|
||||
]
|
||||
)
|
||||
return ", ".join(fields)
|
||||
|
||||
async def PasswordLogin(self, Sub: str, Password: str) -> LoginTokenVO:
|
||||
"""账密登录。
|
||||
|
||||
@@ -33,11 +107,16 @@ class AuthServiceImpl(IAuthService):
|
||||
async with GetAsyncSession() as session:
|
||||
from sqlalchemy import text
|
||||
|
||||
select_fields = await self._build_sso_user_select_fields(
|
||||
session,
|
||||
include_password=True,
|
||||
include_status=True,
|
||||
include_deleted_at=True,
|
||||
include_try_fields=True,
|
||||
)
|
||||
result = await session.execute(
|
||||
text(
|
||||
"SELECT id, sub, username, nick_name, phone_number, email, "
|
||||
"ou_id, ou_name, is_leader, password, status, deleted_at, "
|
||||
"try_count, try_login_time, area, tenant_name, dep_name, dep_short_name "
|
||||
f"SELECT {select_fields} "
|
||||
"FROM sso_users "
|
||||
"WHERE deleted_at IS NULL AND (sub = :identifier OR username = :identifier) "
|
||||
"ORDER BY CASE WHEN sub = :identifier THEN 0 ELSE 1 END, id ASC "
|
||||
@@ -91,11 +170,14 @@ class AuthServiceImpl(IAuthService):
|
||||
async with GetAsyncSession() as session:
|
||||
from sqlalchemy import text
|
||||
|
||||
select_fields = await self._build_sso_user_select_fields(
|
||||
session,
|
||||
include_status=True,
|
||||
include_deleted_at=True,
|
||||
)
|
||||
result = await session.execute(
|
||||
text(
|
||||
"SELECT id, sub, username, nick_name, phone_number, email, "
|
||||
"ou_id, ou_name, is_leader, status, deleted_at, area, "
|
||||
"tenant_name, dep_name, dep_short_name "
|
||||
f"SELECT {select_fields} "
|
||||
"FROM sso_users WHERE sub = :sub"
|
||||
),
|
||||
{"sub": Sub},
|
||||
@@ -149,10 +231,10 @@ class AuthServiceImpl(IAuthService):
|
||||
user_id = created.scalar_one()
|
||||
await self._ensure_default_role(session, user_id)
|
||||
|
||||
select_fields = await self._build_sso_user_select_fields(session)
|
||||
result = await session.execute(
|
||||
text(
|
||||
"SELECT id, sub, username, nick_name, phone_number, email, "
|
||||
"ou_id, ou_name, is_leader, area, tenant_name, dep_name, dep_short_name "
|
||||
f"SELECT {select_fields} "
|
||||
"FROM sso_users WHERE sub = :sub"
|
||||
),
|
||||
{"sub": Sub},
|
||||
@@ -171,11 +253,14 @@ class AuthServiceImpl(IAuthService):
|
||||
async with GetAsyncSession() as session:
|
||||
from sqlalchemy import text
|
||||
|
||||
select_fields = await self._build_sso_user_select_fields(
|
||||
session,
|
||||
include_status=True,
|
||||
include_deleted_at=True,
|
||||
)
|
||||
result = await session.execute(
|
||||
text(
|
||||
"SELECT id, sub, username, nick_name, phone_number, email, "
|
||||
"ou_id, ou_name, is_leader, status, deleted_at, area, "
|
||||
"tenant_name, dep_name, dep_short_name "
|
||||
f"SELECT {select_fields} "
|
||||
"FROM sso_users WHERE id = :uid"
|
||||
),
|
||||
{"uid": UserId},
|
||||
@@ -190,12 +275,30 @@ class AuthServiceImpl(IAuthService):
|
||||
|
||||
await self._ensure_default_role(session, user["id"])
|
||||
identity = await self._loadUserIdentity(session, user["id"])
|
||||
return self._buildUserInfo(user, identity)
|
||||
user_info = self._buildUserInfo(user, identity)
|
||||
tenant_resolution = await self.TenantResolver.ResolveUserContext(
|
||||
Area=user.get("area"),
|
||||
TenantCode=user.get("tenant_code"),
|
||||
TenantName=user.get("tenant_name"),
|
||||
Source="current_user",
|
||||
)
|
||||
user_info["tenant_code"] = tenant_resolution.tenant_code
|
||||
user_info["tenant_name"] = tenant_resolution.tenant_name or user.get("tenant_name")
|
||||
user_info["tenant_type"] = tenant_resolution.tenant_type
|
||||
return user_info
|
||||
|
||||
async def _buildLoginResponse(self, user: dict[str, Any], session) -> LoginTokenVO:
|
||||
"""组装登录响应:查询角色/权限 → 签发 JWT。"""
|
||||
identity = await self._loadUserIdentity(session, user["id"])
|
||||
user_info = self._buildUserInfo(user, identity)
|
||||
tenant_resolution = await self.TenantResolver.ResolveUserContext(
|
||||
Area=user.get("area"),
|
||||
TenantCode=user.get("tenant_code"),
|
||||
TenantName=user.get("tenant_name"),
|
||||
)
|
||||
user_info["tenant_code"] = tenant_resolution.tenant_code
|
||||
user_info["tenant_type"] = tenant_resolution.tenant_type
|
||||
user_info["tenant_name"] = tenant_resolution.tenant_name or user.get("tenant_name")
|
||||
|
||||
tokens = JwtService.generate(
|
||||
userId=user["id"],
|
||||
@@ -206,6 +309,9 @@ class AuthServiceImpl(IAuthService):
|
||||
roles=identity["roles"],
|
||||
permissions=identity["permissions"],
|
||||
area=user.get("area"),
|
||||
tenantCode=tenant_resolution.tenant_code,
|
||||
tenantName=tenant_resolution.tenant_name or user.get("tenant_name"),
|
||||
tenantType=tenant_resolution.tenant_type,
|
||||
userRole=identity["primary_role"],
|
||||
)
|
||||
|
||||
@@ -305,10 +411,12 @@ class AuthServiceImpl(IAuthService):
|
||||
"ou_name": user.get("ou_name"),
|
||||
"is_leader": user.get("is_leader"),
|
||||
"area": user.get("area"),
|
||||
"tenant_code": user.get("tenant_code"),
|
||||
"user_role": identity["primary_role"],
|
||||
"roles": identity["roles"],
|
||||
"permissions": identity["permissions"],
|
||||
"tenant_name": user.get("tenant_name"),
|
||||
"tenant_type": None,
|
||||
"dep_name": user.get("dep_name"),
|
||||
"dep_short_name": user.get("dep_short_name"),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user