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:
ITablePermissionHandler— Controls whether a user can perform Create, Read, Update, Delete, Append, or AppendTo operations on a given table. Decisions are based solely on the user's identity.ITableRecordPermissionHandler— ExtendsITablePermissionHandlerwith record-level overloads that receive the actualTableRecordbeing operated on, enabling fine-grained rules such as "users can only edit their own records."
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"), useITableRecordPermissionHandler.
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
CanUpdateAsyncrecord-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
RequiredColumnsreturns an empty list and the handler's record-level methods only userecord.Id, no additional columns are added to the query. Theownerproperty is always provided by the platform regardless ofRequiredColumns.
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
IFetchXmlQueryFilterInterceptorextendsITablePermissionHandler, a single class handles both table-level permissions and query-level filtering. TheOnQueryAsyncmethod is only invoked for queries matching the interceptor'sTableproperty, 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:
AnonymousPermissionHandler— Grants all operations to all users (including anonymous). Useful for publicly accessible tables. Simply inherit and set theTableproperty.
API Reference
ITablePermissionHandler Interface
Properties
Name | Type | Default | Description |
|---|---|---|---|
Table | string | Logical name of the table this permission handler applies to. |
TableMethods
Name | Parameters | Type | Description |
|---|---|---|---|
CanAppendAsync | Guid? userId | Task<bool> | Method to determine whether a user should be able to append a record. |
CanAppendToAsync | Guid? userId | Task<bool> | Method to determine whether a user should be able to append to a record. |
CanCreateAsync | Guid? userId | Task<bool> | Method to determine whether a user should be able to create a record. |
CanDeleteAsync | Guid? userId | Task<bool> | Method to determine whether a user should be able to delete a record. |
CanReadAsync | Guid? userId | Task<bool> | Method to determine whether a user should be able to read a record. |
CanUpdateAsync | Guid? userId | Task<bool> | Method to determine whether a user should be able to update a record. |
CanAppendAsyncCanAppendToAsyncCanCreateAsyncCanDeleteAsyncCanReadAsyncCanUpdateAsyncITableRecordPermissionHandler Interface
Properties
Name | Type | Default | Description |
|---|---|---|---|
RequiredColumns | List<string> | List of column logical names that must be retrieved for this handler to evaluate record-level permissions. | |
Table | string | Logical name of the table this permission handler applies to. |
RequiredColumnsTableMethods
Name | Parameters | Type | Description |
|---|---|---|---|
CanAppendAsync | Guid? userId TableRecord record | Task<bool> | Method to determine whether a user should be able to append the provided record. |
CanAppendToAsync | Guid? userId TableRecord record | Task<bool> | Method to determine whether a user should be able to append to the provided record. |
CanCreateAsync | Guid? userId TableRecord record | Task<bool> | Method to determine whether a user should be able to create the provided record. |
CanDeleteAsync | Guid? userId TableRecord record | Task<bool> | Method to determine whether a user should be able to delete the provided record. |
CanReadAsync | Guid? userId TableRecord record | Task<bool> | Method to determine whether a user should be able to read the provided record. |
CanUpdateAsync | Guid? userId TableRecord recordWithUpdates TableRecord currentRecord | Task<bool> | Method to determine whether a user should be able to update the provided record. |
CanAppendAsyncTableRecord record
CanAppendToAsyncTableRecord record
CanCreateAsyncTableRecord record
CanDeleteAsyncTableRecord record
CanReadAsyncTableRecord record
CanUpdateAsyncTableRecord recordWithUpdates
TableRecord currentRecord
IFetchXmlQueryFilterInterceptor Interface
Properties
Name | Type | Default | Description |
|---|---|---|---|
Table | string | Logical name of the table this permission handler applies to. |
TableMethods
Name | Parameters | Type | Description |
|---|---|---|---|
CanAppendAsync | Guid? userId | Task<bool> | Method to determine whether a user should be able to append a record. |
CanAppendToAsync | Guid? userId | Task<bool> | Method to determine whether a user should be able to append to a record. |
CanCreateAsync | Guid? userId | Task<bool> | Method to determine whether a user should be able to create a record. |
CanDeleteAsync | Guid? userId | Task<bool> | Method to determine whether a user should be able to delete a record. |
CanReadAsync | Guid? userId | Task<bool> | Method to determine whether a user should be able to read a record. |
CanUpdateAsync | Guid? userId | Task<bool> | Method to determine whether a user should be able to update a record. |
OnQueryAsync | FetchXMLBuilder fetchXmlBuilder FetchXmlParameters parameters | Task<FetchXMLBuilder> | Called to allow modification of a FetchXML query before it is executed. |
CanAppendAsyncCanAppendToAsyncCanCreateAsyncCanDeleteAsyncCanReadAsyncCanUpdateAsyncOnQueryAsyncFetchXmlParameters parameters
