Implement Custom Security Objects (Users, Roles, Operation Permissions)
- 8 minutes to read
This example illustrates how to create custom security objects such as permissions, roles, and users. You can implement a permission an administrator uses to allow or deny users to export data in an XAF application. Each role exposes the CanExport
property (a custom role object is implemented for this purpose). When the corresponding checkbox is checked, users associated with this role have access to the ExportController.ExportAction Action.
Implement a Custom Role Object
Extend the PermissionPolicyRole class with an additional CanExport
property. This property indicates that a role has the ExportPermission
.
using DevExpress.Persistent.BaseImpl.EF.PermissionPolicy;
// ...
[DefaultClassOptions,ImageName("BO_Role")]
public class ExtendedSecurityRole : PermissionPolicyRole {
public virtual bool CanExport { get; set; }
}
// Make sure that you use options.UseChangeTrackingProxies() in your DbContext settings.
To use the custom ExtendedSecurityRole
role instead of the default role, modify the SecurityStrategyComplex.RoleType values, as shown below:
File: SolutionName.Blazor.Server/Startup.cs, SolutionName.Win/Startup.cs
public class Startup {
// ...
public void ConfigureServices(IServiceCollection services) {
// ...
services.AddXaf(Configuration, builder => {
builder.Security
.UseIntegratedMode(options => {
options.RoleType = typeof(ExtendedSecurityRole);
// ...
}
}
}
}
Customize the ApplicationUser Object
For users, the XAF Solution Wizard automatically generates an ApplicationUser
persistent class. This class extends the default PermissionPolicyUser class with the ISecurityUserWithLoginInfo interface that is required to associate multiple authentication methods with a user.
Although this is not required to replicate this example, you can customize the ApplicationUser
class according to your needs. For example, add an AppplicationUser.Tasks
collection property so that each user can be assigned a list of tasks:
// ...
public class ApplicationUser : PermissionPolicyUser, ISecurityUserWithLoginInfo
{
// ...
[DevExpress.ExpressApp.DC.Aggregated]
public virtual IList<Task> Tasks { get; set; } = new ObservableCollection<Task>();
}
[DefaultClassOptions]
[ImageName("BO_Task")]
public class Task : BaseObject {
public virtual string Subject { get; set; }
public virtual DateTime DueDate { get; set; }
public virtual ApplicationUser AssignedTo { get; set; }
}
Implement Custom Operation Permission and Permission Request
Add a class that supports the IOperationPermission interface to implement a custom operation permission.
File: SolutionName.Module/Security/ExportPermission.cs
using DevExpress.ExpressApp.Security;
// ...
public class ExportPermission : IOperationPermission {
public string Operation {
get { return "Export"; }
}
}
The Security System uses permission requests to determine whether permission is granted. To add a permission request for the ExportPermission
, implement the IPermissionRequest interface as follows:
File: SolutionName.Module/Security/ExportPermissionRequest.cs
public class ExportPermissionRequest : IPermissionRequest {
public object GetHashObject() {
return this.GetType().FullName;
}
}
Implement the Permission Request Processor and Register it in the Security Strategy
All permission requests should have a permission request processor registered in the Security Strategy. Inherit the PermissionRequestProcessorBase<ProcessorPermissionRequestType> class and pass the permission request type as the ancestor class’s generic parameter to implement a processor.
File: SolutionName.Module/Security/ExportPermissionRequestProcessor.cs
public class ExportPermissionRequestProcessor :
PermissionRequestProcessorBase<ExportPermissionRequest> {
private IPermissionDictionary permissions;
public ExportPermissionRequestProcessor(IPermissionDictionary permissions) {
this.permissions = permissions;
}
public override bool IsGranted(ExportPermissionRequest permissionRequest) {
return (permissions.FindFirst<ExportPermission>() != null);
}
}
Handle the SecurityStrategy.CustomizeRequestProcessors event in the Startup.cs files in the ASP.NET Core Blazor and WinForms application projects to register the ExportPermissionRequestProcessor
. Subscribe to this event before the XafApplication.Setup method is called.
In the event handler, pass the ExportPermission
object to the PermissionDictionary
as shown below.
Note
You do not need to handle this event on the client side if your XAF application uses the Middle Tier application server. Instead, subscribe to this event in the server’s Startup.cs file.
File: SolutionName.Blazor.Server/Startup.cs, SolutionName.Win/Startup.cs
public class Startup {
// ...
public void ConfigureServices(IServiceCollection services) {
// ...
services.AddXaf(Configuration, builder => {
// ...
builder.Security
.UseIntegratedMode(options => {
options.Events.OnSecurityStrategyCreated = securityStrategy => {
((SecurityStrategy)securityStrategy).CustomizeRequestProcessors += delegate (object sender, CustomizeRequestProcessorsEventArgs e) {
List<IOperationPermission> result = new List<IOperationPermission>();
SecurityStrategyComplex security = sender as SecurityStrategyComplex;
if (security != null) {
ApplicationUser user = security.User as ApplicationUser;
if (user != null) {
foreach (ExtendedSecurityRole role in user.Roles) {
if (role.CanExport) {
result.Add(new ExportPermission());
}
}
}
}
IPermissionDictionary permissionDictionary = new PermissionDictionary((IEnumerable<IOperationPermission>)result);
e.Processors.Add(typeof(ExportPermissionRequest), new ExportPermissionRequestProcessor(permissionDictionary));
};
};
}
}
}
}
Apply the Custom Permission to the ExportController Controller
Add the following View Controller to inform the ExportController controller about the ExportPermission
:
File: SolutionName.Module/Security/SecuredExportController.cs
public class SecuredExportController : ViewController {
protected override void OnActivated() {
base.OnActivated();
ExportController controller = Frame.GetController<ExportController>();
if (controller != null) {
controller.ExportAction.Executing += ExportAction_Executing;
IRequestSecurity requestSecurity = (IRequestSecurity)Application.Security;
controller.Active.SetItemValue("Security",
requestSecurity.IsGranted(new ExportPermissionRequest()));
}
}
void ExportAction_Executing(object sender, System.ComponentModel.CancelEventArgs e) {
IRequestSecurity requestSecurity = (IRequestSecurity)Application.Security;
if (!requestSecurity.IsGranted(new ExportPermissionRequest())) {
throw new UserFriendlyException("Export operation is prohibited.");
}
}
}
The code above deactivates the controller for users who are not allowed to export data. The ActionBase.Executing event handler demonstrates how to throw an exception when a user attempts to perform a prohibited operation.
Add Demo Data
Add the predefined user and role objects in the ModuleUpdater.UpdateDatabaseAfterUpdateSchema method as shown below.
File: SolutionName.Module/DatabaseUpdate/Updater.cs
public class Updater : ModuleUpdater {
public Updater(IObjectSpace objectSpace, Version currentDBVersion) :
base(objectSpace, currentDBVersion) {
}
public override void UpdateDatabaseAfterUpdateSchema() {
base.UpdateDatabaseAfterUpdateSchema();
#if !RELEASE
ExtendedSecurityRole defaultRole = CreateDefaultRole();
ExtendedSecurityRole exportRole = CreateExporterRole();
ApplicationUser sampleUser = ObjectSpace.FirstOrDefault<ApplicationUser>(u => u.UserName == "User");
if (sampleUser == null) {
sampleUser = ObjectSpace.CreateObject<ApplicationUser>();
sampleUser.UserName = "User";
sampleUser.SetPassword("");
ObjectSpace.CommitChanges();
((ISecurityUserWithLoginInfo)sampleUser).CreateUserLoginInfo(SecurityDefaults.PasswordAuthentication, ObjectSpace.GetKeyValueAsString(sampleUser));
sampleUser.Roles.Add(defaultRole);
}
ApplicationUser exportUser = ObjectSpace.FirstOrDefault<ApplicationUser>(u => u.UserName == "exportUser");
if (exportUser == null) {
exportUser = ObjectSpace.CreateObject<ApplicationUser>();
exportUser.UserName = "exportUser";
exportUser.SetPassword("");
ObjectSpace.CommitChanges();
((ISecurityUserWithLoginInfo)exportUser).CreateUserLoginInfo(SecurityDefaults.PasswordAuthentication, ObjectSpace.GetKeyValueAsString(exportUser));
exportUser.Roles.Add(defaultRole);
exportUser.Roles.Add(exportRole);
}
ApplicationUser userAdmin = ObjectSpace.FirstOrDefault<ApplicationUser>(u => u.UserName == "Admin");
if (userAdmin == null) {
userAdmin = ObjectSpace.CreateObject<ApplicationUser>();
userAdmin.UserName = "Admin";
userAdmin.SetPassword("");
ObjectSpace.CommitChanges();
((ISecurityUserWithLoginInfo)userAdmin).CreateUserLoginInfo(SecurityDefaults.PasswordAuthentication, ObjectSpace.GetKeyValueAsString(userAdmin));
}
ExtendedSecurityRole adminRole = ObjectSpace.FirstOrDefault<ExtendedSecurityRole>(r => r.Name == "Administrators");
if (adminRole == null) {
adminRole = ObjectSpace.CreateObject<ExtendedSecurityRole>();
adminRole.Name = "Administrators";
}
adminRole.IsAdministrative = true;
userAdmin.Roles.Add(adminRole);
var cnt = ObjectSpace.GetObjects<Contact>().Count;
if (cnt > 0) {
return;
}
for (int i = 0; i < 5; i++) {
string contactName = "FirstName" + i;
var contact = ObjectSpace.CreateObject<Contact>();
contact.FirstName = contactName;
contact.LastName = "LastName" + i;
contact.Age = i * 10;
}
ObjectSpace.CommitChanges();
#endif
}
public override void UpdateDatabaseBeforeUpdateSchema() {
base.UpdateDatabaseBeforeUpdateSchema();
}
private ExtendedSecurityRole CreateDefaultRole() {
ExtendedSecurityRole defaultRole = ObjectSpace.FirstOrDefault<ExtendedSecurityRole>(role => role.Name == "Default");
if (defaultRole == null) {
defaultRole = ObjectSpace.CreateObject<ExtendedSecurityRole>();
defaultRole.Name = "Default";
defaultRole.AddObjectPermissionFromLambda<ApplicationUser>(SecurityOperations.Read, cm => cm.ID == (Guid)CurrentUserIdOperator.CurrentUserId(), SecurityPermissionState.Allow);
defaultRole.AddNavigationPermission(@"Application/NavigationItems/Items/Default/Items/MyDetails", SecurityPermissionState.Allow);
defaultRole.AddMemberPermissionFromLambda<ApplicationUser>(SecurityOperations.Write, "ChangePasswordOnFirstLogon", cm => cm.ID == (Guid)CurrentUserIdOperator.CurrentUserId(), SecurityPermissionState.Allow);
defaultRole.AddMemberPermissionFromLambda<ApplicationUser>(SecurityOperations.Write, "StoredPassword", cm => cm.ID == (Guid)CurrentUserIdOperator.CurrentUserId(), SecurityPermissionState.Allow);
defaultRole.AddTypePermissionsRecursively<ExtendedSecurityRole>(SecurityOperations.Read, SecurityPermissionState.Deny);
defaultRole.AddTypePermissionsRecursively<ModelDifference>(SecurityOperations.ReadWriteAccess, SecurityPermissionState.Allow);
defaultRole.AddTypePermissionsRecursively<ModelDifferenceAspect>(SecurityOperations.ReadWriteAccess, SecurityPermissionState.Allow);
defaultRole.AddTypePermissionsRecursively<ModelDifference>(SecurityOperations.Create, SecurityPermissionState.Allow);
defaultRole.AddTypePermissionsRecursively<ModelDifferenceAspect>(SecurityOperations.Create, SecurityPermissionState.Allow);
defaultRole.AddTypePermission<Contact>(SecurityOperations.CRUDAccess, SecurityPermissionState.Allow);
defaultRole.AddNavigationPermission(@"Application/NavigationItems/Items/Default/Items/Contact_ListView", SecurityPermissionState.Allow);
}
return defaultRole;
}
private ExtendedSecurityRole CreateExporterRole() {
ExtendedSecurityRole exporterRole = ObjectSpace.FirstOrDefault<ExtendedSecurityRole>(role => role.Name == "Exporter");
if (exporterRole == null) {
exporterRole = ObjectSpace.CreateObject<ExtendedSecurityRole>();
exporterRole.Name = "Exporter";
exporterRole.CanExport = true;
exporterRole.AddTypePermission<Contact>(SecurityOperations.CRUDAccess, SecurityPermissionState.Allow);
exporterRole.AddNavigationPermission(@"Application/NavigationItems/Items/Default/Items/Contact_ListView", SecurityPermissionState.Allow);
}
return exporterRole;
}
}
Ensure that the Application UI Changes Based on the Role
Run the application and log in as “Admin”. Open the extended security role object’s detail view. The Can Export checkbox is checked for the “Exporter” role.
Log in again as “exportUser” who has the “Exporter” role. Ensure that the Export action is available for this user. If you log in as “User”, the Export action is unavailable.
Note
The Export action is available for the “Admin” user even though the “Can Export” option is not checked for the “Administrator” role. The “Is Administrative” option overrides all other permissions (built-in and custom) and grants full access to all operations.