From bf694690b34851b572f8fee435729a23f8251bc5 Mon Sep 17 00:00:00 2001 From: z30057876 Date: Tue, 2 Dec 2025 15:05:26 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=91=98=E5=88=A4=E6=96=AD=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/dependency/__init__.py | 4 +-- apps/dependency/user.py | 71 +++++++++++++++++-------------------- 2 files changed, 34 insertions(+), 41 deletions(-) diff --git a/apps/dependency/__init__.py b/apps/dependency/__init__.py index a5db652e..65b99734 100644 --- a/apps/dependency/__init__.py +++ b/apps/dependency/__init__.py @@ -2,13 +2,13 @@ """FastAPI 依赖注入模块""" from apps.dependency.user import ( + is_admin, verify_admin, verify_personal_token, - verify_session, ) __all__ = [ + "is_admin", "verify_admin", "verify_personal_token", - "verify_session", ] diff --git a/apps/dependency/user.py b/apps/dependency/user.py index 97692a2e..9b95212e 100644 --- a/apps/dependency/user.py +++ b/apps/dependency/user.py @@ -1,60 +1,53 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. """用户鉴权""" +import grp import logging +import pwd from starlette import status from starlette.exceptions import HTTPException from starlette.requests import HTTPConnection -from apps.common.config import config from apps.services.personal_token import PersonalTokenManager -from apps.services.session import SessionManager from apps.services.user import UserManager logger = logging.getLogger(__name__) -async def verify_session(request: HTTPConnection) -> None: +def is_admin(username: str) -> bool: """ - 验证Session是否已鉴权;作为第一层鉴权检查 + 判断用户是否为管理员 - - 如果Authorization头不存在或不以Bearer开头,抛出401 - - 如果Bearer token以sk-开头,跳过(由verify_personal_token处理) - - 如果Bearer token不以sk-开头,则作为Session ID校验 - - 如果是合法session则设置user_id + 管理员条件: + 1. 用户id为0 (root用户) + 2. 用户在"wheel"组中 - :param request: HTTP请求 - :return: + :param username: Linux用户名 + :return: 如果用户是管理员返回True,否则返回False """ - auth_header = request.headers.get("Authorization") - if not auth_header: - logger.warning("鉴权失败:缺少Authorization头") - raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="鉴权失败") - - if not auth_header.startswith("Bearer "): - logger.warning("鉴权失败:Authorization格式错误,需要Bearer token") - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="鉴权失败:需要Bearer token", - ) - - token = auth_header.split(" ", 1)[1] - - # 如果以sk-开头,说明是Personal Token,跳过由verify_personal_token处理 - if token.startswith("sk-"): - return - - # 作为Session ID校验 - request.state.session_id = token - user_id = await SessionManager.get_user(token) - if not user_id: - logger.warning("Session ID鉴权失败:无效的session_id=%s", token) - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Session ID 鉴权失败", - ) - request.state.user_id = user_id + try: + user_info = pwd.getpwnam(username) + if user_info.pw_uid == 0: + return True + except KeyError: + logger.warning("系统中未找到用户 '%s'", username) + return False + except OSError: + logger.exception("访问用户 %s 的信息时出错", username) + return False + + # 检查是否在wheel组中 + try: + wheel_group = grp.getgrnam("wheel") + if username in wheel_group.gr_mem: + return True + except KeyError: + logger.warning("系统中未找到用户组 'wheel'") + except OSError: + logger.exception("访问用户组 'wheel' 信息时出错") + + return False async def verify_personal_token(request: HTTPConnection) -> None: @@ -109,6 +102,6 @@ async def verify_admin(request: HTTPConnection) -> None: if not user: logger.warning("管理员鉴权失败:用户不存在,user_id=%s", user_id) raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="用户不存在") - if user.userName not in config.login.admin_user: + if not is_admin(user.userName): logger.warning("管理员鉴权失败:用户无管理员权限,user_id=%s", user_id) raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="用户无权限") -- Gitee From eba7a2e8dfb017f37bfdbe54c17668da569efb91 Mon Sep 17 00:00:00 2001 From: z30057876 Date: Tue, 2 Dec 2025 15:05:55 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E5=88=A0=E9=99=A4=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E9=9C=80=E8=A6=81=E7=9A=84Manager?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/services/comment.py | 72 --------------- apps/services/session.py | 74 --------------- apps/services/token.py | 192 --------------------------------------- 3 files changed, 338 deletions(-) delete mode 100644 apps/services/comment.py delete mode 100644 apps/services/session.py delete mode 100644 apps/services/token.py diff --git a/apps/services/comment.py b/apps/services/comment.py deleted file mode 100644 index 17816083..00000000 --- a/apps/services/comment.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. -"""评论 Manager""" - -import logging -import uuid - -from sqlalchemy import select - -from apps.common.postgres import postgres -from apps.models import Comment -from apps.schemas.record import RecordComment - -logger = logging.getLogger(__name__) - - -class CommentManager: - """评论相关操作""" - - @staticmethod - async def query_comment(record_id: str) -> RecordComment | None: - """ - 根据问答ID查询评论 - - :param record_id: 问答ID - :return: 评论内容 - """ - async with postgres.session() as session: - result = ( - await session.scalars( - select(Comment).where(Comment.recordId == uuid.UUID(record_id)), - ) - ).one_or_none() - if result: - return RecordComment( - comment=result.commentType, - dislike_reason=result.feedbackType, - reason_link=result.feedbackLink, - reason_description=result.feedbackContent, - feedback_time=round(result.createdAt.timestamp(), 3), - ) - return None - - @staticmethod - async def update_comment(record_id: str, data: RecordComment, user_id: str) -> None: - """ - 更新评论 - - :param record_id: 问答ID - :param data: 评论内容 - """ - async with postgres.session() as session: - result = ( - await session.scalars( - select(Comment).where(Comment.recordId == uuid.UUID(record_id)), - ) - ).one_or_none() - if result: - result.commentType = data.comment - result.feedbackType = data.feedback_type - result.feedbackLink = data.feedback_link - result.feedbackContent = data.feedback_content - else: - comment_info = Comment( - recordId=uuid.UUID(record_id), - userId=user_id, - commentType=data.comment, - feedbackType=data.feedback_type, - feedbackLink=data.feedback_link, - feedbackContent=data.feedback_content, - ) - session.add(comment_info) - await session.commit() diff --git a/apps/services/session.py b/apps/services/session.py deleted file mode 100644 index 5f672d9a..00000000 --- a/apps/services/session.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. -"""会话 Manager""" - -import logging -from datetime import UTC, datetime, timedelta - -from sqlalchemy import select - -from apps.common.postgres import postgres -from apps.constants import SESSION_TTL -from apps.models import Session, SessionType - -logger = logging.getLogger(__name__) - - -class SessionManager: - """浏览器Session管理""" - - @staticmethod - async def create_session(user_id: str, ip: str) -> str: - """创建浏览器Session""" - if not ip: - err = "用户IP错误!" - raise ValueError(err) - - if not user_id: - err = "用户ID错误!" - raise ValueError(err) - - data = Session( - userId=user_id, - ip=ip, - validUntil=datetime.now(UTC) + timedelta(minutes=SESSION_TTL), - sessionType=SessionType.CODE, - ) - - async with postgres.session() as session: - await session.merge(data) - await session.commit() - - return data.id - - - @staticmethod - async def delete_session(session_id: str) -> None: - """删除浏览器Session""" - if not session_id: - return - async with postgres.session() as session: - session_data = (await session.scalars(select(Session).where(Session.id == session_id))).one_or_none() - if session_data: - await session.delete(session_data) - await session.commit() - - - @staticmethod - async def get_user(session_id: str) -> str | None: - """从Session中获取用户ID""" - async with postgres.session() as session: - user_id = ( - await session.scalars(select(Session.userId).where(Session.id == session_id)) - ).one_or_none() - if not user_id: - return None - - return user_id - - - @staticmethod - async def get_session_by_user(user_id: str) -> str | None: - """根据用户ID获取Session""" - async with postgres.session() as session: - data = await session.scalars(select(Session.id).where(Session.userId == user_id)) - return data.one_or_none() diff --git a/apps/services/token.py b/apps/services/token.py deleted file mode 100644 index e9466058..00000000 --- a/apps/services/token.py +++ /dev/null @@ -1,192 +0,0 @@ -# Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. -"""Token Manager""" - -import logging -import uuid -from datetime import UTC, datetime, timedelta - -import httpx -from fastapi import status -from sqlalchemy import and_, select - -from apps.common.config import config -from apps.common.auth import oidc_provider -from apps.common.postgres import postgres -from apps.constants import OIDC_ACCESS_TOKEN_EXPIRE_TIME -from apps.models import Session, SessionType -from apps.schemas.config import OIDCConfig - -logger = logging.getLogger(__name__) - - -class TokenManager: - """管理用户Token和插件Token""" - - @staticmethod - async def get_plugin_token( - plugin_id: uuid.UUID, - user_id: str, - access_token_url: str, - expire_time: int, - ) -> str: - """获取插件Token""" - if not user_id: - err = "用户不存在!" - raise ValueError(err) - - async with postgres.session() as session: - token_data = ( - await session.scalars( - select(Session) - .where( - and_( - Session.userId == user_id, - Session.sessionType == SessionType.ACCESS_TOKEN, - Session.pluginId == str(plugin_id), - ), - ), - ) - ).one_or_none() - - if token_data and token_data.token: - return token_data.token - - token = await TokenManager.generate_plugin_token( - plugin_id, - user_id, - access_token_url, - expire_time, - ) - if token is None: - err = "Generate plugin token failed" - raise RuntimeError(err) - - await session.merge(Session( - userId=user_id, - sessionType=SessionType.PLUGIN_TOKEN, - pluginId=str(plugin_id), - token=token, - )) - await session.commit() - return token - - - @staticmethod - def _get_login_config() -> OIDCConfig: - """获取并验证登录配置""" - login_config = config.login.settings - if not isinstance(login_config, OIDCConfig): - err = "Authhub OIDC配置错误" - raise TypeError(err) - return login_config - - - @staticmethod - async def generate_plugin_token( - plugin_name: uuid.UUID, - user_id: str, - access_token_url: str, - expire_time: int, - ) -> str | None: - """生成插件Token""" - async with postgres.session() as session: - token_data = ( - await session.scalars( - select(Session) - .where( - and_( - Session.userId == user_id, - Session.sessionType == SessionType.ACCESS_TOKEN, - ), - ), - ) - ).one_or_none() - - if token_data: - oidc_access_token = token_data.token - else: - # 检查是否有refresh token - async with postgres.session() as session: - refresh_token = ( - await session.scalars( - select(Session) - .where( - and_( - Session.userId == user_id, - Session.sessionType == SessionType.REFRESH_TOKEN, - ), - ), - ) - ).one_or_none() - - if not refresh_token or not refresh_token.token: - err = "Refresh token均过期,需要重新登录" - raise RuntimeError(err) - - # access token 过期的时候,重新获取 - oidc_config = TokenManager._get_login_config() - try: - token_info = await oidc_provider.get_oidc_token(refresh_token.token) - oidc_access_token = token_info["access_token"] - - # 更新OIDC token - async with postgres.session() as session: - await session.merge(Session( - userId=user_id, - sessionType=SessionType.ACCESS_TOKEN, - token=oidc_access_token, - validUntil=datetime.now(UTC) + timedelta(minutes=OIDC_ACCESS_TOKEN_EXPIRE_TIME), - )) - await session.commit() - except Exception: - logger.exception("[TokenManager] 获取OIDC Access token 失败") - return None - - async with httpx.AsyncClient() as client: - response = await client.post( - url=access_token_url, - json={ - "client_id": oidc_config.app_id, - "access_token": oidc_access_token, - }, - timeout=10.0, - ) - ret = response.json() - if response.status_code != status.HTTP_200_OK: - logger.error("[TokenManager] 获取 %s 插件所需的token失败", plugin_name) - return None - - # 保存插件token - async with postgres.session() as session: - await session.merge(Session( - userId=user_id, - sessionType=SessionType.ACCESS_TOKEN, - pluginId=str(plugin_name), - token=ret["access_token"], - validUntil=datetime.now(UTC) + timedelta(minutes=expire_time), - )) - await session.commit() - - return ret["access_token"] - - - @staticmethod - async def delete_plugin_token(user_id: str) -> None: - """删除插件token(使用PostgreSQL)""" - async with postgres.session() as session: - token_data = (await session.scalars( - select(Session) - .where( - and_( - Session.userId == user_id, - Session.sessionType.in_([ - SessionType.ACCESS_TOKEN, - SessionType.REFRESH_TOKEN, - SessionType.PLUGIN_TOKEN, - ]), - ), - ), - )).all() - for token in token_data: - await session.delete(token) - await session.commit() -- Gitee From 45d3bfda60086b23d1f2cd94b072767f885a42b8 Mon Sep 17 00:00:00 2001 From: z30057876 Date: Tue, 2 Dec 2025 15:06:26 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/services/personal_token.py | 2 +- apps/services/record.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/services/personal_token.py b/apps/services/personal_token.py index c56c6657..178437b3 100644 --- a/apps/services/personal_token.py +++ b/apps/services/personal_token.py @@ -50,7 +50,7 @@ class PersonalTokenManager: try: async with postgres.session() as session: await session.execute( - update(User).where(User.id == user_id).values(personal_token=personal_token_with_prefix), + update(User).where(User.id == user_id).values(personalToken=personal_token_with_prefix), ) await session.commit() except Exception: diff --git a/apps/services/record.py b/apps/services/record.py index 3e069cab..7a52939e 100644 --- a/apps/services/record.py +++ b/apps/services/record.py @@ -10,7 +10,7 @@ from sqlalchemy import and_, select from apps.common.postgres import postgres from apps.common.security import Security -from apps.models import CommentType, Conversation +from apps.models import Conversation from apps.models import Record as PgRecord from apps.models import RecordMetadata as PgRecordMetadata from apps.schemas.record import RecordContent, RecordData, RecordMetadata @@ -165,7 +165,6 @@ class RecordManager: content=record_content, createdAt=pg_record.createdAt.timestamp(), metadata=RecordMetadata(), - comment=CommentType.NONE, ) records.append(record) return records -- Gitee