feat: SSL 证书管家 —— 后台面板 + 专属角色 + 国内机放行
集成在 api.200181.xyz(同一个 Worker),国内机 writeapi.usj.cc 同步可用。 新增第三档后台角色 'ssl': - 只拿证书管家钥匙,看不到评论/文章/用户等模块 - 判定正着枚举放行(canManageSSL = admin|ssl),不用排除法, 免得以后新增角色静默获得私钥权限 - 鉴权只认 Bearer 会话,绝不走 isAdminRequest —— 后者有 Artalk 老客户端的 query 兜底,一旦进来的就是 TLS 私钥 数据(复用 RSS_KV,前缀 certkeeper:): - 凭据一条一键,避开 KV 读-改-写无事务导致的并发丢数据 - 私钥/AK-SK 一律 AES-GCM 密文(TOKEN_SECRET 经 PBKDF2 派生) - 列表接口只回显前 4 后 4 位,明文不进内存 - 配置读失败抛错而非返回空,避免一次保存覆盖线上配置 只读监控: - probeTls 走 cloudflare:sockets 拿证书正文,实现到期分级 与「库里记录 vs 线上实测」对比(match/mismatch/live-only/unreachable) - 到期提醒邮件(HTML 已转义) 国内机 editor-api: - identify 识别 ssl 角色;业务分支前白名单,ssl 只能碰 /health、 /admin/session、/admin/logout 与 /api/v2/ssl* - /api/v2/ssl* 反代放行 admin + ssl(RSS 仍是 admin-only) - posts.mjs 越权兜底方向修正:owns() 从「非 editor 即放行」改为 「只有 admin 不受限」,漏进来的 ssl 被当受限编辑而非管理员全放行 - createPost 显式拒绝非写作角色 验证:typecheck ✓ / selftest:ssl 117 项 ✓ / 浏览器 37 项 ✓
This commit is contained in:
1 parent
ca2fb7163d
commit
a40a92f526
16 files changed
+2791
-59
No files matched your search
@@ -123,12 +123,19 @@ export function makePosts(cfg) {
|
||||
// ------------------------------------------------------------ 归属判定
|
||||
//
|
||||
// viewer = { uid, name, role } —— 由 server.mjs 从请求头/会话里解出来。
|
||||
// admin 不受限;editor 只能碰自己的。
|
||||
// 三档角色(与 Cloudflare 侧 users.role 一一对应):
|
||||
// admin 管理员 —— 不受限
|
||||
// editor 编辑 —— 只能碰自己写的文章
|
||||
// ssl SSL 管理员 —— ★ 压根不该出现在这里:文章相关的路由入口处
|
||||
// 已经按角色拦掉了(见 server.mjs)。万一漏了,
|
||||
// 下面的判定会把它当 editor 处理 —— 「只看得见自己
|
||||
// 名下的东西」,而不是当 admin 全量放行。
|
||||
// 判定的写法是「谁受限」而不是「谁不受限」,就是为了这一刻的兜底方向正确。
|
||||
// viewer 为 null/undefined = 内部调用(不做限制),只用于本文件内部互相调用。
|
||||
|
||||
function owns(item, viewer) {
|
||||
if (!item || !viewer) return true;
|
||||
if (viewer.role !== 'editor') return true; // 管理员(或未知角色走不到这儿)
|
||||
if (viewer.role === 'admin') return true; // 只有管理员不受限
|
||||
const id = String(item.authorId || '');
|
||||
if (id) return id === String(viewer.uid);
|
||||
// 老文章的兜底:没有 author_id 时按昵称认领(改造前 write-server 写的那些)
|
||||
@@ -142,7 +149,7 @@ export function makePosts(cfg) {
|
||||
|
||||
/** 编辑自己在仓库里的文章目录(git pathspec),管理员返回 null 表示「全量」 */
|
||||
function ownedPaths(viewer) {
|
||||
if (!viewer || viewer.role !== 'editor') return null;
|
||||
if (!viewer || viewer.role === 'admin') return null;
|
||||
return findPostDirs()
|
||||
.filter((dir) => owns(toListItem(dir), viewer))
|
||||
.map(relOf);
|
||||
@@ -151,8 +158,9 @@ export function makePosts(cfg) {
|
||||
function listPosts({ q = '', page = 1, perPage = 20 } = {}, viewer) {
|
||||
let rows = findPostDirs().map(toListItem);
|
||||
|
||||
// 编辑角色:列表只显示自己写的(连「别人有几篇」都不让它知道)
|
||||
if (viewer && viewer.role === 'editor') rows = rows.filter((r) => owns(r, viewer));
|
||||
// 编辑角色(以及任何非管理员):列表只显示自己写的
|
||||
// (连「别人有几篇」都不让它知道)
|
||||
if (viewer && viewer.role !== 'admin') rows = rows.filter((r) => owns(r, viewer));
|
||||
|
||||
// slug 撞名告警:Hugo 是 /:slug,撞名意味着线上必有一篇被覆盖掉
|
||||
const seen = new Map();
|
||||
@@ -374,6 +382,13 @@ export function makePosts(cfg) {
|
||||
}
|
||||
|
||||
function createPost({ frontMatter = {}, content = '' } = {}, viewer) {
|
||||
// ★ 只有管理员和编辑能新建。SSL 管理员没有写作权限 —— 它在 server.mjs
|
||||
// 的路由层就该被拦住;这里再卡一道,是因为「新建」会在仓库里落文件,
|
||||
// 一旦漏放不是「看不到」而是「写进去了」,后果比读取严重得多。
|
||||
if (viewer && viewer.role !== 'admin' && viewer.role !== 'editor') {
|
||||
throw bad('当前角色没有创建文章的权限', 403);
|
||||
}
|
||||
|
||||
const title = String(frontMatter.title || '').trim();
|
||||
if (!title) throw bad('缺少标题');
|
||||
const slug = String(frontMatter.slug || '').trim() || makeSlug();
|
||||
|
||||
Reference in new issue
Block a user