App access to sources
A builder querying a source during a session uses their own identity and their own group memberships. A published app is different: it presents its own group, not a general one, so it needs its own recorded permission per source. That permission is a request the app’s owner files and someone with the Ops role approves, and for every source except a Power BI semantic model the record of that request is the whole mechanism: there is no separate grant object to create, rotate, or clean up.
Semantic models are the exception, because Power BI authorizes the signed-in viewer itself. A decision on one of those sources returns operator steps you have to run, and Semantic models: the steps sprig cannot run below is what they are.
The request is the grant
Section titled “The request is the grant”Sprig keeps one live record per app and source pair. Its status is the access:
- Pending: filed and waiting in the Ops inbox. The app cannot read the source yet.
- Approved: the app’s groups read the source’s exposed entities, read-only, with the same column excludes every other reader gets. Approval takes effect through one data-layer configuration roll (about a minute), with no redeploy of the app. On a semantic model there is no roll at all: approval is live the moment it is recorded, and it is only half the access until you run the model grant below.
- Rejected: declined, optionally with a reason shown back to the requester. The owner can file a new request for the same source later.
- Revoked: access withdrawn after an approval - by Ops, by the source being deleted, or by the app being retired. Like a rejection, the pair can be requested again.
A grant never widens what Ops exposed: which entities exist and which columns are hidden stay decided by the source’s exposure, and the grant is read-only regardless of how the source itself is configured.
Builder side: requesting access
Section titled “Builder side: requesting access”Both paths are tools the builder asks Claude for, both are owner-only, and both land in the audit trail:
- Any time.
request_source_accessnames one of your apps and one source id and files a single request. The reply is the record itself, so its status says where you stand:pendingmeans it is waiting in the Ops inbox and the app cannot read the source yet,approvedmeans the source’s policy approved it on the spot and the grant is already live. - At provision time.
provision_app, the tool that gives an app its own database, also takes asourceslist of registered source ids. The requests are filed as one batch after the provision succeeds, and the response reports one outcome per source:filed(a new request),existing(the pair already had a live request, so re-running the same provision stays safe and files nothing twice), orsuperseded(the app was retired, its owner offboarded, or its ownership changed while the batch was being filed, so this source got no request at all; adetailnames what happened). Pairs reportedexistingthat are already approved are re-applied as part of the batch’s single configuration roll, so a re-provision also repairs a grant whose earlier roll did not finish. Asupersededsource never discards the rest of the batch: the sources filed before it stay filed and are applied. An app that reads only registered sources and needs no database of its own asks per source withrequest_source_accessinstead.
What happens next depends on the source’s access policy (below): under manual the
request waits for Ops; under auto the response already carries it approved and live.
A request is refused up front when the source does not exist or is not in service, when the app is retired, or when the app’s owner has been offboarded. A pair whose request is still pending cannot be filed again until that request is decided. Requesting a pair that is already approved succeeds without filing anything new: the response carries the existing record and the platform re-applies the grant with one configuration roll. That re-apply is the recovery path for an approval whose configuration roll failed partway; repeating the request converges the grant instead of erroring. The source must still be in service for that: if it has been disabled or deleted since the approval, the repeat request is refused and names the source’s current state rather than putting the source back in front of the app.
Both operations are also plain API routes for your own tooling: POST /api/apps/{name}/source-access with { "source": "<source-id>" } files one request,
and the provision request carries the same sources list. GET /api/apps/{name}/source-access lists every request one app has ever filed, with each
one’s status and decision. That last one has no tool yet, so while a request is pending
the builder has the reply from filing it and Ops has the inbox.
The same conditions are checked again at approval, because a request can sit in the inbox while the world changes around it: approving is refused if the source has since been disabled or deleted, if the app has been retired, or if its owner has been offboarded. Rejecting stays available in all of those cases, since it is inbox cleanup rather than access.
For a semantic model there is one more condition. The model permission is attached to a group’s object id, so the app’s viewers and builders groups have to exist before access can be granted at all. If they were never provisioned, the approval is refused rather than issuing steps that resolve the group by name: Entra allows two groups to share a display name, and granting the wrong one would open the model to strangers while leaving the app unable to query it. Re-provision the app, then approve.
Ops side: the inbox and decisions
Section titled “Ops side: the inbox and decisions”Open Source access in the admin console to see the pending queue. Select one or more requests, then approve or reject the selection. The console sends the selected ids in one batch, matching the single-roll behavior below.
Each step is an Ops tool, with the plain API route behind it in parentheses for wiring into your own tooling. Tool and route both require the Ops role:
- Inbox.
list_source_access_requests(GET /api/source-access-requests) lists pending requests: request id, app, source, who asked, and when. It is a different inbox fromlist_share_requests: that one decides who may open an app, this one decides what an app may read. - Decide in batch.
approve_source_accessandreject_source_access(POST /api/source-access-requests/decision), each taking every request id you are deciding in one call, a rejection also taking an optional reason shown back to the requester. Rejection reasons are limited to 1,024 characters. One batch is one decision: approve them all or reject them all. An id that is already rejected or revoked when the batch is read fails the whole batch before anything is written, so a stale inbox view cannot half-apply; re-approving an id that is already approved is not an error - it converges, re-applying the existing grant, while a rejection needs every id to still be pending, so an already approved id fails a reject batch. Approving requires every named source to still be in service, on the same terms a builder’s request does: if a source has been disabled or deleted since the request was filed, the approve batch is refused up front and names that source’s current state. A rejection never asks that of the source, since it takes access away rather than granting it, so a source out of service does not block one. The response reports one outcome per request:applied(this decision flipped it),converged(already approved; re-applied), orsuperseded(a concurrent decision, an app retirement, or an owner offboarding won the row mid-batch; adetailnames the winner). A superseded row never blocks the rest of the batch: the other rows still apply, and the batch’s approved and converged rows are served by exactly one data-layer configuration roll whatever the batch size. Rejecting rolls nothing, and a batch in which nothing applied or converged rolls nothing either. - Revoke one approved grant.
revoke_source_access(POST /api/source-access-requests/{id}/revoke) flips the grant off and rolls a configuration without it. Revocation is for approved grants; pending requests are rejected, not revoked. Re-revoking a grant that is already revoked is safe and rolls the configuration again.
What you are deciding is whether this app should read that source’s exposed entities. For an Azure SQL source, each viewer’s queries still run under their own database identity on top of the grant, so the database’s own permissions remain the final word. For a gateway-enforced source (Fabric Warehouse and lakehouse), the grant is the data-access decision itself: treat it with the same care as a share approval for such a source, and check what the source exposes before approving.
Semantic models: the steps sprig cannot run
Section titled “Semantic models: the steps sprig cannot run”For a Power BI semantic model, approving the request opens the route but does not open the data. Queries run under each signed-in viewer’s own identity, so Power BI checks that viewer, and the app’s groups need their own permission on the model. Sprig cannot grant that for you, so it hands you the exact steps instead.
- On approval the response carries a
grantrunbook: the curl that gives the app’s viewers and builders groups Read and Build on the model, addressed by each group’s object id. Until it runs, every query from the app answers access denied. Anauto-policy source carries the same runbook on the response that files the request, including the provision-time batch, since nothing about it ever reaches the Ops inbox. - On a Direct Lake model Read and Build is not enough. The engine reads OneLake as the viewer, so each group also needs a OneLake data-access role on the model’s source lakehouse tables. The runbook says so and names the constraints (table-scoped, security groups only, never a workspace role).
- On revocation the response carries a
revocationrunbook. The app’s groups outlive the revocation and so does their permission on the model, so it does not lapse on its own. Revoking the same request again reissues the steps, which is how you get them back if the first response was lost. - On retirement or source deletion the same removal steps come back: retiring an app
reports them under
sourceRevocations, and deleting the source returns them as the final runbook, naming every group whose grant would otherwise outlive the source.
Permission changes here take a while to reach the query engine (about half an hour for the model, longer for OneLake), so treat an early failure as still propagating and retry before granting anything twice.
To be told when a request arrives rather than checking the inbox, see Notifications.
Per-source policy: manual or auto
Section titled “Per-source policy: manual or auto”Every source carries an appAccessPolicy. Open the source from Sources to change it
without re-running discovery. The same page lists the apps whose access is currently
approved:
manual(the default): every request waits in the inbox for a human decision.auto: a request is approved the moment it is filed, recorded as decided bypolicy:auto, and materialized immediately, with no notification to Ops.
auto means any onboarded builder who owns an app can attach it to that source without
anyone reviewing it. Reserve it for sources whose exposed entities you are comfortable
treating as readable by every app in the installation; leave everything else on
manual.
What removes access without a new decision
Section titled “What removes access without a new decision”- Deleting a source revokes every live relation to it, pending and approved. If the same source id is registered again later it starts with zero live relations: every app must request access again.
- Retiring an app revokes all of the app’s live relations as part of the retirement. Restoring the app does not restore them; each source needs a fresh request and a fresh decision. Retirement also removes the app’s builder and viewer groups from source-level reader defaults, so a later exposure save cannot grant them back. While an app is retired, new requests and approvals for it are refused. Retiring and retired app groups are also excluded from the source editor. If the editor cannot load the current active groups, it reports the lookup failure, offers Retry, and disables save until the lookup succeeds.
On a semantic model, neither of these removes the model permission itself: the app’s groups survive both. Both responses carry the removal steps, and access continues until someone runs them.
Both leave the records in place with status revoked, so the history stays auditable.
Complete external revocations after retirement
Section titled “Complete external revocations after retirement”Sprig records source-owner work before it removes source defaults and entity grants. This
makes a failed retirement safe to retry: POST /api/admin/apps/{name}/retire returns the same
still-pending entries in sourceRevocations until Ops confirms that every returned runbook
has been completed.
Each entry identifies its source and its closed source kind:
sourceKind: azure-sqlcarries ready-to-run T-SQL insql. Run it as that source’s DBA to drop the retired app’s database principals. The response also names any shared reader groups deliberately left in place because removing them could cut off other readers.sourceKind: fabric-semanticcarriesworkspaceId,itemId, and arunbook. Follow it as a Fabric or Power BI administrator. Set each retired app principal’s semantic model access to None, then remove that principal from the model’s OneLake data-access roles.
After every pending runbook for the source has been completed, re-register that source with
its current address and settings, plus --revocations-applied --expected-version <n>. Use
the source version shown by the current source record. This acknowledgment clears the
durable backlog under that version fence and records the confirmation in the audit trail.
Do not acknowledge a partial batch: the flag confirms every pending revocation on the source.
Sprig refuses to delete a source while that backlog is non-empty, so deletion cannot discard
the work before it is acknowledged.
What a viewer sees while access is missing
Section titled “What a viewer sees while access is missing”A published app that queries a source it has no live grant for is refused by the data layer, and the gateway names the reason in the response instead of leaving the app with a blank refusal it cannot explain. Two states, both HTTP 403 with a body carrying a code, a message, whether retrying can help, and the source the answer is about:
- Pending: code
access_pending, marked retryable. An app that handles it shows an “awaiting approval” panel for that data and keeps the rest of the page working. Nothing is broken and nothing needs rebuilding: the request is sitting in the inbox above. - No live relation: code
access_not_granted, marked not retryable. Nobody has asked for this app and source pair, or the request was rejected or revoked. Retrying changes nothing; the app’s owner files a request, or Ops decides the one that already exists.
Once Ops approves, the app fills in on its next query, after the single configuration roll the approval triggers. There is no republish and no redeploy, and nothing for the viewer to do beyond reloading the view.
The response names the source id and nothing else. It never names the Entra groups behind the source, the entities the source exposes, or who owns the app, so a viewer who cannot read that source learns only that this app has no approved access to it. Apps that query the shared GraphQL endpoint rather than the per-entity REST paths get the same 403 without that body, so what the viewer sees there is whatever the app itself renders for a failed call.
Audit trail
Section titled “Audit trail”Every step is recorded with the acting identity:
source_access.requested- one per filed request, including each source filed by a provision-time batch.source_access.approvedandsource_access.rejected- one per request the batch decided (outcomeapplied) or recovered (outcomeconverged), naming the request, its outcome, and who originally asked, so a recovered approval is distinguishable from a first-time one. Superseded rows write no new decision record: the concurrent winner already owns that trail.source_access.revoked- an Ops revocation of one approved grant.
Decisions the platform takes itself are visible on the record’s deciding identity:
policy:auto for auto-approved requests, policy:retire for grants revoked by an app’s
retirement, and system:source-deleted for grants revoked by a source’s deletion.