Skip to content
bitzorcas
中EN

Guide

Tickets Authorization, Tenancy, and Queries

Explain endpoint authorization, DataScope, participant and Sharing merge, trusted tenancy, standard paging, Query Shape ordering, and Ticket list-data boundaries.

Last updated

Tickets has two authorization layers. Generated endpoints send IAuthorizedRequest.Resource/Action through the shared pipeline. Handlers then use TicketAuthorizationService to combine tenant identity, explicit denial, DataScope, requester, direct/group assignment, and live Sharing. Detail, list, global search, linked files, and Reporting export should all reuse the owner’s visibility decision.

1. Permissions and Feature

The Ticket resource uses ResourceDescriptor("tickets", "ticket").

ScenarioAction/permission
openCreate / tickets.ticket.create
detail and listView / tickets.ticket.view
state, comment, attachmentUpdate / tickets.ticket.update
assignmentAssign / tickets.ticket.assign
local search decisionSearch / tickets.ticket.search
batch operationstickets.ticket.batch
project, board, sprinttheir own .view / .manage permissions

At runtime, FeaturePolicyEvaluator maps module tickets to the central platform.tickets switch. The owner catalog also declares disabled-by-default tickets.manage. These are separate keys; a catalog definition is not the runtime module switch.

2. Record visibility

TicketAuthorizationService rejects cross-tenant calls and explicit Deny decisions first. Otherwise any of the following can grant read access:

  • support-agent/operator;
  • requester;
  • direct Assignee;
  • membership in the effective user-group key stored as AssigneeId;
  • an allowed View decision whose DataScope is exactly Tenant;
  • live Sharing from the Authorization owner at Read or higher.

Update similarly accepts a participant, support, Tenant DataScope, or Write Sharing. Assign accepts support, Tenant DataScope, or Write Sharing. A background export cannot use its System identity to widen access: ITicketResourceAuthorizationReader reloads the original user’s active subjects, permissions, and Sharing.

CallerDetailOrdinary listNote
support/operatoralloweddirect tenant-wide pagestill subject to explicit Deny and trusted tenant
View with Tenant DataScopealloweddirect tenant-wide pagerequires explicit Allow plus Tenant scope
requesterallowedincluded in visibility mergeno requester override is used now
direct assigneeallowedincluded in visibility mergedetail and discovery are aligned
effective member of group assigneeallowedincluded in visibility mergeresolved by TicketAssignmentSubjectResolver
Read Sharing onlyallowedmerged with participant rowsdisappears on the next query after revocation
same-tenant nonparticipantdenied/hiddenfiltered outexplicit Deny outranks relation and Sharing
cross-tenantdeniedstore predicate isolatesno client TenantId input exists

3. List execution path

The handler for QUERY /api/tickets runs EnsureCanSearchAsync, then CanViewAllAsync:

yesno

QUERY /api/tickets

Fix TenantId from CurrentUser

EnsureCanSearchAsync

support or View+Tenant DataScope?

Query Shape returns requested page

read candidate pages of 200

merge requester / assignee / group / Sharing

page the visible sequence and calculate TotalCount

A restricted caller’s client RequesterId is no longer replaced with their own ID. The database first applies all business filters. The handler then scans matching records in candidate batches of 200, merges participant and Sharing visibility, and pages the visible sequence. TotalCount therefore means visible records, not raw candidates.

This is authorization-correct but can cause multiple store calls, subject resolution, and Sharing decisions. Candidate Query Shapes use PageWindow with the default maximum offset 100000. Beyond that depth the next candidate page is empty and the handler ends its scan. Large tenants need a push-down DataScope/Sharing predicate or authorization index; otherwise visible TotalCount can be truncated by that boundary.

QUERY /api/tickets/todo follows a different path. The server resolves the current user and effective user-group keys, forces OpenOnly=true, and queries those AssigneeIds. It never accepts another subject from the client.

4. Standard paging semantics

Ticket list, Todo, project, board, sprint, and their Query Shapes use PagingLimits:

InputNormalized result
PageIndex ≤01
PageSize ≤020
PageSize 1..1000unchanged
PageSize >10001000

This replaces the old split behavior of Handler max 200 and Store max 100. Out-of-range values are normalized rather than returned as Validation failures. The restricted-list candidate batch remains 200; that is an internal authorization scan boundary, not the public PageSize maximum.

Query by state, assignee, and text
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"
}

