本文主要说明一类自托管媒体库(以 Octans 为例)在**身份与访问管理(IAM)**上应先固定哪些原则:全局角色只管系统级能力,媒体库访问另用 R/W 表达;再通过三类库集合与资源范围解析,把列表过滤、详情隐藏、写操作拒绝和播放 / 图片等边角资源收进同一套语义。重点是分层与边界,不是登录配置、迁移步骤或排障命令。

家庭或小团队私有化部署里,「多用户」常被简化成 admin / user 两个开关。真正难的是:谁能建用户、谁能建库、谁能扫库改元数据、海报墙该展示并集还是分库、无权限的详情应返回 403 还是 404。若把这些问题都塞进全局角色,模型会迅速膨胀;若只靠路径字符串临时判断,权限边界又会在每个模块里重写一遍。下面按原则收敛一版可复用口径。

先拆两层:系统角色 ≠ 库访问

IAM 主线最容易犯的错误,是把三件事压成同一层:

概念回答什么不宜混入
全局角色这个人在实例级能做什么对某个媒体库的具体可见范围
库级权限对某个 Library 能读还是能改用户生命周期、系统设置
资源所有权 / 负责人谁对这个库负治理责任「能不能登录」「是不是超级用户」

原则:

  1. 全局角色只表达系统级能力,不直接等于「能看见哪些片」。
  2. 具体媒体库访问范围交给库权限模型,而不是继续增加角色名。
  3. 资源所有权(例如库负责人)挂在资源上,不要扩张成新的全局角色。

认证与授权也要拆开:

  • 认证:当前请求是谁、会话是否仍有效。
  • 授权:这个主体对当前资源范围有没有 Read / Write / Manage

不要用「能登录」替代「能读库」,也不要用「是 Admin」替代「对每个库都可写」。

三种全局角色就够

正式只保留三种全局角色(协议层宜固定大小写约定,例如 Root / Admin / User):

角色定位核心职责
Root实例唯一超级用户系统初始化与恢复、用户生命周期、管理员体系、跨库接管与转交
Admin库管理员创建并治理自己负责的媒体库,向用户分配 / 收回这些库的 R/W
User普通使用者内容读取,以及被授权范围内的内容操作;不参与系统级管理

Root:唯一、兜底、不日常

  • 单实例只能有一个 Root;用户名固定为小写 "root" 一类约定,表达系统根用户
  • 命名刻意避开 owner,避免与后续「库资源所有权」混淆。
  • Root 自带全部系统权限与全部库权限,不必再单独讨论库级 R/W 是否对它生效。
  • 日常建库、扫库、改元数据应由库负责人完成;Root 保留紧急接管、跨 Admin 调整与用户体系,而不是日常操作入口。
  • 可提升 / 降级 / 禁用 AdminUser,但不应把任意用户再提升为第二个 Root

Admin:管库,不管用户

  • 职责收缩为库管理员,不参与用户创建、重置密码、禁用、删除。
  • 可以创建媒体库,并管理自己负责的库;可向 User(以及其他被授权对象)分配或收回这些库的 R/W
  • 可以被额外授予非自己负责库的纯 R/W,但这不会自动变成该库负责人,也没有该库的授权分配权。
  • 不能创建新的 Admin,不能修改 Root

User:全局主体,不是某人下属

  • Root 统一创建与维护,不是某个 Admin 的从属账号。
  • 可以同时持有多个不同 Admin 所管理库上的 R/W
  • 不允许创建媒体库;某个 AdminUser 的影响,仅限于自己管理库的授权分配与收回。

这套三分的目标:Root 保持唯一恢复入口;Admin 与用户管理解耦;User 作为跨库授权的主体。儿童模式、访客模式等产品场景,优先用库权限 + 限制策略表达,而不是再堆全局角色。

库权限只保留 R / W

库级权限正式只保留两类:

权限语义典型能力
R读取与消费内容海报墙、详情、人物、列表、搜索、图片;任务结果查看;播放整体
W会影响库内容、库状态或库相关后台任务扫描、刮削、NFO、重命名、删除

采用 R/W 而不是 r/w/x/m 细碎位的原因:

  • R 边界相对稳定。
  • 现阶段把扫描 / 刮削 / 导出 / 重命名 / 删除拆成多个独立权限位,复杂度明显高于收益。
  • 用户管理、系统设置、管理员体系继续由全局角色表达,不塞进库权限位。

