Security

PowerPortalsPro provides a flexible, code-driven security model that lets you control access at both the table level and the record level. Security is implemented by creating permission handler classes and registering them in the dependency injection container.

Overview

There are two interfaces you can implement depending on the level of control you need:

Tip

If you only need table-level checks (e.g. "authenticated users can read this table"), use ITablePermissionHandler. If you need record-level logic (e.g. "users can only delete records they own"), use ITableRecordPermissionHandler.

Best practice — filter at the source

Permission handlers enforce access, but they are not a substitute for filtering. Record-level handlers run after a page of records is retrieved from Dataverse, so relying on them to hide rows distorts grid paging (a page of 50 can show fewer) and the total count, and wastes queries fetching rows that are then discarded. Whenever data must be scoped to the current user, filter at the query level instead — with an IFetchXmlQueryFilterInterceptor (see below) or a custom view that accounts for the current user — so Dataverse returns only the rows the user should see and grids page accurately. Keep the permission handlers as your enforcement guarantee.

Dataverse-Native Security (Preview)

The recommended security model going forward lets Dataverse itself enforce security for your portal users, built on Power Pages' unified Dataverse security feature. Instead of writing permission handlers in code, you manage access with standard Dataverse security roles — the same roles, privileges, and admin tooling used for internal users. The model is currently in preview — both Microsoft's feature and Power Portals Pro's support for it — and Power Portals Pro will follow Microsoft's release schedule to general availability.

When enabled, PowerPortalsPro automatically provisions a system-managed (C2) systemuser for each portal contact — at registration for new contacts, and lazily at sign-in for existing ones — together with the powerpagesusermapping record that links the contact, the system user, and your Power Pages site. Every Dataverse call for the session then impersonates that system user, so the security roles Power Pages syncs from your web roles and table permissions govern exactly what the user can see and do. The session carries a PortalUserType of ContactStubUser so your UI can always distinguish these provisioned users from real internal users.

Note

To use this security model you must create a Power Pages website in your Dataverse environment — this is what installs the Microsoft framework components and lets you enable the unified-security feature. You do not need a paid Power Pages plan, and you can delete the website itself immediately after installing and enabling the feature. The Power Pages site record (powerpagesite) referenced by PowerPagesSiteId must exist in the environment, since user mappings and security-role sync resolve against it.

Enabling Dataverse-Native Security

Enable the model through SecurityOptions in your server's Program.cs, supplying the id of your Power Pages site record:

The configuration is validated at application startup: when ProvisionSystemUsers is enabled, PowerPagesSiteId must be set and must resolve to an existing powerpagesite record, otherwise the application fails to start with guidance.

Grant access exactly the way Power Pages does: configure web roles and table permissions in the Power Pages Management app, and assign web roles to contacts (through the Management app or programmatically). The platform syncs those into auto-generated security roles (named PowerPages_*) and assigns them automatically as web-role membership changes — because the configuration lives entirely in Power Pages, your portal can move to or from an actual Power Pages site without any friction. Note that role and privilege changes can take from a few seconds up to a couple of minutes to propagate through Dataverse's caches before they take effect.

Follow the step-by-step setup guide

Getting Started

1. Create a Permission Handler

Create a class that implements ITablePermissionHandler or ITableRecordPermissionHandler. Set the Table property to the logical name of the Dataverse table the handler applies to, and implement each permission method.

Here is a simple example that grants full access to all users for a specific table:

2. Register in Dependency Injection

Register your handler in the DI container in your project's Program.cs file (or a service collection extension method):

Since handlers are resolved from the DI container, you can inject any required services (such as a database context or user service) through constructor injection.

Record-Level Security

For scenarios where access depends on the specific record being operated on, implement ITableRecordPermissionHandler. This interface extends ITablePermissionHandler with overloads that receive the TableRecord.

The following example allows all users to read records globally, but restricts write operations to records owned by the current user:

Note

The CanUpdateAsync record-level overload provides both the record with the proposed updates (recordWithUpdates) and the record in its currently persisted state (currentRecord), allowing you to compare values or validate specific field changes.

Required Columns