Clients that cannot send QUERY use generated POST /api/tickets/_query; do not switch to GET. See the standard list-query contract.

5. Query Shape, ordering, and filters

The Ticket Query Shape reads Id, TenantId, Subject, Description, status/priority, assignee/requester, project/sprint, and audit timestamps. Its predicate always contains trusted TenantId and !IsDeleted, plus optional:

  • Status or the OpenOnly status set;
  • one AssigneeId or server-resolved AssigneeIds;
  • RequesterId;
  • Subject/Description Contains for SearchText;
  • ProjectId and SprintId.

Default ordering is (ModifyTime descending, CreateTime descending) with no TicketId tail key. Identical timestamps or concurrent updates can still duplicate/omit rows across pages. Add Id for deterministic offset paging or use a cursor on frequently changing lists.

Subject/Description Contains has no Ticket-specific full-text index. Query Shape trims SearchText, but current request rules do not establish a maximum/minimum length or control-character policy. Add an input rule and rate limit before treating this as a large-scale search surface.

6. Detail reads and existence hiding

GetTicket calls FindAsync(currentTenant, ticketId) with TenantId, Id, and !IsDeleted. Missing, deleted, and cross-tenant IDs all return NotFound. Owner authorization runs only for a loaded same-tenant row, so another tenant cannot probe existence.

A same-tenant nonparticipant may receive Forbidden, which still distinguishes existing from missing within the tenant. If the product requires stronger hiding, map both to NotFound at the public boundary.

Detail currently returns the Ticket aggregate rather than a response DTO. Freeze actual HTTP JSON in contract tests so a new public aggregate property does not silently become API surface.

7. List-data minimization

TicketSummary includes the full Description. Descriptions can hold logs, email, device data, or personal details, so including them in every row increases exposure and payload size. Prefer:

  • list item: Subject, state, priority, assignee, times, and short summary;
  • detail: full Description, comments, and attachments;
  • search projection: only an authorized hit snippet.

A single-tenant caller usually does not need TenantId either. Change fields through an explicit API version rather than following aggregate growth.

8. Impersonation and tenant snapshot

Tickets passes currentUser.User.TenantId, while persistence also has an ambient EffectiveTenant filter. During impersonation they must refer to the same target tenant. Capture one EffectiveTenant snapshot at request start and use it for authorization, query, command, notification, search, and audit.

Clients cannot override TenantId in the query body. A Host bypass must not coexist with an explicit home tenant passed into Tickets.

9. Update remains broad

A requester, assignee, or Write Sharing holder with Update can call Start, Resolve, Close, Reopen, comment, and attach. There is no finer policy where a customer only comments/reopens while an agent resolves/closes, and comments/files are not restricted by Ticket state.

If duties must be separated, add owner-local comment, attach, progress, resolve, close, and reopen permissions, each with participant/Sharing conditions.

10. Required contracts

  1. anonymous, wrong permission, explicit Deny, Tenant DataScope, and cross-tenant;
  2. requester, direct assignee, group assignee, support/operator;
  3. Read Sharing grant/revocation and correct Write semantics;
  4. PageIndex, PageSize, and TotalCount after participant/Sharing merge;
  5. PageSize 0, 500, 1001, candidate batch 200, and offset 100000;
  6. project, sprint, status, and text filters survive authorization merge;
  7. tied timestamps, concurrent update, and soft delete;
  8. explicit tenant equals ambient filter during impersonation;
  9. detail, global search, linked files, and Reporting export fail together after Sharing revocation;
  10. real HTTP QUERY/POST fallback, 401/403, and response fields.

11. Review commands

Terminal window
# Review permission, Feature, DataScope, participant, and Sharing together.
rg -n "TicketPermissions|platform.tickets|CanViewAllAsync|FilterVisibleAsync|DataScope|Sharing" \
src/Platform/Tickets src/Framework/BitzOrcas.Application/Authorization -g '*.cs'
# Public paging and the internal authorization candidate batch are different limits.
rg -n "PagingLimits|CandidatePageSize|PageWindow|ExecutePageAsync" \
src/Platform/Tickets src/Framework -g '*.cs'
# Inspect stable ordering, full-text filtering, and the list projection.
rg -n "ReadModelSort|SearchText|Description|TicketListRow" \
src/Platform/Tickets/BitzOrcas.Platform.Tickets.Infrastructure -g '*.cs'

Back to Tickets · Lifecycle and assignment · Comments and attachments

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%