补充原则:

  • 危险操作的「预览」默认跟随真实操作归为 W,不要因为接口只读就当成纯 R
  • 播放进度、播放状态、历史写入虽然会落库,但属于用户自己的使用状态,不因「技术上写了库」就归入库级 W
  • 未来若出现「上传字幕改库内容」「修正媒体资源」等直接改变库内容的能力,再归入 W

一库一责与三类库集合

一库一责

一个媒体库只能有一个当前负责人(允许是 RootAdmin,不允许是 User):

  • 日常创建、编辑、删除库由当前负责人执行。
  • Root 可接管或把负责人从 A 转交给 B。
  • 「当前负责人」≠「被授予该库 R/W 的用户」:前者有治理权与授权分配权,后者只有内容读 / 写能力。
  • 若某用户仍被任何库的 ManagerId 引用,不能直接删除,必须先完成负责人转交。

三类集合

在进入页面规则之前,先区分三类库集合:

集合含义
ReadableLibraries当前用户可读内容的媒体库
WritableLibraries当前用户可做内容操作的媒体库
ManageableLibraries当前用户作为负责人可治理的媒体库

默认理解:

  • Root:三类集合 = 全部媒体库。
  • AdminManageableLibraries = 自己负责的库;ReadableLibraries / WritableLibraries = 负责库 + 别人授予的 R/W
  • User:只有可读 / 可写集合,没有可治理集合。

于是产品语义可以固定为:

内容消费视图  → ReadableLibraries
内容操作视图  → WritableLibraries
库治理视图    → ManageableLibraries
系统管理页面  → 全局角色(Root / 部分 Admin 系统能力)

前端入口宜按能力判断,而不是长期依赖 requiresAdmin 这类纯角色开关:只要某账号对某些库持有 W,就应能进入对应内容操作面;只有库治理页才看 ManageableLibraries

媒体库是真实边界,不是标签视图

原则层对 Library 的定义:它必须是媒体库(有扫描根、有归属),不是虚拟筛选视图或标签集合。

三条硬约束:

  1. 不允许库路径重叠(规范化后的绝对路径视为同一路径)。
  2. 不允许父子目录同时建库(避免扫描范围互相吞掉)。
  3. 不允许同一物理媒体文件同时属于两个库(配置层约束路径,数据层约束文件归属)。

多根目录时,建议在概念上拆成两层:

  • Library:权限边界与唯一负责人。
  • LibraryRoot:实际扫描根目录,全部参与全局路径校验。

内容侧归属则:

  • 逻辑媒体项(MediaItem)归属单个 Library
  • 物理文件(MediaFile)归属单个 LibraryRoot,且必须与所属 MediaItem 的库一致。
  • 不跨库合并同一 TMDB 条目:两个库里的「同一部电影」是两条记录,库边界优先于跨库去重便利。

访问范围:最终结果与解析策略分开

不要假设所有请求都天然对应单个 LibraryId。更稳的做法是:

  1. 先解析请求目标涉及的最终范围
  2. 再在该范围上裁决 Read / Write / Manage

最终范围(授权层消费)

范围含义
Global不绑定具体库,系统级对象
LibrarySet绑定明确库集合;集合大小为 1 即单库,大于 1 即多库

解析策略(范围怎么来的)

策略典型用途
DirectLibrary资源自身直接给出唯一库,如媒体项、库本体
ExplicitLibrarySet任务等显式记录涉及的库集合
DerivedAssociationSet通过关联聚合,如人物、合集、部分文件
FollowParent图片、字幕、播放流等跟随父资源
GlobalByDesign定义上就是系统级对象
AuthorizationContextSet列表 / 筛选从当前用户库能力集合派生

一句话:

  • 最终范围是裁决输入;
  • 解析策略是如何得到该输入。

主线最小映射可以记:

MediaItem     → DirectLibrary        → LibrarySet
MediaFile     → 关联推导             → LibrarySet
Task          → ExplicitLibrarySet   → LibrarySet(可多库)
Image/字幕/播放 → FollowParent        → 继承父资源范围
列表/聚合      → 当前用户能力集合     → 过滤后再统计

多库任务不要靠任务名或路径前缀反推所属库,应在创建时显式记录库集合,否则授权与推送过滤无法稳定。

展示与 HTTP 语义:先过滤,再聚合

所有汇总视图与展示型接口默认同一顺序:

计算当前用户库集合
  → 按集合过滤内容
  → 再聚合 / 排序 / 分页 / 统计

固定规则:

  • 默认展示可见内容并集,并提供库筛选器;不要强制每个库一个独立首页。
  • 所有 total、作品数、合集成员数等统计,只基于当前用户可见数据。
  • 汇总无可见内容时返回空列表,而不是 403
  • 直接访问无权限的详情、图片、字幕、播放资源时返回 404,避免泄露资源存在性。
  • 不跨库合并重复内容;同一影片在两个库中显示为两条,并带库来源标识。
  • 合集 / 人物:过滤后若无任何可见成员 / 作品,则对该用户视为不存在(列表不出、详情 404)。
  • 搜索必须在可见范围内执行,不能「全库搜完再裁剪」,以免侧信道泄露。

写操作与治理动作则不同:

场景常见对外语义
未认证 / 会话失效401
列表 / 聚合不可见空集合或过滤结果
详情 / 受保护资源不可见404
已认证但无写 / 无治理权403(入口层);具体不可见资源仍可 404

图片与播放等边角资源遵循 FollowParent:权限主体是父级媒体项 / 文件,而不是缓存文件名。即使底层图片文件被复用,判断仍以父资源是否在 ReadableLibraries 中为准。播放整体视为 R 的延伸;进度上报不要求库 Write

IAM 主链如何串起来(原则级)

不必记住控制器名,只需记住分流:

请求进入
  → 认证主链(身份 + 会话)
  → 请求上下文(用户、会话、库权限快照)
  → 按接口主体类型分流
       · 当前用户自服务(改自己密码等)
       · 全局系统能力(用户管理等)
       · 库范围请求(内容读 / 写 / 任务)
       · 混合型(先过系统门槛,再对具体库做 Manage)
  → 解析 ResourceScope / QueryScope
  → Read / Write / Manage 裁决
  → 翻译成 HTTP / 实时事件语义

实时层也应共享同一套语义:登录失效与「权限上下文变了但会话仍有效」要分开处理——前者清登录态,后者刷新权限快照。具体传输方案(JWT、刷新令牌、Hub 事件名)属于实现文档,原则层只要求:REST、实时推送与前端状态不要各说一套

明确不在本文范围

以下内容刻意不写,以免把原则文写成运维手册:

  • 初始化密码、环境变量、容器端口与 Compose 步骤(见部署文)。
  • 登录 / 刷新 / 设备会话的协议字段与迁移命令。
  • 控制器、DTO、EF 映射、索引与数据修复 runbook。
  • OAuth / SSO、API Key、MFA 等扩展能力选型细节。
  • 逐步排障命令与日志关键字。

原则收敛的成功标准是:任何人都能回答「谁管用户、谁管库、谁能读、谁能写、看不见时返回什么」,而不是能背出某次部署命令。

小结

  1. 系统角色与库权限分层Root / Admin / User 只管实例级能力;库访问用 R/W
  2. Admin 是库管理员,不是用户管理员User 是全局主体,可跨多个库被授权。
  3. 一库一责 + 三类集合(可读 / 可写 / 可治理)驱动 UI 与 API。
  4. 媒体库是真实边界:路径不重叠、无父子库、文件不跨库归属;不跨库硬合并内容。
  5. 先解析范围再裁决:最终范围与解析策略分离;图片 / 字幕 / 播放跟父资源。
  6. 先过滤再聚合:列表空结果、详情与受保护资源 404、写 / 治理 403,避免存在性泄露。

把这些边界钉住之后,再往下做认证实现、权限服务与前端能力位,才不容易在每个模块各自发明一套「看起来能用」的判断。

相关阅读

同系列还可对照:

来源

本文综合改写自 Octans 用户角色系统原则研究、用户角色与库权限架构,以及身份与访问管理(IAM)架构中的模型与边界口径。实现类字段、接口契约、迁移与排障细节已省略或抽象为原则表述;以目标版本架构文档为准。