"""认证与权限模块(云端架构,首版)。 认证方式:首版只用 API key + 哈希。 - 创建用户时用 ``secrets.token_urlsafe(32)`` 生成明文 key,仅返回给调用方一次; 库里只存 ``hashlib.sha256(api_key_plain.encode()).hexdigest()``,绝不存明文。 - 认证时对输入做同样的 sha256 再查库比对。 OAuth(GitHub/Google)是下一步接线:本模块不提供任何 OAuth 流程、 不编造假 OAuth 端点或假 token 校验,接线时再由控制面按真实 OAuth 协议接入,本模块的 API key 认证保持独立可用。 权限模型:项目内角色 viewer < contributor < maintainer < owner。 动作最低角色:read -> viewer,create_task -> contributor, approve_merge -> maintainer,manage_project -> owner(仅 owner)。 未加入项目一律无权限。 审计:audit() 只追加,list_audit() 只读;本模块不提供删除或修改 审计记录的方法。 零第三方依赖,只用 Python 标准库。 """ import hashlib import secrets import sqlite3 import time import uuid ROLES = ["viewer", "contributor", "maintainer", "owner"] _ROLE_LEVEL = {role: i for i, role in enumerate(ROLES)} _ACTION_MIN_ROLE = { "read": "viewer", "create_task": "contributor", "approve_merge": "maintainer", "manage_project": "owner", } class AuthStore: """基于 SQLite 的用户 / 项目成员 / 审计存储。""" def __init__(self, db_path): self.db_path = str(db_path) self.conn = sqlite3.connect(self.db_path) self.conn.execute( "CREATE TABLE IF NOT EXISTS users(" "id TEXT PRIMARY KEY, " "username TEXT UNIQUE, " "api_key_hash TEXT, " "created_at REAL)" ) self.conn.execute( "CREATE TABLE IF NOT EXISTS project_members(" "project_id TEXT, " "user_id TEXT, " "role TEXT, " "PRIMARY KEY(project_id, user_id))" ) self.conn.execute( "CREATE TABLE IF NOT EXISTS audit_log(" "id INTEGER PRIMARY KEY AUTOINCREMENT, " "user_id TEXT, " "project_id TEXT, " "action TEXT, " "detail TEXT, " "created_at REAL)" ) self.conn.commit() def create_user(self, username): """创建用户,返回 (user_id, api_key_plain),明文仅返回这一次。""" user_id = uuid.uuid4().hex api_key_plain = secrets.token_urlsafe(32) api_key_hash = hashlib.sha256(api_key_plain.encode()).hexdigest() self.conn.execute( "INSERT INTO users(id, username, api_key_hash, created_at)" " VALUES(?, ?, ?, ?)", (user_id, username, api_key_hash, time.time()), ) self.conn.commit() return user_id, api_key_plain def authenticate(self, api_key): """用 API key 认证,成功返回 user_id,失败返回 None。""" if not api_key: return None api_key_hash = hashlib.sha256(api_key.encode()).hexdigest() row = self.conn.execute( "SELECT id FROM users WHERE api_key_hash = ?", (api_key_hash,) ).fetchone() return row[0] if row else None def create_project(self, project_id, owner_user_id): """创建项目成员记录:指定用户为该项目的 owner。""" self.conn.execute( "INSERT OR REPLACE INTO project_members(project_id, user_id, role)" " VALUES(?, ?, 'owner')", (project_id, owner_user_id), ) self.conn.commit() def _member_role(self, project_id, user_id): row = self.conn.execute( "SELECT role FROM project_members" " WHERE project_id = ? AND user_id = ?", (project_id, user_id), ).fetchone() return row[0] if row else None def set_member(self, project_id, actor_user_id, target_user_id, role): """改成员角色:只有 owner 能改,否则 raise PermissionError;同时记审计。""" if role not in ROLES: raise ValueError("unknown role: %r" % (role,)) if self._member_role(project_id, actor_user_id) != "owner": raise PermissionError( "only project owner can change members" ) self.conn.execute( "INSERT OR REPLACE INTO project_members(project_id, user_id, role)" " VALUES(?, ?, ?)", (project_id, target_user_id, role), ) self.conn.commit() self.audit( actor_user_id, project_id, "set_member", "%s:%s" % (target_user_id, role), ) def check_permission(self, user_id, project_id, action): """检查权限:满足最低角色返回 True,否则 False(含未加入项目)。""" min_role = _ACTION_MIN_ROLE.get(action) if min_role is None: return False role = self._member_role(project_id, user_id) if role is None: return False return _ROLE_LEVEL[role] >= _ROLE_LEVEL[min_role] def require_permission(self, user_id, project_id, action): """不满足权限时 raise PermissionError。""" if not self.check_permission(user_id, project_id, action): raise PermissionError( "user %s lacks %s on project %s" % (user_id, action, project_id) ) def audit(self, user_id, project_id, action, detail=""): """只追加一条审计记录。""" self.conn.execute( "INSERT INTO audit_log(user_id, project_id, action, detail, created_at)" " VALUES(?, ?, ?, ?, ?)", (user_id, project_id, action, detail, time.time()), ) self.conn.commit() def list_audit(self, project_id, limit=100): """按 created_at 倒序返回该项目的审计列表(dict)。""" rows = self.conn.execute( "SELECT id, user_id, project_id, action, detail, created_at" " FROM audit_log WHERE project_id = ?" " ORDER BY created_at DESC, id DESC LIMIT ?", (project_id, limit), ).fetchall() return [ { "id": row[0], "user_id": row[1], "project_id": row[2], "action": row[3], "detail": row[4], "created_at": row[5], } for row in rows ]