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.

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:

public class MyTablePermissionHandler : ITablePermissionHandler
{
    public string Table => "my_customtable";

    public Task<bool> CanCreateAsync(Guid? userId) => Task.FromResult(true);
    public Task<bool> CanReadAsync(Guid? userId) => Task.FromResult(true);
    public Task<bool> CanUpdateAsync(Guid? userId) => Task.FromResult(true);
    public Task<bool> CanDeleteAsync(Guid? userId) => Task.FromResult(true);
    public Task<bool> CanAppendAsync(Guid? userId) => Task.FromResult(true);
    public Task<bool> CanAppendToAsync(Guid? userId) => Task.FromResult(true);
}

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):

builder.Services.AddSingleton<ITablePermissionHandler, MyTablePermissionHandler>();

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:

public class ContactPermissionHandler : ITableRecordPermissionHandler
{
    public string Table => "contact";
    public List<string> RequiredColumns => [];

    // Table-level: allow access broadly
    public Task<bool> CanReadAsync(Guid? userId) => Task.FromResult(true);
    public Task<bool> CanCreateAsync(Guid? userId)
        => Task.FromResult(userId != null && userId != Guid.Empty);

    // Record-level: restrict writes to the record owner
    public Task<bool> CanUpdateAsync(
        Guid? userId, TableRecord recordWithUpdates, TableRecord currentRecord)
    {
        return Task.FromResult(userId == currentRecord.Id);
    }

    public Task<bool> CanDeleteAsync(Guid? userId, TableRecord record)
    {
        return Task.FromResult(userId == record.Id);
    }

    // ... implement remaining methods
}

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:

public class RegionPermissionHandler : ITableRecordPermissionHandler
{
    public string Table => "ppp_region";
    public List<string> RequiredColumns => ["ppp_owningportaluserid"];

    public Task<bool> CanReadAsync(Guid? userId) => Task.FromResult(true);
    public Task<bool> CanCreateAsync(Guid? userId) => Task.FromResult(userId != null);

    public Task<bool> CanReadAsync(Guid? userId, TableRecord record)
    {
        return Task.FromResult(
            record.GetValueOrDefault<LookupValue>("ppp_owningportaluserid")?.Value == userId);
    }

    // ... implement remaining methods
}

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.

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

public class ContactQueryFilterInterceptor : IFetchXmlQueryFilterInterceptor
{
    public string Table => "contact";

    public Task<bool> CanCreateAsync(Guid? userId) => Task.FromResult(false);
    public Task<bool> CanReadAsync(Guid? userId) => Task.FromResult(true);
    public Task<bool> CanUpdateAsync(Guid? userId) => Task.FromResult(false);
    public Task<bool> CanDeleteAsync(Guid? userId) => Task.FromResult(false);
    public Task<bool> CanAppendAsync(Guid? userId) => Task.FromResult(false);
    public Task<bool> CanAppendToAsync(Guid? userId) => Task.FromResult(false);

    public Task<FetchXMLBuilder> OnQueryAsync(
        FetchXMLBuilder fetchXmlBuilder, FetchXmlParameters parameters)
    {
        var filter = fetchXmlBuilder.Fetch.Entity.AddFilter();
        var condition = filter.AddCondition();
        condition.Column = "contactid";
        condition.Operator = ConditionOperator.Equal;
        condition.Value = parameters.CurrentUser.Id.ToString();

        return Task.FromResult(fetchXmlBuilder);
    }
}

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.

builder.Services.AddTransient<ContactQueryFilterInterceptor>();
builder.Services.AddTransient<ITablePermissionHandler>(
    sp => sp.GetRequiredService<ContactQueryFilterInterceptor>());
builder.Services.AddTransient<IFetchXmlQueryFilterInterceptor>(
    sp => sp.GetRequiredService<ContactQueryFilterInterceptor>());

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.