DEV Community

Andrew Elans
Andrew Elans

Posted on • Edited on

2 1 1 1

Dataverse: custom telemetry setup with allowed POST and disallowed GET

==========================
Update 26.02.2025:

See important note on "Prefer": "return=representation" below.

==========================

Briefly on the project

I'm developing an SPA with Power Pages for managing corporate supplier base. The Power Pages portal is completely stripped of the default dependencies. The MSAL library is authenticating a user with the corporate Azure tenant and Dataverse behind the Power Pages. All queries are done with a token obtained by the MSAL from the Dataverse scope.

This blog is mostly about this journey.

Telemetry implementation

I have come up with the custom telemetry logger that will enable to see statistic of the portal usage, queries sent, user errors logged, etc.

To simplify things, I have created a Dataverse table with two columns:
1) wb_datasuccess - to log params of the successful requests in JSON
2) wb_dataerror - to log user errors in JSON

Each query is accompanied by the simple POST request to add a record to the telemetry table that looks like this:

// rev 26.02.2025
// make sure that header "Prefer": "return=representation" is not present
// or set to "Prefer": "null", otherwise the POST will return 403 with comments:
// user with id ... does not have ReadAccess right(s) for record 
// with id ... of entity wb Telemetry

function recordTelemetryData(type, dataObj, token) {
    try {
        fetch(
            `${baseUrl}/api/data/v9.2/wb_telemetries`
            , {
                method: "POST",
                headers: {
                    "Accept": "application/json",
                    "OData-MaxVersion": "4.0",
                    "OData-Version": "4.0",
                    "Authorization": "Bearer " + token,
                    "Content-Type": "application/json; charset=utf-8"
                },
                body: JSON.stringify({
                    [`wb_data${type}`]: dataObj, // has all required params
                })
            }
        )
    } catch (err) {
        console.dir(err)
    }
}
Enter fullscreen mode Exit fullscreen mode

Query call with telemetry enabled:

queryWithMSALToken(
    {
        queryStr: `WhoAmI`
        , doTelemetry: true // invokes the telemetry call
        , false
    }
    , "User login, WhoAmI query" // specifies the type of request
)
Enter fullscreen mode Exit fullscreen mode

The call of the recordTelemetryData:

async function queryWithMSALToken(queryObj, queryTitle = "", forceRefresh = true, providedToken = '') {
    // other code here is removed
    if (doTelemetry) recordTelemetryData(
        'success'
        , JSON.stringify({
            queryTitle,
            fetchUrl: url,
            fetchParams,
            responseToReturn,
            browser: navigator.userAgent.indexOf("Edg") !== -1 ? "Edge" : "Chrome"
        })
        , token
    )
    // other code here is removed
}
Enter fullscreen mode Exit fullscreen mode

The telemetry call is made on behalf of the logged in user, so I would need to limit the user's access to his data only. Ideally, the user shall not be able to get any data from the wb_telemetry table.

How can we do this?

Security Roles setup and results

Microsoft provides explanation of Security roles and privileges here but generally speaking all documentation on Power Apps is cumbersome and lacks important details.

Let's see options/settings and results available to us with different setup.

First check what admin is getting

Admin by default has access to all data.

Get data

Open new private window in your browser -> type in the URL bar https://your-env.crm.dynamics.com/ -> authenticate with the user that has Admin role-> type in the URL bar https://your-env.api.crm.dynamics.com/api/data/v9.2/wb_telemetries?$select=wb_telemetryid.

Response

{
    "@odata.context": "https://your-env.api.crm.dynamics.com/api/data/v9.2/$metadata#wb_telemetries(wb_telemetryid)",
    "value": [
        {
            "@odata.etag": "W/\"9289665\"",
            "wb_telemetryid": "1cabe573-85e8-ef11-be20-000d3aaccaf0"
        },
        {
            "@odata.etag": "W/\"9289199\"",
            "wb_telemetryid": "1690f9ba-7ae8-ef11-be20-6045bd950c14"
        },
        {
            "@odata.etag": "W/\"9289228\"",
            "wb_telemetryid": "59a6c775-7ee8-ef11-be20-7c1e5273f892"
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

We have in total 3 records, each was POSTed by a different user.

Create new Security Role for a basic user

For all basic users I tend to create a new Security Role from scratch, clear all permissions (see my other post on how to do this with one single fetch) and add only those permissions, relevant for my scenario.

I have created a role aaa Basic User. My basic user is assigned to this role only.

Here (post to be made shortly) I will show a minimal set of permission you would need for such role.

Check what the basic user is getting

Setup 1

  • Member's privilege inheritance -> Direct User (Basic) access level and Team privileges
  • Create -> Organization
  • Read -> User

Image description

Get data

Open new private window in your browser -> type in the URL bar https://your-env.crm.dynamics.com/ -> authenticate as the user that has role aaa Basic User-> type in the URL bar https://your-env.api.crm.dynamics.com/api/data/v9.2/wb_telemetries?$select=wb_telemetryid.

Response

{
    "@odata.context": "https://your-env.api.crm.dynamics.com/api/data/v9.2/$metadata#wb_telemetries(wb_telemetryid)",
    "value": [
        {
            "@odata.etag": "W/\"9289199\"",
            "wb_telemetryid": "1690f9ba-7ae8-ef11-be20-6045bd950c14"
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

We get only 1 record that was POSTed by this authenticated basic user. In fact there are 3 in total as we saw above.

Setup 2 - the one I will use

  • Member's privilege inheritance -> Team privileges only
  • Create -> Organization
  • Read -> User

Image description

Get data

Do as stated in the Get data for Setup 1 above.

Response

{
    "@odata.context": "https://your-env.api.crm.dynamics.com/api/data/v9.2/$metadata#wb_telemetries",
    "value": []
}
Enter fullscreen mode Exit fullscreen mode

No data is returned as a result.

Conclusion

With Member's privilege inheritance set to Team privileges only, Read privileges set to User and Create privileges set to Organization or Business Unit the basic user:
1) Can POST a request to record his telemetry data.
2) Cannot GET any data from this table with any call.
3) Make sure not to include header "Prefer": "return=representation":
- if "Prefer": "return=representation is present, the POST will return the created record details
- if "Prefer": "null" or not included, the POST will return 204 No Content
- more on this here

That's exactly the right behavior I need and use in my production scenario.

Top comments (0)

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay