Puter 文件共享权限审计指南:用 puter.fs.getShares() 查看谁能访问你的文件与目录
【免费下载链接】puter🌐 The Internet Computer! Free, Open-Source, and Self-Hostable.项目地址: https://gitcode.com/GitHub_Trending/pu/puter
puter.fs.getShares()是 Puter 文件系统(FS)模块中用于“反向审计共享关系”的核心方法:给定一个你拥有或拥有manage权限的文件或目录,它会返回一份完整的授权清单,告诉你谁能到达它、以什么模式到达、授权来自哪里。本文以 getShares.md 为骨架,结合puter-jsSDK 与后端ShareController的实现细节,完整讲解其语法、返回结构、邀请(pending)语义、继承授权行为,并提供可直接运行的实战示例,帮助你精确掌握 Puter 共享模型的边界与审计能力。
适用前提:谁可以调用、能查到什么
getShares()用于列出“谁能访问某个文件或目录”,调用者必须满足以下条件之一:
- 是该文件或目录的所有者;
- 拥有该对象的
manage权限(例如你被授予了对某个共享文件夹的manage权限)。
只有具备上述身份,你才能读到该对象的共享关系清单。
应用(App)能分享什么:越权是设计上被禁止的
Puter 的共享模型强调“App 永远不会获得超过用户授予它的权限”。文档中明确了 App 可触达的边界:
- App 可以分享自己的 AppData,以及用户明确授予它的文件;
- 分享的权限级别不超过 App 自身持有的级别——例如一个只有
read权限的 App,最多只能再授予read,无法授予write或manage; - 用户拥有但从未交给 App 的文件,App 无法触达;
listShared()(见 listShared.md)对 App 只展示它能触达的那部分共享关系;- App 创建的共享归属于背后的用户,同时携带
issuedByApp标记,便于所有者通过getShares()区分“用户本人创建”与“App 代为创建”。
你能看到的共享由“谁授予”决定
getShares()返回的清单不仅包含你自己授予的共享,还包含任何持有该对象manage权限的人所授予的共享。这正是所有者查看“我信任的人又转授给了谁”的途径。
语法与参数
两种等价调用形式:
puter.fs.getShares(path); puter.fs.getShares(options);从 SDK 源码(operations/getShares.js)看,该操作在defineOperation中声明了位置参数['path'],并在请求构造时二选一写入查询参数:优先使用uid,否则把path交给getAbsolutePathForApp解析为绝对路径,最终请求GET /share/shares?...。
参数说明
| 参数 | 类型 | 必需 | 说明 |
|---|---|---|---|
path | String | 二者必居其一 | 文件或目录的路径。若为相对路径,将相对 App 的根目录解析(SDK 通过getAbsolutePathForApp完成归一化);若在调用位置参数形式时,path为第一个位置参数 |
options.path | String | 仅以 options 作为唯一入参时必需 | 与位置参数语义相同 |
options.uid | String | 二者必居其一 | 以对象的 UID 定位条目,可替代path使用 |
若同时不提供uid与path,后端会直接拒绝。见后端实现 ShareController.ts:GET /share/shares校验后若target为空会抛出400 one of 'uid' or 'path' is required(legacyCode: 'bad_request')。此外,当以路径定位时,后端会用expandTildePath处理~起始的路径。
返回值结构:逐个共享对象的字段
getShares()返回一个Promise,解析为一个共享对象数组。前端映射发生在 shareUtil.js 的toShare函数中,它把服务端下发的行记录归一化为 SDK 公开的Share结构(类型定义见 types.js)。下表字段与该映射一一对应:
| 字段 | 类型 | 含义 |
|---|---|---|
uid | String | 这条共享记录的 UID |
mode | String | 授予的访问模式(read/write/manage等) |
path | String | 被共享条目的路径 |
entryUid | String | 被共享条目本身(目录项)的 UID(服务端字段为uid_entry或entryUid) |
isDir | Boolean | 被共享对象是否为目录(服务端字段为is_dir或isDir) |
issuer | String | null | 这条共享由谁发出 |
holder | String | null | 持有访问权限的人;邀请(pending)共享时为null |
inheritedFrom | String | null | 该访问权限所继承自的“共享祖先”路径;共享落在对象自身时值为null |
issuedByApp | String | null | 请求该共享的 App 的 UID;由用户直接创建时值为null |
modified | Number | 修改时间戳 |
size | Number | null | 大小(用于文件浏览器渲染) |
注:
toShare还会尽力携带name、type、thumbnail、owner等展示字段,原因是共享清单背后没有可 stat 的 fsentry,服务端直接在行记录中携带文件浏览器渲染所需的信息;这些字段在其它场景下可能缺失。当记录处于pending状态(服务端status === 'pending'或pending === true)时,还会追加pending: true与recipientEmail。
issuedByApp:区分“人”与“App”的授权
issuedByApp为 App 的 UID,表示这条共享是某个 App 代替其背后的用户发出的;若由用户直接操作创建,则为null。这一字段让所有者在审计时可以一眼区分两类来源——这正是文档中“App 创建的共享归属用户但携带来源标记”的设计落地。
继承授权与邀请:两个必须理解的行为
inheritedFrom 与“在祖先上托管”
若某条访问权限不是直接落在当前条目上,而是来自其某个共享祖先目录,则该记录的inheritedFrom字段会给出那个祖先的路径;若共享直接落在当前条目自身,则为null。
关键管理约束有两层:
- 当你不是所有者时,
inheritedFrom与path一样会被掩码处理,防止非所有者探测完整的目录结构; - 从父目录继承来的访问权限,托管在那个父目录上——因此你无法在这里(当前条目上)撤除它,因为该授权记录并不存在于当前条目。要收回继承授权,必须到授权实际所在的祖先目录上操作。
invitations:面向未确认邮箱的待认领共享
返回清单同时包含邀请(invitations)——即发送到某个邮箱地址、但该邮箱还没有对应已确认账户的共享:
- 这类记录携带
pending: true; holder为null;- 目标地址存放在
recipientEmail中; - 在收件人确认该邮箱之前,它们不授予任何实际权限;
- 如需在认领前取消,调用
puter.fs.unshare()即可。
错误与隐私行为:不会确认“对象存在”
如果调用者完全看不到该条目(既非所有者也无manage权限),getShares()的拒绝方式与请求一个不存在的文件完全相同——也就是说,该 API 不会通过错误差异来向无权者确认对象是否存在,避免信息泄露。
完整实战示例
示例一:查看谁能访问一个文件
先写入文件、用邮箱发起共享,再列出所有共享并逐条打印持有者、模式与发出者:
<html> <body> <script src="https://js.puter.com/v2/"></script> <script> (async () => { await puter.fs.write('report.txt', 'Quarterly numbers'); await puter.fs.share('report.txt', 'friend@example.com', 'read'); const shares = await puter.fs.getShares('report.txt'); for (const share of shares) { puter.print(`${share.holder}: ${share.mode} (from ${share.issuer})<br>`); } })() </script> </body> </html>输出将包含直接授予friend@example.com的共享;若该邮箱账户未确认,这条记录会以pending形式出现(holder为null、recipientEmail为目标地址)。
示例二:撤销所有人的访问
配合puter.fs.share()与puter.fs.unshare(),实现“一键清理”:
const shares = await puter.fs.getShares('report.txt'); for (const share of shares) { await puter.fs.unshare('report.txt', share.holder); }需要留意:unshare对每个被撤对象的语义在 SDK 中有所体现(见 operations/unshare.js)——它把撤销请求发往POST /share/revoke,并在响应后清空相关条目缓存(item:<path>与readdir:<dirname>,见 shareUtil.js 的 invalidateShareCache)。无法对holder为null的待认领邀请直接用上面的循环撤销,应针对邀请取消场景单独处理。
示例三:按来源过滤——区分人授予与 App 授予
结合issuedByApp做更精细的审计展示:
const shares = await puter.fs.getShares('/Documents/project'); for (const share of shares) { if (share.issuedByApp) { puter.print(`App ${share.issuedByApp} 授予了 ${share.holder} ${share.mode}(由 ${share.issuer} 发出)`); } else { puter.print(`${share.issuer} 直接授予了 ${share.holder} ${share.mode}`); } }底层实现:从 SDK 到后端的一条链路
getShares()的实现与源码证据可以归纳为一条清晰链路:
- SDK 层(operations/getShares.js):声明位置参数
path,用URLSearchParams构造uid或path查询串,向GET /share/shares发起请求,再把响应体{ items: [...] }逐项经toShare映射为Share[]返回。 - 后端路由层(ShareController.ts):
GET /share/shares装饰器声明了subdomain: 'api'、requireVerified: true(要求已验证邮箱)与rateLimit: SHARE_LIST_LIMIT。处理器要求uid与path二选一,随后调用共享服务层的listSharesOf(actor, target),最后同样经统一的#toClientShare转换后返回{ items }信封。 - 服务层:
listSharesOf承担实际查询逻辑,相关测试(如 ShareService.test.ts、ShareConsistency.test.ts)对其按权限遍历与一致性进行了专门验证。
与它对照的两个“宏观视角”接口也值得了解(实现于同一 ShareController.ts):
GET /share/shared-by-me:分页列出调用者分享出去的全部内容,支持appUid过滤到某个 App 或none(用户本人创建);适用于调用者不知道要问哪个条目的场景,而GET /share/shares一次只能针对一个条目;GET /share/shared-by-me/apps:列出持有调用者所建共享的 App 及计数,是查询appUid的入口。
SDK 侧对应的两种“自己视角”能力见 listSharedByMe.md 与 listShared.md。
边界与注意事项小结
- 身份要求:必须是所有者或持有
manage权限,否则按“文件不存在”拒绝; - 相对路径:非绝对路径会相对 App 根目录解析(SDK 层)或处理
~(后端expandTildePath); - 范围覆盖:结果包含所有
manage持有者转授的共享,不只你自己; - 继承 vs 直授:
inheritedFrom标明来源;继承授权在祖先上托管,不能在当前条目撤除; - 邀请语义:
pending: true、holder: null、地址在recipientEmail,认领前不授任何权限,可用unshare()提前取消; - App 边界:App 只能分享自身 AppData 与被用户授予的文件,且授予级别不超过自身所持级别,共享归用户所有并携带
issuedByApp来源标记。
相关 API
puter.fs.share()— 授予访问权限puter.fs.unshare()— 撤回访问权限puter.fs.listSharedByMe()— 分页查看自己分享出去的内容puter.fs.listShared()— 查看共享给我的内容
【免费下载链接】puter🌐 The Internet Computer! Free, Open-Source, and Self-Hostable.项目地址: https://gitcode.com/GitHub_Trending/pu/puter
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考