本文主要说明一类自托管媒体库(以 Octans 为例)在**身份与访问管理(IAM)**上应先固定哪些原则:全局角色只管系统级能力,媒体库访问另用 R/W 表达;再通过三类库集合与资源范围解析,把列表过滤、详情隐藏、写操作拒绝和播放 / 图片等边角资源收进同一套语义。重点是分层与边界,不是登录配置、迁移步骤或排障命令。
家庭或小团队私有化部署里,「多用户」常被简化成 admin / user 两个开关。真正难的是:谁能建用户、谁能建库、谁能扫库改元数据、海报墙该展示并集还是分库、无权限的详情应返回 403 还是 404。若把这些问题都塞进全局角色,模型会迅速膨胀;若只靠路径字符串临时判断,权限边界又会在每个模块里重写一遍。下面按原则收敛一版可复用口径。
先拆两层:系统角色 ≠ 库访问
IAM 主线最容易犯的错误,是把三件事压成同一层:
| 概念 | 回答什么 | 不宜混入 |
|---|---|---|
| 全局角色 | 这个人在实例级能做什么 | 对某个媒体库的具体可见范围 |
| 库级权限 | 对某个 Library 能读还是能改 | 用户生命周期、系统设置 |
| 资源所有权 / 负责人 | 谁对这个库负治理责任 | 「能不能登录」「是不是超级用户」 |
原则:
- 全局角色只表达系统级能力,不直接等于「能看见哪些片」。
- 具体媒体库访问范围交给库权限模型,而不是继续增加角色名。
- 资源所有权(例如库负责人)挂在资源上,不要扩张成新的全局角色。
认证与授权也要拆开:
- 认证:当前请求是谁、会话是否仍有效。
- 授权:这个主体对当前资源范围有没有
Read/Write/Manage。
不要用「能登录」替代「能读库」,也不要用「是 Admin」替代「对每个库都可写」。
三种全局角色就够
正式只保留三种全局角色(协议层宜固定大小写约定,例如 Root / Admin / User):
| 角色 | 定位 | 核心职责 |
|---|---|---|
Root | 实例唯一超级用户 | 系统初始化与恢复、用户生命周期、管理员体系、跨库接管与转交 |
Admin | 库管理员 | 创建并治理自己负责的媒体库,向用户分配 / 收回这些库的 R/W |
User | 普通使用者 | 内容读取,以及被授权范围内的内容操作;不参与系统级管理 |
Root:唯一、兜底、不日常
- 单实例只能有一个
Root;用户名固定为小写"root"一类约定,表达系统根用户。 - 命名刻意避开
owner,避免与后续「库资源所有权」混淆。 Root自带全部系统权限与全部库权限,不必再单独讨论库级R/W是否对它生效。- 日常建库、扫库、改元数据应由库负责人完成;
Root保留紧急接管、跨Admin调整与用户体系,而不是日常操作入口。 - 可提升 / 降级 / 禁用
Admin与User,但不应把任意用户再提升为第二个Root。
Admin:管库,不管用户
- 职责收缩为库管理员,不参与用户创建、重置密码、禁用、删除。
- 可以创建媒体库,并管理自己负责的库;可向
User(以及其他被授权对象)分配或收回这些库的R/W。 - 可以被额外授予非自己负责库的纯
R/W,但这不会自动变成该库负责人,也没有该库的授权分配权。 - 不能创建新的
Admin,不能修改Root。
User:全局主体,不是某人下属
- 由
Root统一创建与维护,不是某个Admin的从属账号。 - 可以同时持有多个不同
Admin所管理库上的R/W。 - 不允许创建媒体库;某个
Admin对User的影响,仅限于自己管理库的授权分配与收回。
这套三分的目标:Root 保持唯一恢复入口;Admin 与用户管理解耦;User 作为跨库授权的主体。儿童模式、访客模式等产品场景,优先用库权限 + 限制策略表达,而不是再堆全局角色。
库权限只保留 R / W
库级权限正式只保留两类:
| 权限 | 语义 | 典型能力 |
|---|---|---|
R | 读取与消费内容 | 海报墙、详情、人物、列表、搜索、图片;任务结果查看;播放整体 |
W | 会影响库内容、库状态或库相关后台任务 | 扫描、刮削、NFO、重命名、删除 |
采用 R/W 而不是 r/w/x/m 细碎位的原因:
R边界相对稳定。- 现阶段把扫描 / 刮削 / 导出 / 重命名 / 删除拆成多个独立权限位,复杂度明显高于收益。
- 用户管理、系统设置、管理员体系继续由全局角色表达,不塞进库权限位。
补充原则:
- 危险操作的「预览」默认跟随真实操作归为
W,不要因为接口只读就当成纯R。 - 播放进度、播放状态、历史写入虽然会落库,但属于用户自己的使用状态,不因「技术上写了库」就归入库级
W。 - 未来若出现「上传字幕改库内容」「修正媒体资源」等直接改变库内容的能力,再归入
W。
一库一责与三类库集合
一库一责
一个媒体库只能有一个当前负责人(允许是 Root 或 Admin,不允许是 User):
- 日常创建、编辑、删除库由当前负责人执行。
Root可接管或把负责人从 A 转交给 B。- 「当前负责人」≠「被授予该库
R/W的用户」:前者有治理权与授权分配权,后者只有内容读 / 写能力。 - 若某用户仍被任何库的
ManagerId引用,不能直接删除,必须先完成负责人转交。
三类集合
在进入页面规则之前,先区分三类库集合:
| 集合 | 含义 |
|---|---|
ReadableLibraries | 当前用户可读内容的媒体库 |
WritableLibraries | 当前用户可做内容操作的媒体库 |
ManageableLibraries | 当前用户作为负责人可治理的媒体库 |
默认理解:
Root:三类集合 = 全部媒体库。Admin:ManageableLibraries= 自己负责的库;ReadableLibraries/WritableLibraries= 负责库 + 别人授予的R/W。User:只有可读 / 可写集合,没有可治理集合。
于是产品语义可以固定为:
内容消费视图 → ReadableLibraries
内容操作视图 → WritableLibraries
库治理视图 → ManageableLibraries
系统管理页面 → 全局角色(Root / 部分 Admin 系统能力)
前端入口宜按能力判断,而不是长期依赖 requiresAdmin 这类纯角色开关:只要某账号对某些库持有 W,就应能进入对应内容操作面;只有库治理页才看 ManageableLibraries。
媒体库是真实边界,不是标签视图
原则层对 Library 的定义:它必须是媒体库(有扫描根、有归属),不是虚拟筛选视图或标签集合。
三条硬约束:
- 不允许库路径重叠(规范化后的绝对路径视为同一路径)。
- 不允许父子目录同时建库(避免扫描范围互相吞掉)。
- 不允许同一物理媒体文件同时属于两个库(配置层约束路径,数据层约束文件归属)。
多根目录时,建议在概念上拆成两层:
Library:权限边界与唯一负责人。LibraryRoot:实际扫描根目录,全部参与全局路径校验。
内容侧归属则:
- 逻辑媒体项(
MediaItem)归属单个Library。 - 物理文件(
MediaFile)归属单个LibraryRoot,且必须与所属MediaItem的库一致。 - 不跨库合并同一 TMDB 条目:两个库里的「同一部电影」是两条记录,库边界优先于跨库去重便利。
访问范围:最终结果与解析策略分开
不要假设所有请求都天然对应单个 LibraryId。更稳的做法是:
- 先解析请求目标涉及的最终范围;
- 再在该范围上裁决
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 等扩展能力选型细节。
- 逐步排障命令与日志关键字。
原则收敛的成功标准是:任何人都能回答「谁管用户、谁管库、谁能读、谁能写、看不见时返回什么」,而不是能背出某次部署命令。
小结
- 系统角色与库权限分层:
Root / Admin / User只管实例级能力;库访问用R/W。 - Admin 是库管理员,不是用户管理员;
User是全局主体,可跨多个库被授权。 - 一库一责 + 三类集合(可读 / 可写 / 可治理)驱动 UI 与 API。
- 媒体库是真实边界:路径不重叠、无父子库、文件不跨库归属;不跨库硬合并内容。
- 先解析范围再裁决:最终范围与解析策略分离;图片 / 字幕 / 播放跟父资源。
- 先过滤再聚合:列表空结果、详情与受保护资源
404、写 / 治理403,避免存在性泄露。
把这些边界钉住之后,再往下做认证实现、权限服务与前端能力位,才不容易在每个模块各自发明一套「看起来能用」的判断。
相关阅读
同系列还可对照:
- Octans Docker 私有化部署:单镜像、Compose 叠加与健康检查:多用户实例落地时的运行形态、卷与健康检查;本文角色模型与部署入口衔接,但不替代部署步骤。
来源
本文综合改写自 Octans 用户角色系统原则研究、用户角色与库权限架构,以及身份与访问管理(IAM)架构中的模型与边界口径。实现类字段、接口契约、迁移与排障细节已省略或抽象为原则表述;以目标版本架构文档为准。