Skip to content
bitzorcas
中EN

Guide

Tickets 授权、租户与查询

说明 Tickets 的端点授权、DataScope、参与人和 Sharing 合并、可信租户、统一分页、Query Shape 排序及列表数据边界。

Last updated

Tickets 有两层授权。生成端点把 IAuthorizedRequest.Resource/Action 交给统一管道;Handler 内的 TicketAuthorizationService 再合并 Tenant、显式拒绝、DataScope、请求人、直接/用户组受理关系和实时 Sharing。详情、列表、全局搜索、文件关联和 Reporting 导出都应复用 owner 的同一可见性规则。

1. 权限与 Feature

Ticket 主资源使用 ResourceDescriptor("tickets", "ticket")。主要动作如下:

场景Action/权限
打开工单Create / tickets.ticket.create
查看详情与列表View / tickets.ticket.view
更新状态、评论、附件Update / tickets.ticket.update
分派Assign / tickets.ticket.assign
本地 Search 判定Search / tickets.ticket.search
批量操作tickets.ticket.batch
项目、看板、迭代各自 .view / .manage

运行时 FeaturePolicyEvaluator 把模块名 tickets 映射到中央开关 platform.tickets。owner catalog 另有默认关闭的 tickets.manage;两者不是同一个键,不能把目录声明当作运行时模块开关。

2. 记录级可见性

TicketAuthorizationService 先拒绝跨租户和统一决策的显式 Deny。未被显式拒绝时,下列任一条件可授予读取:

  • support-agent/operator;
  • 请求人;
  • 直接 Assignee;
  • 当前用户有效用户组键与 AssigneeId 相同;
  • View 决策为 Allow,且 DataScope 精确为 Tenant;
  • Authorization owner 的实时 Sharing 至少为 Read。

Update 同样接受参与人、support、Tenant DataScope 或 Write Sharing;Assign 接受 support、Tenant DataScope 或 Write Sharing。导出后台不能借 System 身份扩大范围,ITicketResourceAuthorizationReader 会按原请求人的稳定主体键重新读取有效主体、权限和 Sharing。

调用者详情普通列表说明
support/operator允许Tenant 全量直查仍受显式 Deny 和可信 Tenant 限制
Tenant DataScope 的 View 主体允许Tenant 全量直查必须是明确 Allow + Tenant
requester允许参与人合并后可见不再靠强制 RequesterId 实现
直接 assignee允许参与人合并后可见列表与详情已对齐
用户组 assignee 的有效成员允许参与人合并后可见通过 TicketAssignmentSubjectResolver 解析
只获 Read Sharing允许与参与人结果合并撤销后下一次查询立即不可见
同租户非参与人且无 Sharing拒绝/不可见从结果中裁剪明确 Deny 优先于参与关系和 Sharing
跨租户拒绝Store 谓词隔离客户端不能提交 TenantId

3. 列表执行路径

QUERY /api/tickets 的 Handler 先运行 EnsureCanSearchAsync,再判断 CanViewAllAsync:

是否

QUERY /api/tickets

从 CurrentUser 固定 TenantId

EnsureCanSearchAsync

support 或 View+Tenant DataScope?

Query Shape 直接返回目标页

按 200 条读取候选页

合并 requester / assignee / group / Sharing

在可见序列上计算目标页与 TotalCount

非全量主体不会把客户端 RequesterId 改成自己。所有业务筛选先在数据库应用,再以固定 200 条候选批次扫描匹配集合,逐条合并参与关系和 Sharing,最后在“可见序列”上分页。因此返回的 TotalCount 是可见记录总数,而不是候选总数。

这条路径保证了授权正确性,但可能产生多次 Store 查询、主体解析和 Sharing 批量决策。候选 Query Shape 使用 PageWindow 的默认最大 offset 100000;候选集合超过该深度时,后续候选页会为空,当前 Handler 随即结束扫描。大租户需要把 DataScope/Sharing 转为可下推谓词或授权索引,否则 TotalCount 可能被 100000 深度边界截断。

QUERY /api/tickets/todo 是另一条路径:服务端解析当前用户和有效用户组键,强制 OpenOnly=true,按这些 AssigneeIds 查询,不接受客户端指定他人主体。

4. 统一分页语义

Tickets 列表、Todo、项目、看板、迭代以及 Store Query Shape 均使用 PagingLimits:

输入归一化结果
PageIndex ≤01
PageSize ≤020
PageSize 1..1000原值
PageSize >10001000

这与旧的“Handler 上限 200、Store 上限 100”不同。越界不会返回 Validation,而是由 Handler/Store 防御性归一。非全量列表的内部候选批次仍固定为 200,这是授权扫描边界,不是公开 PageSize 上限。

按状态、受理人和关键词查询
QUERY /api/tickets HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9
Content-Type: application/json
{
"status": "Assigned",
"assigneeId": "agent-9",
"searchText": "login",
"pageIndex": 1,
"pageSize": 50,
"projectId": "project-1",
"sprintId": "sprint-1"
}

