Embedding Fabric reports with service principal and rls - External users

Kaarel Kõrvemaa | Apr 24, 2025 min read

This blog is about how you can embed Fabric reports into your website. 99% of the code is from Microsoft sample. The example code has everything need to embed a regular Power bi report into your website.

In this example we will look into how you can embed an Fabric report with row level security into your website that has a different authentication than your organization(none Entra). This is for embedding content for customers that are outside of your organization. Also, at the time of writing this blog, Enterprise app is not supported with Fabric direct lake embedding.

Preliminary tasks

Service principal = Service principal needs to be in the Power Bi workspace where the semantic model and report are, has to have viewer or higher rights. Also, when creating the service principal you need to add delegated scopes for the service principal. The options need to be Lakehouse.Read.All or Lakehouse.ReadWrite.All or Item.Read.All or Item.ReadWrite.All .

Delegated permissions

Power bi admin = Service principal needs to be in the power bi admin. The full list of tasks can be found from here

External authentication = When you have an external authentication, like AD B2C the report doesn’t know the user, because it is not in the Microsoft Entra.

Row level security = Row level security is something that you create into you semantic model, which provides you with specific data or visibility access. More about rls.

Full code

If you already have everything in place the full code can be found from here

What are the changes you need to do for RLS ?

High level picture on how this works:

Diagram

When embedding your reports, there is a token that is generated for that. There are few levels of security for accessing the data itself. Even if you will get the token, you won’t see the data, because there is an rls level security and you need to pass the correct user with it. This is where the Microsoft provided script needed some configuration.

Here is the full code changes that provides the right way to get the token.

    def get_embed_token_for_single_report_single_workspace(self, report_id, dataset_ids, target_workspace_id=None):
        '''Get Embed token for single report, multiple datasets, and an optional target workspace

        Args:
            report_id (str): Report Id
            dataset_ids (list): Dataset Ids
            target_workspace_id (str, optional): Workspace Id. Defaults to None.
            userName (str): uuid id, principal id or other identifier 
            rlsRole (str):The rls role created in Power bi desktop

        Returns:
            EmbedToken: Embed token
        '''
        userName = app.config['USER_ID']
        rlsRole = app.config['RLS_ROLE']
        request_body = EmbedTokenRequestBody()

        for dataset_id in dataset_ids:
            request_body.datasets.append({'id': dataset_id})

        request_body.reports.append({'id': report_id})

        if target_workspace_id is not None:
            request_body.targetWorkspaces.append({'id': target_workspace_id})
        
        if userName is not None:
            request_body.identities = [{"username": userName, "roles": [rlsRole], "datasets": [f"{dataset_ids[0]}"]}]
        # Generate Embed token for multiple workspaces, datasets, and reports. Refer https://aka.ms/MultiResourceEmbedToken
        embed_token_api = 'https://api.powerbi.com/v1.0/myorg/GenerateToken'
        api_response = requests.post(embed_token_api, data=json.dumps(request_body.__dict__), headers=self.get_request_header())
        if api_response.status_code != 200:
            abort(api_response.status_code, description=f'Error while retrieving Embed token\n{api_response.reason}:\t{api_response.text}\nRequestId:\t{api_response.headers.get("RequestId")}')

        api_response = json.loads(api_response.text)
        embed_token = EmbedToken(api_response['tokenId'], api_response['token'], api_response['expiration'])
        return embed_token

For the “regular” reports that are embedded to the web, there is no need to specific what you need to see from the semantic model, but with the rls you need to provide role, username and dataset.


userName = app.config['USER_ID']
rlsRole = app.config['RLS_ROLE']
request_body = EmbedTokenRequestBody()

for dataset_id in dataset_ids:
    request_body.datasets.append({'id': dataset_id})

request_body.reports.append({'id': report_id})

if target_workspace_id is not None:
    request_body.targetWorkspaces.append({'id': target_workspace_id})

if userName is not None:
    request_body.identities = [{"username": userName, "roles": [rlsRole], "datasets": [f"{dataset_ids[0]}"]}]

The values are defined in config.py file. The name of the role is the Row-level security role name and the username is the id (For example principal id ). The dataset id is always the underlying dataset it from the report.

Example how the request body would look like

  {
    "datasets": [

        {

        "id": "",


        }

    ],

    "reports": [

        {

        "id": ""

        }

    ],

    "identities": [
            {
                "username": f"{userName}",
                "roles": ["RLS_ROLE"],
                "datasets": ["dataset_ids"]
            }
        ]
     }

Link to the official documentation

Summary

When embedding your reports to external users, use always rls, even if everyone has the same username. This will provide an extra layer of security for you reports and you can make sure that even if the token gets compromised the report data will always be safe. Also report hijacking will only result in an empty report.

Have questions ? Reach out ot me on Linkedin