Power BI Apps are the cleanest way to share reports with business users. But in many organisations, getting access to one still works like this: someone sends an email or a Teams message, it waits in an admin’s inbox, the admin opens the app settings and adds the person by hand, and a couple of days later the user finds out (or not) that they’re in. There is no record of who asked, who approved, or why.

This post shows a small, working setup that replaces that with a self-service request and an approval by email, using only tools most Microsoft 365 tenants already have: SharePoint lists, Power Automate and Microsoft Entra ID groups.

The problem

Let’s say Company A publishes a Power BI App called Sales Dashboard. Access is managed manually:

  • Requests arrive through different channels (email, chat, a hallway conversation).
  • Whoever manages the app adds users one by one to the app audience.
  • Approvals, when they happen, are buried in email threads.
  • Nobody can easily answer “who has access to Sales Dashboard, and who approved it?”

The goal is a process where:

  1. The user asks for access in one place.
  2. The right business approver gets the request by email and approves or rejects it there.
  3. If approved, access is granted automatically.
  4. The user is told the outcome.
  5. Every step leaves a record.

Solution overview

The key design decision is to never grant access to individuals. Each Power BI App audience gets a group, and the automation only manages group membership. Power BI does the rest.

ComponentRole
Entra ID group (one per app)Added to the Power BI App audience. Anyone in the group can open the app.
SharePoint list AccessRequestsWhere users create their requests. Also the audit trail.
SharePoint list AccessAppConfigOne row per app: app name, group ID and approver.
Power Automate flowRuns the approval and adds the user to the group.

The config list is what makes this scalable: to put another app under the same process, you add a row to AccessAppConfig. The flow doesn’t change.

How a request moves through SharePoint, Power Automate, the approver and Entra ID.

Step 1. Prepare Entra ID and the Power BI App

First, a group for the app. I named it after the app it protects, and used the description to say what membership means:

The Entra ID group for the Sales Dashboard app. It starts with no members.

In this proof of concept the group is a Microsoft 365 group. If you go that way, make sure it’s Private: in a Public Microsoft 365 group, anyone in the organisation can join on their own from Outlook or Teams, which would skip the approval entirely. With a Private group, only owners (and the flow) add members. In production I’d use a security group instead (see What I’d change for production below).

Then, in the Power BI App, edit the audience and grant access to that group instead of to individual users:

The group added to the Sales Dashboard app audience.

From this point, anyone who is in the group can open the app. Nobody needs to touch the app settings again.

Step 2. Create the SharePoint lists

The first list, AccessRequests, is where users ask for access. It’s on a site everyone in the company can reach.

The AccessRequests list.

Columns:

  • RequestedByUPN: the requester’s email address (user principal name).

  • RequestedApp: a Choice column with the apps you can request. A drop-down instead of free text, so every request matches an app name exactly.

    RequestedApp is a choice column, so users pick an app instead of typing it.

  • Justification: why the user needs access. It’s shown to the approver.

  • Status: a Choice column, New by default, then Approved or Rejected. I also added Pending Approval for later use.

    The Status column.

  • ApproverComments: filled in by the flow with whatever the approver writes when answering.

  • Title (the list’s default column), shown to the approver as “Access requested for”. It’s there for requests made on behalf of a colleague, and stays empty when you ask for yourself.

The second list, AccessAppConfig, is the configuration. One row per app:

The AccessAppConfig list.

  • Title: the app name, exactly as it appears in the RequestedApp choices.
  • GroupId: the Object ID of the Entra group from step 1.
  • ApproverEmail: who approves access to this app.

Step 3. Build the flow

Here’s the whole flow at a glance:

The flow: trigger, look up the app configuration, then the approval loop.

Trigger: a new request

The flow starts when an item is created in AccessRequests:

Trigger: When an item is created.

Look up the app configuration

Get items reads AccessAppConfig, filtered to the app in the request. Because RequestedApp is a Choice column, the value sits one level down:

Get items, filtered by the requested app.

Title eq '@{triggerOutputs()?['body/RequestedApp']?['Value']}'

Get items always returns a list, even when there’s only one match, so three Compose actions pick the first row and pull out the two fields the flow needs:

AppConfig: the first matching row.

GroupId from the config row.

ApproverEmail from the config row.

first(body('Get_items')?['value'])
outputs('AppConfig')?['GroupId']
outputs('AppConfig')?['ApproverEmail']