The RequiredColumns property specifies which columns must be retrieved for the handler to evaluate record-level permissions. The framework automatically adds these columns to FetchXML queries when they are not already included, ensuring the handler always has the data it needs — even if the view or query does not include those columns.

The following example uses the ppp_owningportaluserid lookup column to restrict access to records owned by the current user:

Note

If RequiredColumns returns an empty list and the handler's record-level methods only use record.Id, no additional columns are added to the query. The owner property is always provided by the platform regardless of RequiredColumns.

FetchXML Query Filter Interceptors

In addition to table and record-level security, PowerPortalsPro supports IFetchXmlQueryFilterInterceptor — an interface that lets you modify FetchXML queries before they are executed. This is useful for enforcing row-level filtering, such as ensuring a user can only see records that belong to them or their team.

IFetchXmlQueryFilterInterceptor extends ITablePermissionHandler, so each interceptor also defines table-level permissions via the Table property and the standard permission methods. The interceptor's OnQueryAsync method is only called for queries that target its specified table.

Unlike record-level permission handlers that evaluate each record after retrieval, query filter interceptors modify the query before it reaches Dataverse. This means filtered-out records are never retrieved, which improves both performance and security.

1. Create a Query Filter Interceptor

Implement IFetchXmlQueryFilterInterceptor, set the Table property to the logical name of the table, implement the permission methods, and use the FetchXMLBuilder in OnQueryAsync to add filters to the query. The FetchXmlParameters record provides the current user's EntityReference. FetchXMLBuilder

The following example restricts queries on the contact table to only return the record matching the current user:

2. Register in Dependency Injection

Register your interceptor as both an IFetchXmlQueryFilterInterceptor and an ITablePermissionHandler so that both the query filtering and table-level permissions are applied. Multiple interceptors can be registered and each will be executed for queries targeting its table.

Tip

Because IFetchXmlQueryFilterInterceptor extends ITablePermissionHandler, a single class handles both table-level permissions and query-level filtering. The OnQueryAsync method is only invoked for queries matching the interceptor's Table property, so there is no need to check the table name inside the method.

Built-in Base Classes

The framework provides base classes to simplify common security patterns:

API Reference

ITablePermissionHandler Interface

Properties

Name
Type
Default
Description
Tablestring
Logical name of the table this permission handler applies to.
Name: Table
Type: string
Description: Logical name of the table this permission handler applies to.

Methods

Name
Parameters
Type
Description
CanAppendAsyncGuid? userId
Task<bool>
Method to determine whether a user should be able to append a record.
CanAppendToAsyncGuid? userId
Task<bool>
Method to determine whether a user should be able to append to a record.
CanCreateAsyncGuid? userId
Task<bool>
Method to determine whether a user should be able to create a record.
CanDeleteAsyncGuid? userId
Task<bool>
Method to determine whether a user should be able to delete a record.
CanReadAsyncGuid? userId
Task<bool>
Method to determine whether a user should be able to read a record.
CanUpdateAsyncGuid? userId
Task<bool>
Method to determine whether a user should be able to update a record.
Name: CanAppendAsync
Parameters: Guid? userId
Type: Task<bool>
Description: Method to determine whether a user should be able to append a record.
Name: CanAppendToAsync
Parameters: Guid? userId
Type: Task<bool>
Description: Method to determine whether a user should be able to append to a record.
Name: CanCreateAsync
Parameters: Guid? userId
Type: Task<bool>
Description: Method to determine whether a user should be able to create a record.
Name: CanDeleteAsync
Parameters: Guid? userId
Type: Task<bool>
Description: Method to determine whether a user should be able to delete a record.
Name: CanReadAsync
Parameters: Guid? userId
Type: Task<bool>
Description: Method to determine whether a user should be able to read a record.
Name: CanUpdateAsync
Parameters: Guid? userId
Type: Task<bool>
Description: Method to determine whether a user should be able to update a record.

ITableRecordPermissionHandler Interface

Properties

Name
Type
Default
Description
RequiredColumnsList<string>
List of column logical names that must be retrieved for this handler to evaluate record-level permissions.
Tablestring
Logical name of the table this permission handler applies to.
Name: RequiredColumns
Type: List<string>
Description: List of column logical names that must be retrieved for this handler to evaluate record-level permissions.
Name: Table
Type: string
Description: Logical name of the table this permission handler applies to.

