Skip to content
bitzorcas
中EN

Reference

Guidance contextual selection and authorization

A deep guide to four-level platform/tenant and component/page selection, role and language filtering, administration list/detail authorization, platform Draft exposure, permission separation, and cache boundaries.

Last updated

Guidance serves end users, content administrators, and campaign operators, and the three read surfaces have different security goals. End users should see only the current published body allowed by their audience. Administrators need Draft, state, and version metadata. Operators manage campaign lifecycle and audience. Content requests share the guidance/content resource, campaign requests use the guidance/campaign resource, and assistant requests use the guidance/assistant resource.

1. Explicit authorization declaration

Every request sets a ResourceDescriptor explicitly rather than relying on type-name inference. Actions are View/Create/Update/Delete for content and Read/Manage/Use for campaigns. The permission catalog registers six codes across three resources:

  • guidance.content.read, guidance.content.manage (content);
  • guidance.campaign.read, guidance.campaign.manage, guidance.campaign.use (campaign);
  • guidance.assistant.use (assistant).

How authorization Actions map to these codes must still be confirmed through real HTTP tests. The content side is still not split into finer permissions such as PublishedRead/DraftRead/Publish/PlatformManage, which is the root of the shared content-administration and end-user read surface (see §7).

2. Platform and tenant guards

TenancyDefaults.UnsetTenantId represents the platform tenant. Regular tenants can create/mutate only their own content. The platform tenant can create/mutate platform or arbitrary-tenant content. For reads, TenantId-null platform content passes for every tenant, and the platform tenant can read every tenant.

Review platform and tenant mutation
// Primary-key lookup is global, so object scope must be checked immediately.
var content = (await store.GetByIdAsync(contentId, cancellationToken)).Value;
if (content is null)
return GuidanceErrors.ContentNotFound(contentId);
// Platform content requires the platform tenant; tenant content requires the same tenant.
var access = GuidanceApplicationGuards.EnsureCanMutate(currentUser, content);
if (access.IsFailure)
return access.Error;
return await store.DeleteAsync(contentId, cancellationToken);

Platform power is inferred from a special tenant ID. There is no evidence here of a separate operator role, approval, or step-up check. Platform administration still needs dedicated permission and audit.

3. Contextual-query security surface

GetContextualGuide requires a nonblank RouteKey and passes current TenantId, Roles, and LanguageCode to the store. The store queries only undeleted Published rows for the tenant or platform null, then applies RequiredRole and rank.

Read the best published guide for a page
GET /api/v1/guidance/contextual?routeKey=%2Fbilling%2Finvoices&componentId=grid&languageCode=en-US
Authorization: Bearer <token>

Success returns full BodyMarkdown. No match returns ContextualNotFound. The query never returns Draft and does not invoke AI automatically.

4. Four-level ranking

With a ComponentId, tenant-component is rank 0, tenant-page 1, platform-component 2, and platform-page 3. Without a component, component candidates are excluded and only page candidates remain.

Within a rank, a nonblank matching RequiredRole sorts before general content, followed by ContentVersion, modify/create time, and ID descending. Role-specific content can therefore beat a newer general guide.

5. Single-role audience limitations

RequiredRole supports one role name or no restriction. It cannot express any/all roles, permission, office, plan, feature, data attribute, or deny rule. Renaming a role silently hides content, with no foreign key or migration.

GA needs a stable Audience Policy and an administration explanation of why a guide matched. End-user errors must not reveal hidden candidates.

6. Platform override semantics

Tenant content always beats platform content. For a component request, a tenant page guide rank 1 also beats a platform component guide rank 2. That is the exact current algorithm; product owners should confirm whether tenant precedence should outweigh component specificity.

There is no explicit platform-guide suppression/tombstone. A tenant that wants to hide platform content must provide another candidate; an empty-body suppression record is invalid.

7. GetById Draft exposure