Ask for approval

Start and wait for an approval (Approve/Reject – First to respond) sends the request to the approver from the config list, with the requester, the app and the justification in the details:

Start and wait for an approval.

Approved or rejected

A condition checks the outcome:

Condition on the approval outcome.

If approved, the flow looks up the requester in Entra ID and adds them to the app’s group:

Get user, then Add user to group.

Then it updates the request in SharePoint with the status and the approver’s comment, and emails the requester:

The request is marked Approved, with the approver’s comment.

The confirmation email to the requester.

If rejected, the same two steps run with Rejected, and no group membership changes:

The request is marked Rejected.

The rejection email.

Step 4. Test it end to end

The test user is Adele Vance, one of Microsoft’s demo users, in my test tenant:

Signed in as Adele.

Adele creates a request for Sales Dashboard. It arrives with status New:

Adele’s request in AccessRequests.

The config list says who approves Sales Dashboard and which group gives access to it:

The Sales Dashboard row in AccessAppConfig.

And before the approval, that group is empty:

No members yet.

The approver receives the request by email:

The approval email.

Two details in this email:

  • “Requested by” shows the flow owner, not Adele. Approvals always show the account whose connection created the approval. That’s why the email title and Access requested by name the actual requester.
  • “Access requested for” is empty because Adele asked for herself. It’s only filled in for on-behalf-of requests.

The approver approves directly from the email and adds a comment:

Approved from the email, with a comment.

Right after the approval, Adele has two new emails:

Adele’s inbox after the approval.

The first comes from the flow and confirms the access:

Access granted.

The second isn’t sent by the flow. It’s the automatic welcome message that Microsoft 365 groups send to new members. With a security group, it wouldn’t arrive.

The Microsoft 365 group welcome email.

The request in SharePoint now shows who approved it and why, without anyone editing it:

The request, approved, with the approver’s comment.

And Adele is in the group:

Adele is now a member of the group.

The catch: access to the app is not access to the data

Adele opens the app, and… nothing loads:

The app opens, but the visuals are blocked by row-level security.

This is working as intended. The report uses row-level security (RLS), and Adele isn’t in any RLS role. Being in the app audience lets her open the app. It doesn’t decide which rows she can see.

So a complete access process has to cover both:

  • App access: the audience group, handled by this flow.
  • Data access: the RLS role. The simplest option is to add the same group (or a dedicated one per role) to the RLS role in the semantic model. Then one approval grants both. For row filters that depend on the user (each country manager sees only their own country, for example), dynamic RLS with a mapping table is the usual pattern.

What I’d change for production

The proof of concept works, but a few things would need tightening before real users depend on it:

  1. Use a security group. For pure access control, a security group has no mailbox, no welcome email, and no “join” option for users. If you keep a Microsoft 365 group, make sure it’s Private.
  2. Don’t trust a typed email address. RequestedByUPN is free text, so in theory anyone could request access for someone else. Use the list’s built-in Created By field as the requester instead. For on-behalf-of requests, add a Person column (“Requested for”) and have the flow add that person, with the approver seeing both names.
  3. Link to the request, not the list. The approval’s Item link should be the trigger’s Link to item, so the approver lands on the exact request.
  4. Run the flow under a service account. Flows tied to a personal account break when that person leaves, and the approval emails show their name. The account that runs the flow only needs to be an owner of the app groups, not an admin.
  5. Handle the edge cases. Add user to group fails if the user is already a member, and Get user fails if the address doesn’t exist. Wrap those actions in a scope, check membership first, and set the Status to an error value instead of letting the run fail silently.
  6. Drop the For each. The approval sits inside a loop over the Get items result. Since there’s only one config row per app, the Compose outputs alone are enough.
  7. Make access expire. Approved access tends to stay forever. A scheduled flow could ask approvers to confirm access every few months, or remove members after a set period.
  8. Consider the native option. If your tenant has Microsoft Entra ID P2 or ID Governance licences, entitlement management (access packages) provides requests, approvals, expiry and access reviews out of the box. The flow in this post is the lightweight version for tenants with standard licences.

Conclusion

The flow itself is small, but the design choices are what make it hold up: groups instead of individuals, a config list instead of hard-coded values, and a SharePoint list that doubles as the audit trail. Access requests stop living in inboxes, and the question “who has access to this app, and who approved it?” has an answer you can filter.