feat: 添加飞书用户模块和订阅功能支持 - 新增feishu_users模块用于处理飞书用户身份验证和权限管理 - 新增subscriptions模块用于处理订阅相关功能 - 新增personalization模块用于个性化服务 - 在alembic迁移配置中注册新的模型模块 - 在API路由器中添加feishu_users和subscriptions路由 - 实现事件调度服务的改进,包括错误处理和状态更新优化 - 添加飞书命令处理的权限检查机制 - 实现飞书应用票据事件处理 - 改进审计日志记录功能 ```
193 lines
5.6 KiB
Python
193 lines
5.6 KiB
Python
from typing import Any
|
|
|
|
from fastapi import HTTPException
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.application.feishu.delivery import send_text_if_configured
|
|
from app.application.feishu.results import command_result
|
|
from app.modules.audit.constants import AuditRiskLevel, AuditSource
|
|
from app.modules.audit.schemas import AuditLogCreate
|
|
from app.modules.audit.service import AuditService
|
|
from app.modules.feishu.constants import FeishuCommandName, FeishuReplyType
|
|
from app.modules.feishu.service import FeishuService
|
|
from app.modules.feishu_users.constants import (
|
|
FEISHU_USER_TARGET_TYPE,
|
|
FeishuUserAuditAction,
|
|
FeishuUserRole,
|
|
FeishuUserStatus,
|
|
)
|
|
from app.modules.feishu_users.principal import FeishuMention, FeishuPrincipal
|
|
from app.modules.feishu_users.services import (
|
|
FeishuIdentityService,
|
|
FeishuUserManagementService,
|
|
)
|
|
|
|
ADMIN_COMMAND_TITLE = "飞书用户管理"
|
|
ADMIN_COMMAND_HELP = (
|
|
"用户管理命令必须使用一个真实的飞书 @用户:\n"
|
|
"设为管理员 @用户\n"
|
|
"设为普通用户 @用户\n"
|
|
"停用用户 @用户\n"
|
|
"启用用户 @用户"
|
|
)
|
|
|
|
_ADMIN_COMMANDS: dict[str, tuple[FeishuCommandName, dict[str, str], str]] = {
|
|
"设为管理员": (
|
|
FeishuCommandName.USER_SET_ADMIN,
|
|
{"role": FeishuUserRole.ADMIN},
|
|
"已设为管理员",
|
|
),
|
|
"设为普通用户": (
|
|
FeishuCommandName.USER_SET_USER,
|
|
{"role": FeishuUserRole.USER},
|
|
"已设为普通用户",
|
|
),
|
|
"停用用户": (
|
|
FeishuCommandName.USER_DISABLE,
|
|
{"status": FeishuUserStatus.DISABLED},
|
|
"已停用",
|
|
),
|
|
"启用用户": (
|
|
FeishuCommandName.USER_ENABLE,
|
|
{"status": FeishuUserStatus.ACTIVE},
|
|
"已启用",
|
|
),
|
|
}
|
|
|
|
|
|
def is_admin_command(command_text: str) -> bool:
|
|
return any(command_text.startswith(prefix) for prefix in _ADMIN_COMMANDS)
|
|
|
|
|
|
def handle_admin_command(
|
|
db: Session,
|
|
feishu: FeishuService,
|
|
*,
|
|
raw_text: str,
|
|
command_text: str,
|
|
principal: FeishuPrincipal,
|
|
auto_reply: bool,
|
|
) -> dict[str, Any] | None:
|
|
"""Manage a user selected only from verified structured mention metadata."""
|
|
|
|
matched = next(
|
|
(
|
|
(prefix, definition)
|
|
for prefix, definition in _ADMIN_COMMANDS.items()
|
|
if command_text.startswith(prefix)
|
|
),
|
|
None,
|
|
)
|
|
if matched is None:
|
|
return None
|
|
command_prefix, definition = matched
|
|
command, changes, success_text = definition
|
|
target = _target_mention(raw_text, command_prefix, principal.mentions)
|
|
if target is None or not target.open_id:
|
|
_audit_denied(db, principal, "missing_or_ambiguous_structured_mention")
|
|
return _result(
|
|
feishu,
|
|
command,
|
|
ADMIN_COMMAND_HELP,
|
|
principal,
|
|
auto_reply,
|
|
)
|
|
target_tenant = target.tenant_key or principal.tenant_key
|
|
if target_tenant != principal.tenant_key:
|
|
_audit_denied(db, principal, "cross_tenant_target")
|
|
return _result(
|
|
feishu,
|
|
command,
|
|
"只能管理当前租户内通过飞书 @ 提及的用户。",
|
|
principal,
|
|
auto_reply,
|
|
)
|
|
|
|
target_principal = FeishuIdentityService(db).resolve_or_register(
|
|
tenant_key=target_tenant,
|
|
open_id=target.open_id,
|
|
union_id=target.union_id,
|
|
user_id=target.user_id,
|
|
actor=principal.user_code,
|
|
)
|
|
try:
|
|
updated = FeishuUserManagementService(db).update_user(
|
|
target_principal.user_code,
|
|
changes=changes,
|
|
actor=principal.user_code,
|
|
)
|
|
display_name = target.name or updated.code
|
|
content = f"{display_name} {success_text}。"
|
|
except HTTPException as exc:
|
|
if exc.status_code == 409:
|
|
content = "操作已拒绝:不能停用或降级最后一个有效管理员。"
|
|
else:
|
|
content = "用户状态未修改,请确认目标用户后重试。"
|
|
return _result(feishu, command, content, principal, auto_reply)
|
|
|
|
|
|
def _target_mention(
|
|
raw_text: str,
|
|
command_text: str,
|
|
mentions: tuple[FeishuMention, ...],
|
|
) -> FeishuMention | None:
|
|
command_index = raw_text.find(command_text)
|
|
if command_index >= 0:
|
|
command_tail = raw_text[command_index + len(command_text) :]
|
|
candidates = [
|
|
mention
|
|
for mention in mentions
|
|
if mention.key and mention.key in command_tail
|
|
]
|
|
if len(candidates) == 1:
|
|
return candidates[0]
|
|
return None
|
|
if len(mentions) == 1:
|
|
return mentions[0]
|
|
return None
|
|
|
|
|
|
def _audit_denied(
|
|
db: Session,
|
|
principal: FeishuPrincipal,
|
|
reason: str,
|
|
) -> None:
|
|
AuditService(db).record(
|
|
AuditLogCreate(
|
|
actor=principal.user_code,
|
|
source=AuditSource.FEISHU,
|
|
action=FeishuUserAuditAction.UPDATE_DENIED,
|
|
target_type=FEISHU_USER_TARGET_TYPE,
|
|
risk_level=AuditRiskLevel.HIGH,
|
|
response_payload={"result": "denied", "reason": reason},
|
|
status="denied",
|
|
)
|
|
)
|
|
db.commit()
|
|
|
|
|
|
def _result(
|
|
feishu: FeishuService,
|
|
command: FeishuCommandName,
|
|
content: str,
|
|
principal: FeishuPrincipal,
|
|
auto_reply: bool,
|
|
) -> dict[str, Any]:
|
|
response = (
|
|
send_text_if_configured(
|
|
feishu,
|
|
principal.chat_id,
|
|
content,
|
|
principal.user_code,
|
|
)
|
|
if auto_reply
|
|
else None
|
|
)
|
|
return command_result(
|
|
command,
|
|
FeishuReplyType.TEXT,
|
|
ADMIN_COMMAND_TITLE,
|
|
content,
|
|
response,
|
|
)
|