多 key:OAuth 登录签发附加 key 不再轮换,keys 管理端点
This commit is contained in:
1 parent
aa5ad41eba
commit
7b85b4a4fd
7 files changed
+544
-37
No files matched your search
+112
-14
@@ -577,6 +577,25 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
|
||||
else:
|
||||
self._send_json(404, {"error": "not found"})
|
||||
return
|
||||
if path.startswith("/users/me/keys"):
|
||||
# API keys 管理:全部只操作认证用户自己的 key。
|
||||
if method == "GET" and path == "/users/me/keys":
|
||||
self._user_keys_list()
|
||||
elif method == "POST" and path == "/users/me/keys":
|
||||
self._user_keys_create()
|
||||
elif method == "POST" and \
|
||||
path == "/users/me/keys/revoke-others":
|
||||
self._user_keys_revoke_others()
|
||||
elif method == "DELETE" and \
|
||||
path.startswith("/users/me/keys/"):
|
||||
key_id = path[len("/users/me/keys/"):]
|
||||
if key_id and "/" not in key_id:
|
||||
self._user_keys_revoke(key_id)
|
||||
else:
|
||||
self._send_json(404, {"error": "not found"})
|
||||
else:
|
||||
self._send_json(404, {"error": "not found"})
|
||||
return
|
||||
if path.startswith("/github-app/"):
|
||||
# GitHub App 一键安装:setup/state 与 status 须持本站 key
|
||||
# (在处理器内验);setup 表单页与 callback 靠 setup
|
||||
@@ -1801,9 +1820,10 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
|
||||
账号,返回 is_bind=True、api_key_plain=None(绑定成功页
|
||||
绝不显示任何 key);冲突抛 BindError;
|
||||
2. 身份已绑定到某本站账号 -> 登录落到被绑账号,并按
|
||||
OAuth 登录规矩轮换 key(created=False、rotated=True),
|
||||
与 oauth 账号登录同一语义,不发明新语义;
|
||||
3. 否则走原来的确定性 oauth 账号映射(新建/复用 + 轮换)。
|
||||
OAuth 登录规矩签发附加 key(created=False、rotated
|
||||
恒为 False),与 oauth 账号登录同一语义,不发明新语义;
|
||||
3. 否则走原来的确定性 oauth 账号映射(新建/复用 + 签发
|
||||
附加 key)。
|
||||
"""
|
||||
store = OAuthStateStore(self.server.db_path)
|
||||
try:
|
||||
@@ -1827,19 +1847,20 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
|
||||
return (bind_user_id, False, None, False, identity, True)
|
||||
bound_user_id = find_bound_user(auth, provider, identity)
|
||||
if bound_user_id is not None:
|
||||
api_key_plain = auth.rotate_api_key(bound_user_id)
|
||||
_key_id, api_key_plain = auth.create_api_key(
|
||||
bound_user_id, "oauth:%s" % provider)
|
||||
auth.audit(bound_user_id, "", "oauth_login",
|
||||
"%s:%s" % (provider, identity.get("external_id")))
|
||||
auth.audit(bound_user_id, "", "api_key_rotated", provider)
|
||||
return (bound_user_id, False, api_key_plain, True, identity,
|
||||
auth.audit(bound_user_id, "", "api_key_issued", provider)
|
||||
return (bound_user_id, False, api_key_plain, False, identity,
|
||||
False)
|
||||
local_id, created, api_key_plain, rotated = \
|
||||
map_external_identity_to_local_user_with_key(
|
||||
auth, provider, identity)
|
||||
auth.audit(local_id, "", "oauth_login",
|
||||
"%s:%s" % (provider, identity.get("external_id")))
|
||||
if rotated:
|
||||
auth.audit(local_id, "", "api_key_rotated", provider)
|
||||
if not created:
|
||||
auth.audit(local_id, "", "api_key_issued", provider)
|
||||
return local_id, created, api_key_plain, rotated, identity, False
|
||||
|
||||
def _oauth_callback(self, path):
|
||||
@@ -1880,7 +1901,8 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
|
||||
identity.get("already_bound", False)})
|
||||
return
|
||||
# api_key 明文只在这里出现一次:调用方须立即保存,
|
||||
# 之后任何接口都不再返回它;轮换时旧 key 已失效。
|
||||
# 之后任何接口都不再返回它;登录签发的是附加 key,旧 key
|
||||
# 仍可继续使用(多 key;不用的旧 key 可在 keys 管理里撤销)。
|
||||
self._send_json(200, {"user_id": local_id, "created": created,
|
||||
"provider": provider,
|
||||
"login": identity.get("login"),
|
||||
@@ -1905,10 +1927,18 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
|
||||
def _oauth_success_page(self, provider, local_id, created, login,
|
||||
api_key_plain, rotated):
|
||||
"""GET 回调的成功页:只显示 login、user_id、created 与新 API key
|
||||
一次;绝不显示 access_token、client_secret、旧 API key。"""
|
||||
一次;绝不显示 access_token、client_secret、旧 API key。
|
||||
|
||||
rotated 只为向后兼容保留(登录不再轮换,恒为 False):
|
||||
新 key 是附加 key,旧 key 仍可继续使用,页面不再写“旧 key
|
||||
已失效”,而是说明多 key 与撤销入口。
|
||||
"""
|
||||
rotation_note = (
|
||||
"<p>Your previous API key has been rotated and no longer works.</p>"
|
||||
if rotated else "")
|
||||
if rotated else
|
||||
"<p>Previous keys still work (multi-key). Revoke unused keys "
|
||||
"via the API keys management endpoints.</p>"
|
||||
if not created else "")
|
||||
return (
|
||||
"<!doctype html><html><head><meta charset='utf-8'>"
|
||||
"<title>OAuth login succeeded</title></head><body>"
|
||||
@@ -1922,7 +1952,7 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
|
||||
"<p><code>%s</code></p>"
|
||||
"<p>Please save it immediately. It will not be shown again "
|
||||
"after you leave or refresh this page."
|
||||
"%s</p>"
|
||||
"</p>"
|
||||
"<p>Use it as the X-API-Key header for API calls.</p>"
|
||||
"</body></html>"
|
||||
% (html.escape(str(provider)),
|
||||
@@ -1930,8 +1960,7 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
|
||||
html.escape(str(local_id)),
|
||||
"yes" if created else "no",
|
||||
rotation_note,
|
||||
html.escape(str(api_key_plain)),
|
||||
" Your old key has expired (rotated)." if rotated else ""))
|
||||
html.escape(str(api_key_plain))))
|
||||
|
||||
def _oauth_bind_success_page(self, provider, login):
|
||||
"""绑定成功页:只显示 provider + 外部 login + 成功提示。
|
||||
@@ -2266,6 +2295,75 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
|
||||
conn.close()
|
||||
self._send_json(200, status)
|
||||
|
||||
# -- API keys 管理 ---------------------------------------------------
|
||||
def _user_keys_list(self):
|
||||
"""列出自己的 key:primary(id "legacy")+ 附加 key。
|
||||
|
||||
只返回 id / label / 后 4 位提示 / 创建 / 最近使用 / 撤销
|
||||
时间,绝不返回明文与哈希;只看得到自己的(认证用户)。
|
||||
"""
|
||||
user_id = self._authenticate()
|
||||
if user_id is None:
|
||||
self._send_json(401, {"error": "unauthorized"})
|
||||
return
|
||||
with _auth_store(self.server.db_path) as auth:
|
||||
keys = auth.list_api_keys(user_id)
|
||||
self._send_json(200, {"keys": keys})
|
||||
|
||||
def _user_keys_create(self):
|
||||
"""给自己签发一个附加 key:明文只在本次响应里出现一次。
|
||||
|
||||
body 可含 label(字符串,缺省 "api-key");新 key 立刻
|
||||
可用,与 primary 及已有附加 key 互不影响。
|
||||
"""
|
||||
user_id = self._authenticate()
|
||||
if user_id is None:
|
||||
self._send_json(401, {"error": "unauthorized"})
|
||||
return
|
||||
body, err = self._read_json()
|
||||
if err:
|
||||
body = {}
|
||||
label = (body or {}).get("label")
|
||||
if not isinstance(label, str) or not label:
|
||||
label = "api-key"
|
||||
with _auth_store(self.server.db_path) as auth:
|
||||
key_id, api_key_plain = auth.create_api_key(user_id, label)
|
||||
self._send_json(201, {"id": key_id, "label": label,
|
||||
"hint": api_key_plain[-4:],
|
||||
"api_key": api_key_plain})
|
||||
|
||||
def _user_keys_revoke(self, key_id):
|
||||
"""撤销自己的一个 key:附加 key 记 revoked_at,primary
|
||||
(id "legacy")清哈希;无此 key 或已撤销返回 404。
|
||||
|
||||
撤销自己的最后一个 key 后须重新登录拿新 key(登录总能
|
||||
签发,不会把自己锁死在账号外)。
|
||||
"""
|
||||
user_id = self._authenticate()
|
||||
if user_id is None:
|
||||
self._send_json(401, {"error": "unauthorized"})
|
||||
return
|
||||
with _auth_store(self.server.db_path) as auth:
|
||||
ok = auth.revoke_api_key(user_id, key_id)
|
||||
if not ok:
|
||||
self._send_json(404, {"error": "key not found"})
|
||||
return
|
||||
self._send_json(200, {"revoked": True, "id": key_id})
|
||||
|
||||
def _user_keys_revoke_others(self):
|
||||
"""撤销除本次请求 key 之外的全部自己的 key(附加 + primary),
|
||||
返回撤销数。用于“这台设备之外的 key 全作废”;本次请求
|
||||
的 key 本身不受影响。"""
|
||||
user_id = self._authenticate()
|
||||
if user_id is None:
|
||||
self._send_json(401, {"error": "unauthorized"})
|
||||
return
|
||||
api_key = self.headers.get("X-API-Key") or ""
|
||||
keep_hash = hashlib.sha256(api_key.encode()).hexdigest()
|
||||
with _auth_store(self.server.db_path) as auth:
|
||||
count = auth.revoke_other_api_keys(user_id, keep_hash)
|
||||
self._send_json(200, {"revoked_count": count})
|
||||
|
||||
# -- 推送订阅与分发 ---------------------------------------------------
|
||||
def _push_create_endpoint(self, user_id, project_id):
|
||||
body, err = self._read_json()
|
||||
|
||||
Reference in new issue
Block a user