Power Pages archive

PowerApps Portal Security Part 2: Triaging Link-Entity Counts

A small Liquid web template that shows you the FetchXML your entity permissions actually produce.

Updated 4 min readPortal security · Part 2 of 3Still applies

Where this stands in 2026

Still applies

Still works. The fetchxml Liquid tag still applies table permissions, and Microsoft’s docs now point to its xml attribute for exactly this kind of debugging. If you need a real data endpoint rather than a debugging tool, the Power Pages Web API is now the supported option.

Official docs for today’s approach

Written in 2020 for PowerApps Portals. See what things are called now.

Microsoft has since renamed the product and most of its parts. The text keeps the original terms.

In this noteToday
PowerApps PortalsPower Pages
EntityTable
Entity PermissionTable permission
Web FormAdvanced (multistep) form
Entity ListList

Overview

As discussed in my last post, utilizing Entity Permissions in Entity/Web Forms (sub-grids) and Entity Lists injects link-entity tags into the queries in order to build a relationship hierarchy back to the Contact or the Contact’s “Customer” field.

Similarly, using the fetchxml tag will always honor Entity Permissions and inject the links. Using this to our advantage, we can create a few quick and easy tools to help with debugging and/or understanding our Entity Permissions and Web Roles.

Set Up the Base Web Template

Add the following code snippet to a new Web Template titled “Permissions Query” with a MIME type of application/xml:

Liquid/XML
{%- assign etn = etn | default: params.etn | default: 'account' -%}
{%- assign pk = pk | default: params.pk | default: 'name' -%}
{%- assign show = show | default: params.show | default: false | boolean -%}
{%- assign recordid = etn | append: "id" -%}
{%- fetchxml query -%}
    <fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false">
      <entity name="{{ etn }}">
          <attribute name="createdon" />
          <attribute name="{{ recordid }}" />
          {%- if show -%}<attribute name="{{ pk }}" />{%- endif -%}
          <order attribute="createdon" descending="false" />
      </entity>
    </fetch>
{%- endfetchxml -%}
<PermissionsQuery>
  <Query>
    {{ query.xml }}
  </Query>
  {%- if query.results -%}
    <ResultCount>{{ query.results.entities.size | default: 0 }}</ResultCount>
    {%- if show -%}
      <Results>
        {%- for result in query.results.entities -%}
          <Record>
            <Key>{{ result[pk] | xml_escape }}</Key>
            <Guid>{{ result[recordid] | xml_escape }}</Guid>
          </Record>
        {%- endfor -%}
      </Results>
    {%- endif -%}
  {%- endif -%}
</PermissionsQuery>

Note that you could change this to JSON or any other preferred output type.

The parameters work as follows:

  • etn: Defines the Entity to query against [Default: account]
  • pk: Defines the Primary Key for entities that do not use name, such as Contact or custom Entities [Default: name]
  • show: Whether or not to show the results of the query [Default: false]

Option 1: Use as an API

Now that the Web Template is available, let’s set up an API. This API can either be consumed (query for it, act on the data) or displayed in the browser for simple viewing. A few steps will get things started:

  1. Create a Page Template – “Permissions Query”
    • Type: Web Template
    • Web Template: Permissions Query
    • Use Website Header and Footer: False
  2. Create a Web Page – “Permissions Query”
    • Parent Page: Home
    • Partial URL: permissions
    • Page Template: Permissions Query
  3. OPTIONAL: Create a Web Page Access Control Rule to prevent non-Admins from using it – “Permissions Query Admin Access”
    • Web Page: Permissions Query
    • Right: Restrict Read
    • Web Roles: Administrators
Figure 1Example of an XML-based API

Recreated illustration: Example of an XML-based API: the Permissions Query page returns the FetchXML for All Opportunities, including the link-entity and condition injected by the user’s Entity Permissions, plus a result count.

  1. Callout 1: The <condition> injected for the root permission: the Contact’s Company (parentcustomerid) must equal the user’s Account.
  2. Callout 2: The <link-entity> injected for the Parent permission, joining Opportunity to Contact through opportunity_parent_contact.
  3. Callout 3: ResultCount: the number of records this user’s combined Web Roles can actually read.

Recreated illustration. The original screenshot was lost with the old site; this drawing follows the article text.

The image above is a visual representation of the manipulation to the query “All Opportunities” when the following Entity Permissions are associated with the user’s Web Roles:

  1. Contacts associated with my Account
    • Entity Name: contact
    • Scope: Account
    • Account Relationship: contact_customer_accounts
  2. My Colleague’s Opportunities
    • Entity Name: opportunity
    • Scope: Parent
    • Parent Entity Permission: Contacts associated with my Account
    • Parent Relationship: opportunity_parent_contact

Option 2: Add to Existing Pages

With the way the Web Template has been built, it can easily be added to any other existing template. A good use case for this is when you have an Entity List template that you want to compare against both the query and the result set. Keep in mind that the Entity List must have Enable Entity Permissions set to True for the comparison to make sense, and any other differences will come from the filters of the view used in the Entity List.

To do this, simply use the include Liquid tag. An example is given below, with the tag added to a Web Template intended to show an Entity List. It uses the same URL parameters as the API, but only shows the query if the parameter “list” is set to “true”.

Optionally, you can also use Liquid to check whether the user has an Admin role to control the flow here.

Liquid/HTML
{% extends 'Layout 1 Column' %}

{% block main %}
  {%- assign etn = etn | default: params.etn | default: 'account' -%}
  {%- assign pk = pk | default: params.pk | default: 'name' -%}
  {%- assign show = show | default: params.show | default: false | boolean -%}
  {%- assign recordid = etn | append: "id" -%}
  {% include 'Page Copy' %}
  {%- if params.list -%}
    {%- capture permissions -%}{% include 'Permissions Query' etn:etn pk:pk recordid:recordid show:show %}{%- endcapture -%}
    <pre lang="xml">{{ permissions | escape }}</pre>
  {%- endif -%}
  {% if page.adx_entitylist %}
    {% include 'entity_list' key: page.adx_entitylist.id %}
  {% endif %}
{% endblock %}

Checking this Web Page with the same parameters as before, plus list=true, gives us the following example. Feel free to play around with other entities – e.g., ?list=true&etn=contact&pk=fullname&show=true to see all of the Contacts included in the Opportunity filter.

Figure 2Example of including in another template

Recreated illustration: Example of Including in Another Template: an Entity List page with list=true shows the escaped Permissions Query output above the list, so the generated query and the visible records can be compared.

  1. Callout 1: The included Permissions Query template, escaped into a <pre> block because list=true is in the URL.
  2. Callout 2: The Entity List itself. With Enable Entity Permissions on, its rows match the query’s ResultCount.

Recreated illustration. The original screenshot was lost with the old site; this drawing follows the article text.

I hope you find this helpful, whether for understanding the security further or for triaging issues. I’ve had success sharing this with developers new to Dynamics & PowerApps Portals to help them understand their changes, and I’ve used it myself to understand how my combinations of Web Roles were impacting the list results I’d see.

Restored in 2026 from the Internet Archive at its original address. Figures are recreated illustrations of lost screenshots; the text was lightly copy-edited.