cloud: production control plane (API, auth/RBAC, queue, sandbox, memory/KB, fallback/cost, webhooks)
This commit is contained in:
1 parent
b97c34465f
commit
b74d99e959
17 files changed
+3420
No files matched your search
+178
@@ -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
|
||||
]
|
||||
Reference in new issue
Block a user