Resolution Engine
The SDK's authorization engine resolves a partner's effective abilities for a given app at a given timestamp — Authz = f(App, Actor, Timestamp). It uses one SQL query — no N+1, no in-PHP filtering, no cached partial state.
The set of abilities an actor holds is an AbilitySet, reached at runtime through SdkContext::abilities(). ("Ability set" is the capability axis — what an actor may do. It is distinct from row-level data scope, which is the Partners #[Scoped*] family; the two never share a name. See Scoped Entity Sets.)
Snapshot mode
Class:
Shell\Context\SnapshotAbilitiesContributorReturns the client-relevant bucket:
[
'act' => [ ability => bool ], // operation guards (UX hint; server-enforced)
]see is not delivered here. Per-element visibility is resolved server-side from the same See grants and applied by injecting sap.ui.viewModifications into the app manifest when it is served (View Visibility); only act reaches the client. The underlying AbilityGrantQuery still resolves See grants — they just travel through the manifest, not context.json.
Algorithm
- Load all abilities for the app (from
sdk_abilitiesfiltered byartifact_id). - Initialize each entry as
false. - Run
AbilityGrantQuery. - Mark granted abilities as
true.
That's it. Three steps, one query, no surprises.
AbilityGrantQuery
A single UNION ALL query that resolves four grant paths in parallel:
- Direct ability assignments —
sdk_ability_assignmentsjoined tosdk_abilities. - Role assignments —
sdk_role_assignmentsjoined throughsdk_ability_roletosdk_abilities. - Group → role grants —
sdk_group_assignmentsjoined throughsdk_group_roleandsdk_ability_roletosdk_abilities. - Group → ability grants —
sdk_group_assignmentsjoined throughsdk_ability_grouptosdk_abilities.
All four are filtered by:
partner_idvalid_from <= :timestampvalid_until >= :timestamp- artifact namespace
The query uses positional parameters so it stays SQLite-safe and portable across MySQL and PostgreSQL.
Read abilities — the OData entity-set gates — are checked against the same grant set. ODataReadAuthorizer asks SdkContext::abilities() whether the actor holds the set's Read ability: a denied root set answers 403, a denied $expand target is dropped with a sap-messages warning. Read and Access are enforced on the server; they are not part of the snapshot the client receives, but explain lists them.
Explain mode
Class:
Shell\Context\ExplainAbilitiesContributorSame AbilityGrantQuery, projected into a verbose structure:
[
'access' => [ /* same shape as 'act' */ ],
'act' => [
'closePeriod' => [
'granted' => true,
'sources' => [
[
'type' => 'role',
'role' => 'Accountant',
'role_id' => 7,
'assignment_id' => 15,
'valid_from' => ...,
'valid_until' => ...,
],
],
],
],
]Explain returns all four buckets, in the order access, act, see, read (see and read have the same shape). Every ability of the app is listed with granted; each granted one carries one or more sources (duplicates removed). A source's type is one of:
direct— a direct ability assignment.role— a role assignment; also carriesroleandrole_id.group— a group assignment; carriesgroupplus eithervia_role(the group grants a role that holds the ability) ordirect: true(the group holds the ability itself).
Every source also carries assignment_id, valid_from and valid_until.
Explain is descriptive, not normative.
The snapshot and explain modes share the same SQL backbone — they cannot disagree.
Why one query
A single union-based grant resolution gives you:
- Deterministic counts — no race between filters and joins.
- No N+1 — every grant path is one query, not one query per grant.
- Index-friendly — the query uses simple equality filters and timestamp ranges.
- Testable — every grant path can be exercised by the synthetic test DSL.
- Explainable — every granted ability can attribute its sources without re-querying.
See also
- Domain Model — entities the query resolves over
- View Visibility — how
Seegrants reach the app manifest - Scoped Entity Sets — row-level data scope, the axis next to abilities