cloud: production control plane (API, auth/RBAC, queue, sandbox, memory/KB, fallback/cost, webhooks)

This commit is contained in:
host committed 2026-10-26 01:23:21 +08:00
1 parent b97c34465f
commit b74d99e959
17 files changed
+3420

No files matched your search

+178
View File
@@ -0,0 +1,178 @@
"""认证与权限模块(云端架构,首版)。
认证方式:首版只用 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
]