不支持 QUERY 的客户端使用生成的 POST /api/tickets/_query;不要改用 GET。具体 HTTP Query 兼容规则见统一列表查询契约。

5. Query Shape、排序与筛选

Ticket Query Shape 读取 Id、TenantId、Subject、Description、Status/Priority、Assignee/Requester、Project/Sprint 和审计时间。谓词始终包含可信 TenantId 与 !IsDeleted,并可附加:

  • Status 或 OpenOnly 状态集合;
  • 单 AssigneeId 或服务端解析的 AssigneeIds;
  • RequesterId;
  • Subject/Description Contains 的 SearchText;
  • ProjectId、SprintId。

默认排序是 (ModifyTime desc, CreateTime desc),没有 TicketId 尾键。时间完全相同或分页期间更新时仍可能重复/遗漏;稳定 offset 分页至少应再加 Id,频繁变化的大列表宜使用 cursor。

Subject/Description Contains 没有 Tickets 专属全文索引。SearchText 在 Query Shape 中会 trim,但当前请求规则没有明确最大长度、最短长度或控制字符限制,需要通过输入规则和限流补齐。

6. 详情读取与信息隐藏

GetTicket 使用 FindAsync(currentTenant, ticketId);谓词包含 TenantId、Id 和 !IsDeleted。跨租户、缺失和软删除统一返回 NotFound,只有读到同租户记录后才执行 owner 授权。这避免向其他租户暴露 ID 是否存在。

同租户非参与人可能收到 Forbidden,因此同租户范围内仍可区分“不存在”和“存在但无权”。若产品要求更强的存在性隐藏,应在公开边界统一为 NotFound。

详情当前直接返回 Ticket 聚合,而不是专用 DTO。应以 HTTP 契约测试锁定公开 JSON,避免新增聚合属性自动进入 API。

7. 列表数据最小化

TicketSummary 包含完整 Description。Description 可能含日志、邮箱、设备或个人信息,把它放入每行会扩大暴露面和传输量。建议拆分:

  • list item:Subject、状态、优先级、受理人、时间和短摘要;
  • detail:完整 Description、评论和附件;
  • search projection:只返回授权后的命中片段。

单租户调用通常也不需要回传 TenantId。字段调整必须通过显式 API version,不应跟随聚合自然增长。

8. 模拟登录与租户快照

Tickets 显式使用 currentUser.User.TenantId,持久化层还有 EffectiveTenant filter。模拟登录期间,两者必须指向同一目标租户。应在请求开始时捕获一次 EffectiveTenant,并贯穿授权、查询、写入、通知、搜索和审计。

客户端不能通过查询体覆盖 TenantId;Host bypass 也不能与显式 home tenant 混用。

9. Update 动作仍然较宽

Requester、Assignee 或 Write Sharing 获得 Update 后,可调用 Start、Resolve、Close、Reopen、评论和附件。当前没有“客户只能评论/重开、坐席才能解决/关闭”的细分策略,也没有按状态限制评论与附件。

若产品需要职责分离,应引入 comment、attach、progress、resolve、close、reopen 等 owner-local 权限,并为每个动作保留参与关系/Sharing 条件。

10. 必测契约

  1. 匿名、错误权限、显式 Deny、Tenant DataScope 与跨租户;
  2. requester、直接 assignee、用户组 assignee、support/operator;
  3. Read Sharing 授予与撤销,Write Sharing 不能被错误降级;
  4. 非全量列表在参与人和 Sharing 混合后的 PageIndex、PageSize、TotalCount;
  5. PageSize 0、500、1001,以及内部候选 200 与 offset 100000 边界;
  6. 项目、迭代、状态、搜索筛选不会在授权合并时丢失;
  7. 同时间排序、并发更新和软删除;
  8. 模拟登录下显式 Tenant 与 ambient filter 一致;
  9. 详情、全局搜索、文件关联和 Reporting 导出在 Sharing 撤销后同步失败关闭;
  10. 实际 HTTP 的 QUERY/POST fallback、401/403 和响应字段。

11. 审查命令

Terminal window
# 权限、Feature、DataScope、参与关系和 Sharing 必须一起审查。
rg -n "TicketPermissions|platform.tickets|CanViewAllAsync|FilterVisibleAsync|DataScope|Sharing" \
src/Platform/Tickets src/Framework/BitzOrcas.Application/Authorization -g '*.cs'
# 公开分页与内部授权候选批次是两个边界。
rg -n "PagingLimits|CandidatePageSize|PageWindow|ExecutePageAsync" \
src/Platform/Tickets src/Framework -g '*.cs'
# 稳定排序、全文筛选和最小投影。
rg -n "ReadModelSort|SearchText|Description|TicketListRow" \
src/Platform/Tickets/BitzOrcas.Platform.Tickets.Infrastructure -g '*.cs'

返回 Tickets 总览 · 生命周期与分派 · 评论与附件

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%