TT Lab
开始
学习 学习路径 课程

FastAPI — 类型就是契约

有效令牌不代表拥有文档访问权

在 TT Lab 中继续学习

目标

把认证、权限和所有权分开,并用同样的 404 来隐藏资源是否存在。

为什么重要

已登录的用户只是改了地址里的文档编号,就读到了别的团队的文档。令牌有效这件事,与有权读取某份特定文档,是两回事。本实验用固定的令牌字典来代替认证服务器。它不是实现签发令牌或 JWT 签名的实验,而是实现认证结果之后的权限边界的实验。

步骤

  1. 在 /root/work/fa-ownership-lab/service.py 中,bearer(header) 在头部恰好以 'Bearer ' 开头,且后面有一个不含空格的令牌时,返回该令牌。None、空令牌、其他 scheme、多余的空格都是 ValueError。

先做一次准备。已有的文件不会被覆盖。

mkdir -p /root/work/fa-ownership-lab
test -e /root/work/fa-ownership-lab/service.py || cp /opt/fixtures/ten_labs/fa-ownership-lab/service.py /root/work/fa-ownership-lab/service.py
cd /root/work/fa-ownership-lab
  1. 在 /root/work/fa-ownership-lab/service.py 中,principal(token, users) 返回令牌字典中用户的 {id, scopes},但要把 scopes 列表也一并复制。不认识的令牌是 ValueError。

  2. 在 /root/work/fa-ownership-lab/service.py 中,require_scope(user, scope) 在 scopes 中恰好含有 scope 字符串时返回 None,没有时抛出 PermissionError。read-all 不是 read。

  3. 在 /root/work/fa-ownership-lab/service.py 中,visible(user, document) 只有在 document 不是 None,并且 owner 与 user 的 id 完全相同时才返回 True。

  4. 在 /root/work/fa-ownership-lab/service.py 中,public_document(document) 是只含 id 和 title 的新字典。不包含 owner 或 internal_cost。

  5. 在 /root/work/fa-ownership-lab/service.py 中,authenticate(header, users) 把 bearer 和 principal 连接起来。ValueError 要变成 HTTPException(401),并且 headers 中 WWW-Authenticate 的值是 Bearer。

  6. 在 /root/work/fa-ownership-lab/service.py 中,read_document(user, documents, document_id) 在没有 read scope 时是 HTTPException(403),文档不存在或属于别人时是 HTTPException(404),否则就是 public_document 的结果。

  7. 在 /root/work/fa-ownership-lab/service.py 中,create_app(users, documents) 返回一个 FastAPI 应用,它在 GET /documents/{document_id} 中接收 Authorization 头,并调用 authenticate 和 read_document。请用真实请求验证 200、401、403、404 以及非公开字段是否被去除。

参考

分离 Bearer 头

在 /root/work/fa-ownership-lab/service.py 中,bearer(header) 在头部恰好以 'Bearer ' 开头,且后面有一个不含空格的令牌时,返回该令牌。None、空令牌、其他 scheme、多余的空格都是 ValueError。

先做一次准备。已有的文件不会被覆盖。

mkdir -p /root/work/fa-ownership-lab
test -e /root/work/fa-ownership-lab/service.py || cp /opt/fixtures/ten_labs/fa-ownership-lab/service.py /root/work/fa-ownership-lab/service.py
cd /root/work/fa-ownership-lab

如果把头部随意切成多段,就可能把空格错误当成正常令牌接受下来。

保存后用 bash /opt/lab/checks/fa-ownership-lab/01-contract.sh 确认。

复制身份后再返回

在 /root/work/fa-ownership-lab/service.py 中,principal(token, users) 返回令牌字典中用户的 {id, scopes},但要把 scopes 列表也一并复制。不认识的令牌是 ValueError。

如果修改返回的 scopes 会连带改变原用户的权限,不同请求之间的权限就会混在一起。

保存后用 bash /opt/lab/checks/fa-ownership-lab/02-contract.sh 确认。

精确比较权限

在 /root/work/fa-ownership-lab/service.py 中,require_scope(user, scope) 在 scopes 中恰好含有 scope 字符串时返回 None,没有时抛出 PermissionError。read-all 不是 read。

子串比较会把更长的权限名称误认为另一种权限。

保存后用 bash /opt/lab/checks/fa-ownership-lab/03-contract.sh 确认。

单独确认所有权

在 /root/work/fa-ownership-lab/service.py 中,visible(user, document) 只有在 document 不是 None,并且 owner 与 user 的 id 完全相同时才返回 True。

把资源不存在和他人所有合并成同一个判定。

保存后用 bash /opt/lab/checks/fa-ownership-lab/04-contract.sh 确认。

用白名单挑选响应字段

在 /root/work/fa-ownership-lab/service.py 中,public_document(document) 是只含 id 和 title 的新字典。不包含 owner 或 internal_cost。

不要从原件中删除字段,而要组装新的响应。

保存后用 bash /opt/lab/checks/fa-ownership-lab/05-contract.sh 确认。

让错误符合 HTTP 契约

在 /root/work/fa-ownership-lab/service.py 中,authenticate(header, users) 把 bearer 和 principal 连接起来。ValueError 要变成 HTTPException(401),并且 headers 中 WWW-Authenticate 的值是 Bearer。

不要把认证失败和应用错误统统归成一个 500。

保存后用 bash /opt/lab/checks/fa-ownership-lab/06-contract.sh 确认。

固定拒绝的顺序

在 /root/work/fa-ownership-lab/service.py 中,read_document(user, documents, document_id) 在没有 read scope 时是 HTTPException(403),文档不存在或属于别人时是 HTTPException(404),否则就是 public_document 的结果。

即使认证之后,scope 和所有权也必须分别确认。

保存后用 bash /opt/lab/checks/fa-ownership-lab/07-contract.sh 确认。

在真实请求中封住边界

在 /root/work/fa-ownership-lab/service.py 中,create_app(users, documents) 返回一个 FastAPI 应用,它在 GET /documents/{document_id} 中接收 Authorization 头,并调用 authenticate 和 read_document。请用真实请求验证 200、401、403、404 以及非公开字段是否被去除。

即使函数各自都对,如果在路径中漏掉了调用,访问控制就不会生效。

保存后用 bash /opt/lab/checks/fa-ownership-lab/08-contract.sh 确认。