feat: add tenant-scoped rule and permission management

This commit is contained in:
wren
2026-05-21 22:03:08 +08:00
parent a2c2bf1969
commit 1f1bccf3b3
193 changed files with 64463 additions and 1771 deletions
@@ -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"),
}