GetById loads only by global ID and undeleted state. The read guard permits TenantId-null platform content and checks neither Status nor RequiredRole. A regular tenant with content View can read full platform Draft Markdown if it knows the ID.

This is not a ranking defect. It is caused by reusing an administration-detail contract as a general read contract. The fix belongs in API/use-case authorization, not in hiding IDs in the UI.

8. List as an administration surface

List defaults IncludePlatform to true and filters exact route/component, language, status, and offset page. Search scopes tenant/platform rows but intentionally does not filter RequiredRole because it is an administration list. Its current permission is still ordinary View.

Target: separate end-user read from administration read
// End-user use case returns only Published content visible to the current audience.
var visible = await publishedReader.FindForCurrentAudienceAsync(
routeKey, componentId, languageCode, currentUser, cancellationToken);
// Administration requires dedicated DraftRead/Manage permission.
authorization.Require(GuidancePermissions.DraftRead);
var drafts = await adminStore.SearchDraftsAsync(scope, page, cancellationToken);

These ports and permissions are target boundaries, not current source.

9. Language selection

Queries default to zh-CN. en-US normalizes case-insensitively; every other value becomes zh-CN. There is no automatic negotiation from request culture, user preference, or Accept-Language, and no explicit missing-en-US fallback chain.

Product policy should order explicit request, user preference, tenant default, and platform default, and distinguish missing translation from invalid locale.

10. Cache boundary

The current store queries the database every time and has no Guidance cache. A future key must contain TenantId, RouteKey, ComponentId, LanguageCode, Audience/role policy version, and PublishedRevision. A route-only key would leak across tenants or roles.

Publish, unpublish, audience changes, and role/permission changes must invalidate. Cache only a published safe projection; Draft administration needs a separate namespace.

11. Error and enumeration behavior

CrossTenantAccessDenied is Forbidden, while an unknown ID is NotFound. The handler reads globally before returning Forbidden for a cross-tenant ID, which may form an existence oracle. High-security APIs can normalize this to NotFound.

Platform Draft access should be Forbidden. An audience-mismatched Published guide should also look absent so its title/required role is not disclosed.

12. HTTP test matrix

ScenarioExpected
anonymous Contextual401
ordinary reader gets matching Published200
ordinary reader gets platform Draft by IDforbidden/not found
ordinary tenant lists platform Draftexcluded
role mismatchcontextual not found
tenant B asks for tenant A IDnot found
tenant A creates for tenant Bforbidden
regular tenant mutates platform contentforbidden
platform operator cross-tenant writededicated permission plus audit
unknown LanguageCodevalidation

13. Audit and privacy

Record route/component, scope, revision, match rank, audience-policy ID, and actor, but not BodyMarkdown. Platform cross-tenant writes need target tenant, reason, change summary, and correlation.

Aggregate no-match metrics safely. Do not return RequiredRole or hidden titles to unauthorized users.

14. Change-design checks

Before adding a match dimension, place it explicitly before or after the four scope ranks and define a truth table for every combination. Do not casually append a ThenBy; that silently changes which published content wins without a versioned migration.

Separate response models as well. End-user DTOs contain safe display fields and PublishedRevision; administration DTOs alone expose status, audience, actor, and change metadata. They must not share cached serialization objects.

  • New dimensions require platform/tenant, page/component, and role-change regression tests.
  • Selection-rule changes require compatibility notes, rollout metrics, and rollback.

Before rollout, replay production-shaped distributions through old and new algorithms and list every winner change instead of comparing only aggregate hit rate.

15. Review commands

Terminal window
# Current guards, rank, and administration list scope.
rg -n "EnsureCanRead|EnsureCanMutate|MatchRank|RoleRank|IncludePlatform" \
src/Platform/Guidance -g '*.cs'
# Fine-grained permissions and audience policy should have no matches.
rg -n "DraftRead|PublishedRead|AssistantUse|AudiencePolicy" \
src/Platform/Guidance -g '*.cs'

Guidance overview · Content lifecycle · Assistant security

100%

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