Methods

Name
Parameters
Type
Description
CanAppendAsyncGuid? userId
TableRecord record
Task<bool>
Method to determine whether a user should be able to append the provided record.
CanAppendToAsyncGuid? userId
TableRecord record
Task<bool>
Method to determine whether a user should be able to append to the provided record.
CanCreateAsyncGuid? userId
TableRecord record
Task<bool>
Method to determine whether a user should be able to create the provided record.
CanDeleteAsyncGuid? userId
TableRecord record
Task<bool>
Method to determine whether a user should be able to delete the provided record.
CanReadAsyncGuid? userId
TableRecord record
Task<bool>
Method to determine whether a user should be able to read the provided record.
CanUpdateAsyncGuid? userId
TableRecord recordWithUpdates
TableRecord currentRecord
Task<bool>
Method to determine whether a user should be able to update the provided record.
Name: CanAppendAsync
Parameters: Guid? userId
TableRecord record
Type: Task<bool>
Description: Method to determine whether a user should be able to append the provided record.
Name: CanAppendToAsync
Parameters: Guid? userId
TableRecord record
Type: Task<bool>
Description: Method to determine whether a user should be able to append to the provided record.
Name: CanCreateAsync
Parameters: Guid? userId
TableRecord record
Type: Task<bool>
Description: Method to determine whether a user should be able to create the provided record.
Name: CanDeleteAsync
Parameters: Guid? userId
TableRecord record
Type: Task<bool>
Description: Method to determine whether a user should be able to delete the provided record.
Name: CanReadAsync
Parameters: Guid? userId
TableRecord record
Type: Task<bool>
Description: Method to determine whether a user should be able to read the provided record.
Name: CanUpdateAsync
Parameters: Guid? userId
TableRecord recordWithUpdates
TableRecord currentRecord
Type: Task<bool>
Description: Method to determine whether a user should be able to update the provided record.

IFetchXmlQueryFilterInterceptor Interface

Properties

Name
Type
Default
Description
Tablestring
Logical name of the table this permission handler applies to.
Name: Table
Type: string
Description: Logical name of the table this permission handler applies to.

Methods

Name
Parameters
Type
Description
CanAppendAsyncGuid? userId
Task<bool>
Method to determine whether a user should be able to append a record.
CanAppendToAsyncGuid? userId
Task<bool>
Method to determine whether a user should be able to append to a record.
CanCreateAsyncGuid? userId
Task<bool>
Method to determine whether a user should be able to create a record.
CanDeleteAsyncGuid? userId
Task<bool>
Method to determine whether a user should be able to delete a record.
CanReadAsyncGuid? userId
Task<bool>
Method to determine whether a user should be able to read a record.
CanUpdateAsyncGuid? userId
Task<bool>
Method to determine whether a user should be able to update a record.
OnQueryAsyncFetchXMLBuilder fetchXmlBuilder
FetchXmlParameters parameters
Task<FetchXMLBuilder>
Called to allow modification of a FetchXML query before it is executed.
Name: CanAppendAsync
Parameters: Guid? userId
Type: Task<bool>
Description: Method to determine whether a user should be able to append a record.
Name: CanAppendToAsync
Parameters: Guid? userId
Type: Task<bool>
Description: Method to determine whether a user should be able to append to a record.
Name: CanCreateAsync
Parameters: Guid? userId
Type: Task<bool>
Description: Method to determine whether a user should be able to create a record.
Name: CanDeleteAsync
Parameters: Guid? userId
Type: Task<bool>
Description: Method to determine whether a user should be able to delete a record.
Name: CanReadAsync
Parameters: Guid? userId
Type: Task<bool>
Description: Method to determine whether a user should be able to read a record.
Name: CanUpdateAsync
Parameters: Guid? userId
Type: Task<bool>
Description: Method to determine whether a user should be able to update a record.
Name: OnQueryAsync
Parameters: FetchXMLBuilder fetchXmlBuilder
FetchXmlParameters parameters
Type: Task<FetchXMLBuilder>
Description: Called to allow modification of a FetchXML query before it is executed.