expressappframework-113452-data-security-and-safety-security-system-security-object-model-implement-custom-security-objects-implement-a-custom-security-system-user-based-on-an-existing-business-class.md
Consider the following situation. You have an unsecure XAF application, and its business model includes the Employee business class. This class exposes information such as personal data, the associated department, and assigned tasks. When enabling the Security System, a User object is added to the business model, but the Users who log in to your application are Employees. This topic explains how to merge the User and Employee into a single entity. For this purpose, several security-related interfaces in the Employee class will be supported, and as a result, the Security System will recognize the Employee type as one of the possible User types. You will assign the Employee type to the SecurityStrategy.UserType property in the Application Designer. As an additional benefit, it will be possible to use the CurrentUserId() Function Criteria Operator to get the identifier of the current Employee (for example, to define a “tasks assigned to me” List View filter).
Tip
A complete sample project is available in the DevExpress Code Examples database at https://supportcenter.devexpress.com/ticket/details/e4160/xaf-how-to-implement-a-security-system-user-based-on-an-existing-business-class.
Note
Employee class from PermissionPolicyUser. To see an example, refer to the following topic: How to: Implement Custom Security Objects (Users, Roles, Operation Permissions).Start with a new XAF solution. Add the following Employee and EmployeeTask business classes to the module project.
[DefaultClassOptions]
public class Employee : Person {
public virtual IList<EmployeeTask> OwnTasks { get; set; } = new ObservableCollection<EmployeeTask>();
}
public class Person : BaseObject {
public virtual string FirstName { get; set; }
public virtual string LastName { get; set; }
}
[DefaultClassOptions,ImageName("BO_Task")]
public class EmployeeTask : BaseObject {
public virtual string Subject { get; set; }
public virtual Employee Owner { get; set; }
}
// Make sure that you use options.UseChangeTrackingProxies() in your DbContext settings.
[DefaultClassOptions]
public class Employee : Person {
public Employee(Session session)
: base(session) { }
[Association("Employee-Task")]
public XPCollection<EmployeeTask> OwnTasks {
get { return GetCollection<EmployeeTask>(nameof(OwnTasks)); }
}
}
[DefaultClassOptions, ImageName("BO_Task")]
public class EmployeeTask : Task {
public EmployeeTask(Session session)
: base(session) { }
private Employee owner;
[Association("Employee-Task")]
public Employee Owner {
get { return owner; }
set { SetPropertyValue(nameof(Owner), ref owner, value); }
}
}
Add a reference to the DevExpress.ExpressApp.Security.v25.2.dll assembly for the project that contains the Employee class. Extend the Employee class with the following code:
using DevExpress.ExpressApp.Security;
using DevExpress.Persistent.Validation;
// ...
public class Employee : Person, ISecurityUser {
// ...
#region ISecurityUser Members
public virtual bool IsActive { get; set; } = true;
[RuleRequiredField("EmployeeUserNameRequired", DefaultContexts.Save)]
[RuleUniqueValue("EmployeeUserNameIsUnique", DefaultContexts.Save,
"The login with the entered user name was already registered within the system.")]
public virtual string UserName { get; set; }
#endregion
}
// Make sure that you use options.UseChangeTrackingProxies() in your DbContext settings.
using DevExpress.ExpressApp.Security;
using DevExpress.Persistent.Validation;
// ...
public class Employee : Person, ISecurityUser {
// ...
#region ISecurityUser Members
private bool isActive = true;
public bool IsActive {
get { return isActive; }
set { SetPropertyValue(nameof(IsActive), ref isActive, value); }
}
private string userName = String.Empty;
[RuleRequiredField("EmployeeUserNameRequired", DefaultContexts.Save)]
[RuleUniqueValue("EmployeeUserNameIsUnique", DefaultContexts.Save,
"The login with the entered user name was already registered within the system.")]
public string UserName {
get { return userName; }
set { SetPropertyValue(nameof(UserName), ref userName, value); }
}
#endregion
}
Refer to the ISecurityUser interface description for details on this interface and its members.
Note
If you are not planning to use the AuthenticationStandard authentication type, skip this section.
Extend the Employee class with the following code:
using System.ComponentModel;
using DevExpress.ExpressApp.Security;
using DevExpress.Persistent.Base;
using DevExpress.Persistent.Base.Security;
// ...
public class Employee : Person, ISecurityUser, IAuthenticationStandardUser {
// ...
#region IAuthenticationStandardUser Members
public virtual bool ChangePasswordOnFirstLogon { get; set; }
[Browsable(false), FieldSize(FieldSizeAttribute.Unlimited), SecurityBrowsable]
public virtual string StoredPassword { get; set; }
public bool ComparePassword(string password) {
return PasswordCryptographer.VerifyHashedPasswordDelegate(this.StoredPassword, password);
}
public void SetPassword(string password) {
this.StoredPassword = PasswordCryptographer.HashPasswordDelegate(password);
}
#endregion
}
// Make sure that you use options.UseChangeTrackingProxies() in your DbContext settings.
using System.ComponentModel;
using DevExpress.ExpressApp.Security;
using DevExpress.Persistent.Base;
using DevExpress.Persistent.Base.Security;
// ...
public class Employee : Person, ISecurityUser, IAuthenticationStandardUser {
// ...
#region IAuthenticationStandardUser Members
private bool changePasswordOnFirstLogon;
public bool ChangePasswordOnFirstLogon {
get { return changePasswordOnFirstLogon; }
set {
SetPropertyValue(nameof(ChangePasswordOnFirstLogon), ref changePasswordOnFirstLogon, value);
}
}
private string storedPassword;
[Browsable(false), Size(SizeAttribute.Unlimited), Persistent, SecurityBrowsable]
public string StoredPassword {
get { return storedPassword; }
set { storedPassword = value; }
}
public bool ComparePassword(string password) {
return PasswordCryptographer.VerifyHashedPasswordDelegate(this.storedPassword, password);
}
public void SetPassword(string password) {
this.storedPassword = PasswordCryptographer.HashPasswordDelegate(password);
OnChanged(nameof(StoredPassword));
}
#endregion
}
Refer to the IAuthenticationStandardUser interface description for details on this interface and its members.
Note
If you are not planning to use the AuthenticationActiveDirectory authentication type, skip this section.
Add the IAuthenticationActiveDirectoryUser interface to the supported interfaces list of the Employee class.
using DevExpress.ExpressApp.Security;
using DevExpress.Persistent.Base.Security;
// ...
public class Employee : Person, ISecurityUser,
IAuthenticationStandardUser, IAuthenticationActiveDirectoryUser {
// ...
}
using DevExpress.ExpressApp.Security;
using DevExpress.Persistent.Base.Security;
// ...
public class Employee : Person, ISecurityUser,
IAuthenticationStandardUser, IAuthenticationActiveDirectoryUser {
// ...
}
The IAuthenticationActiveDirectoryUser.UserName property declared by this interface has already been implemented in your code as a part of the ISecurityUser interface.
Extend the Employee class with the following code:
using DevExpress.ExpressApp.Security;
using DevExpress.Persistent.Base.Security;
using DevExpress.Persistent.Validation;
using System.Collections.ObjectModel;
//...
[DefaultClassOptions]
public class Employee : Person, ISecurityUser,
IAuthenticationStandardUser, IAuthenticationActiveDirectoryUser, ISecurityUserWithRoles {
// ...
#region ISecurityUserWithRoles Members
IList<ISecurityRole> ISecurityUserWithRoles.Roles {
get {
IList<ISecurityRole> result = new List<ISecurityRole>();
foreach (EmployeeRole role in EmployeeRoles) {
result.Add(role);
}
return result;
}
}
[RuleRequiredField("EmployeeRoleIsRequired", DefaultContexts.Save,
TargetCriteria = "IsActive",
CustomMessageTemplate = "An active employee must have at least one role assigned")]
public virtual IList<EmployeeRole> EmployeeRoles { get; set; } = new ObservableCollection<EmployeeRole>();
#endregion
}
using DevExpress.ExpressApp.Security;
using DevExpress.Persistent.Base.Security;
using DevExpress.Persistent.Validation;
//...
[DefaultClassOptions]
public class Employee : Person, ISecurityUser,
IAuthenticationStandardUser, IAuthenticationActiveDirectoryUser,
ISecurityUserWithRoles {
// ...
#region ISecurityUserWithRoles Members
IList<ISecurityRole> ISecurityUserWithRoles.Roles {
get {
IList<ISecurityRole> result = new List<ISecurityRole>();
foreach (EmployeeRole role in EmployeeRoles) {
result.Add(role);
}
return result;
}
}
#endregion
[Association("Employees-EmployeeRoles")]
[RuleRequiredField("EmployeeRoleIsRequired", DefaultContexts.Save,
TargetCriteria = "IsActive",
CustomMessageTemplate = "An active employee must have at least one role assigned")]
public XPCollection<EmployeeRole> EmployeeRoles {
get {
return GetCollection<EmployeeRole>(nameof(EmployeeRoles));
}
}
}
Refer to the ISecurityUserWithRoles interface description for details on this interface and its members.
A many-to-many association with the built-in PermissionPolicyRole class cannot be defined (this class is already associated with the PermissionPolicyUser), which is why the following custom Role class should be implemented in the module project.
using System.Linq;
using DevExpress.Persistent.Base;
using DevExpress.Persistent.BaseImpl.PermissionPolicy;
using System.Collections.ObjectModel;
// ...
[ImageName("BO_Role")]
public class EmployeeRole : PermissionPolicyRoleBase, IPermissionPolicyRoleWithUsers {
public virtual IList<Employee> Employees { get; set; } = new ObservableCollection<Employee>();
IEnumerable<IPermissionPolicyUser> IPermissionPolicyRoleWithUsers.Users {
get { return Employees.OfType<IPermissionPolicyUser>(); }
}
}
using System.Linq;
using DevExpress.Persistent.Base;
using DevExpress.Persistent.BaseImpl.PermissionPolicy;
// ...
[ImageName("BO_Role")]
public class EmployeeRole : PermissionPolicyRoleBase, IPermissionPolicyRoleWithUsers {
public EmployeeRole(Session session)
: base(session) {
}
[Association("Employees-EmployeeRoles")]
public XPCollection<Employee> Employees {
get {
return GetCollection<Employee>(nameof(Employees));
}
}
IEnumerable<IPermissionPolicyUser> IPermissionPolicyRoleWithUsers.Users {
get { return Employees.OfType<IPermissionPolicyUser>(); }
}
}
Extend the Employee class with the following code:
using System.Linq;
using DevExpress.ExpressApp.Security;
using DevExpress.Persistent.Base;
using DevExpress.Persistent.Base.Security;
// ...
[DefaultClassOptions]
public class Employee : Person, ISecurityUser,
IAuthenticationStandardUser, IAuthenticationActiveDirectoryUser,
IPermissionPolicyUser {
// ...
#region IPermissionPolicyUser Members
IEnumerable<IPermissionPolicyRole> IPermissionPolicyUser.Roles {
get { return EmployeeRoles.OfType<IPermissionPolicyRole>(); }
}
#endregion
}
Refer to the IPermissionPolicyUser interface description for details on this interface and its members.
The ICanInitialize.Initialize method is used to assign the default role when you use the AuthenticationActiveDirectory authentication and set the AuthenticationActiveDirectory.CreateUserAutomatically property to true. If you do not need to support user autocreation, skip this step. Otherwise, extend the Employee class with the following code:
using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Security;
using DevExpress.Data.Filtering;
using DevExpress.Persistent.Base;
using DevExpress.Persistent.Base.Security;
// ...
[DefaultClassOptions]
public class Employee : Person, ISecurityUser,
IAuthenticationStandardUser, IAuthenticationActiveDirectoryUser,
IPermissionPolicyUser, ICanInitialize {
// ...
#region ICanInitialize Members
void ICanInitialize.Initialize(IObjectSpace objectSpace, SecurityStrategyComplex security) {
EmployeeRole newUserRole = objectSpace.FirstOrDefault<EmployeeRole>(role => role.Name == security.NewUserRoleName);
if (newUserRole == null) {
newUserRole = objectSpace.CreateObject<EmployeeRole>();
newUserRole.Name = security.NewUserRoleName;
newUserRole.IsAdministrative = true;
newUserRole.Employees.Add(this);
}
}
#endregion
}
In applications that support multiple authentication schemes, a user type must implement the ISecurityUserWithLoginInfo interface so that a user account record can store login information for all available schemes. Implement this interface as follows:
using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Security;
using DevExpress.Data.Filtering;
using DevExpress.Persistent.Base;
using DevExpress.Persistent.Base.Security;
using System.ComponentModel.DataAnnotations;
// ...
[DefaultClassOptions]
public class Employee : Person, ISecurityUser,
IAuthenticationStandardUser, IAuthenticationActiveDirectoryUser,
IPermissionPolicyUser, ICanInitialize, ISecurityUserWithLoginInfo {
// ...
#region ISecurityUserWithLoginInfo Members
public Employee() : base() {
// ...
EmployeeLogins = new ObservableCollection<EmployeeLoginInfo>();
}
[Browsable(false)]
[DevExpress.ExpressApp.DC.Aggregated]
public virtual IList<EmployeeLoginInfo> EmployeeLogins { get; set; }
IEnumerable<ISecurityUserLoginInfo> IOAuthSecurityUser.UserLogins => EmployeeLogins.OfType<ISecurityUserLoginInfo>();
ISecurityUserLoginInfo ISecurityUserWithLoginInfo.CreateUserLoginInfo(string loginProviderName, string providerUserKey) {
EmployeeLoginInfo result = ((IObjectSpaceLink)this).ObjectSpace.CreateObject<EmployeeLoginInfo>();
result.LoginProviderName = loginProviderName;
result.ProviderUserKey = providerUserKey;
result.User = this;
return result;
}
#endregion
}
public class EmployeeLoginInfo : ISecurityUserLoginInfo {
public EmployeeLoginInfo() { }
[Browsable(false)]
public virtual Guid ID { get; protected set; }
[Appearance("PasswordProvider", Enabled = false, Criteria = "!(IsNewObject(this)) and LoginProviderName == '" + SecurityDefaults.PasswordAuthentication + "'", Context = "DetailView")]
public virtual string LoginProviderName { get; set; }
[Appearance("PasswordProviderUserKey", Enabled = false, Criteria = "!(IsNewObject(this)) and LoginProviderName == '" + SecurityDefaults.PasswordAuthentication + "'", Context = "DetailView")]
public virtual string ProviderUserKey { get; set; }
[Browsable(false)]
public virtual Guid UserForeignKey { get; set; }
[Required]
[ForeignKey(nameof(UserForeignKey))]
public virtual Employee User { get; set; }
object ISecurityUserLoginInfo.User => User;
}
using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Security;
using DevExpress.Data.Filtering;
using DevExpress.Persistent.Base;
using DevExpress.Persistent.Base.Security;
using System.ComponentModel.DataAnnotations;
// ...
[DefaultClassOptions]
public class Employee : Person, ISecurityUser,
IAuthenticationStandardUser, IAuthenticationActiveDirectoryUser,
IPermissionPolicyUser, ICanInitialize, ISecurityUserWithLoginInfo {
// ...
#region ISecurityUserWithLoginInfo Members
[Browsable(false)]
[Aggregated, Association("User-LoginInfo")]
public XPCollection<EmployeeLoginInfo> LoginInfo {
get { return GetCollection<EmployeeLoginInfo>(nameof(LoginInfo)); }
}
IEnumerable<ISecurityUserLoginInfo> IOAuthSecurityUser.UserLogins => LoginInfo.OfType<ISecurityUserLoginInfo>();
ISecurityUserLoginInfo ISecurityUserWithLoginInfo.CreateUserLoginInfo(string loginProviderName, string providerUserKey) {
EmployeeLoginInfo result = new EmployeeLoginInfo(Session);
result.LoginProviderName = loginProviderName;
result.ProviderUserKey = providerUserKey;
result.User = this;
return result;
}
#endregion
}
[DeferredDeletion(false)]
[Persistent("PermissionPolicyUserLoginInfo")]
public class EmployeeLoginInfo : BaseObject, ISecurityUserLoginInfo {
private string loginProviderName;
private Employee user;
private string providerUserKey;
public EmployeeLoginInfo(Session session) : base(session) { }
[Indexed("ProviderUserKey", Unique = true)]
[Appearance("PasswordProvider", Enabled = false, Criteria = "!(IsNewObject(this)) and LoginProviderName == '" + SecurityDefaults.PasswordAuthentication + "'", Context = "DetailView")]
public string LoginProviderName {
get { return loginProviderName; }
set { SetPropertyValue(nameof(LoginProviderName), ref loginProviderName, value); }
}
[Appearance("PasswordProviderUserKey", Enabled = false, Criteria = "!(IsNewObject(this)) and LoginProviderName == '" + SecurityDefaults.PasswordAuthentication + "'", Context = "DetailView")]
public string ProviderUserKey {
get { return providerUserKey; }
set { SetPropertyValue(nameof(ProviderUserKey), ref providerUserKey, value); }
}
[Association("User-LoginInfo")]
public Employee User {
get { return user; }
set { SetPropertyValue(nameof(User), ref user, value); }
}
object ISecurityUserLoginInfo.User => User;
}
Implement the ISecurityUserLockout interface to support the user lockout feature (the ability to lock out users who fail to enter the correct password several times in a row).
using System.Collections.ObjectModel;
using System.ComponentModel;
using DevExpress.ExpressApp.Security;
using DevExpress.Persistent.Base;
using DevExpress.Persistent.BaseImpl.EF;
using DevExpress.Persistent.BaseImpl.EF.PermissionPolicy;
// ...
public class Employee : Person, ISecurityUser,
IAuthenticationStandardUser, IAuthenticationActiveDirectoryUser,
IPermissionPolicyUser, ICanInitialize, ISecurityUserWithLoginInfo, ISecurityUserLockout{
// ...
#region ISecurityUserLockout Members
[Browsable(false)]
public virtual int AccessFailedCount { get; set; }
[Browsable(false)]
public virtual DateTime LockoutEnd { get; set; }
#endregion
}
using System.ComponentModel;
using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Security;
using DevExpress.Persistent.Base;
using DevExpress.Persistent.BaseImpl.PermissionPolicy;
using DevExpress.Xpo;
// ...
public class Employee : Person, ISecurityUser,
IAuthenticationStandardUser, IAuthenticationActiveDirectoryUser,
IPermissionPolicyUser, ICanInitialize, ISecurityUserWithLoginInfo, ISecurityUserLockout {
// ...
#region ISecurityUserLockout Members
private int accessFailedCount;
private DateTime lockoutEnd;
[Browsable(false)]
public int AccessFailedCount {
get { return accessFailedCount; }
set { SetPropertyValue(nameof(AccessFailedCount), ref accessFailedCount, value); }
}
[Browsable(false)]
public DateTime LockoutEnd {
get { return lockoutEnd; }
set { SetPropertyValue(nameof(LockoutEnd), ref lockoutEnd, value); }
}
#endregion
}
To use the custom EmployeeRole, custom Employee user, and custom EmployeeLoginInfo instead of the default types, modify the SecurityStrategyComplex.RoleType and SecurityStrategy.UserType values, as shown below.
File : MyApplication.Blazor\Startup.cs
public class Startup {
// ...
public void ConfigureServices(IServiceCollection services) {
// ...
services.AddXaf(Configuration, builder => {
builder.Security
.UseIntegratedMode(options => {
// ...
options.RoleType = typeof(EmployeeRole);
options.UserType = typeof(Employee);
options.UserLoginInfoType = typeof(EmployeeLoginInfo);
})
// ...
})
// ...
}
}
File : MyApplication.Win\Startup.cs
public class ApplicationBuilder : IDesignTimeApplicationFactory {
// ...
public static WinApplication BuildApplication(string connectionString) {
var builder = WinApplication.CreateBuilder();
// ..
builder.Security
.UseIntegratedMode(options => {
options.RoleType = typeof(EmployeeRole);
options.UserType = typeof(Employee);
options.UserLoginInfoType = typeof(EmployeeLoginInfo);
// ...
})
// ..
};
}
If you decide to utilize Active Directory authentication, you can skip this section. The minimum requirements for starting with Standard Authentication is an Administrator role and the Administrator user associated with this role. To add these objects, edit the Updater.cs file located in the DatabaseUpdate folder of your module project. Override the ModuleUpdater.UpdateDatabaseAfterUpdateSchema method in the following manner.
using DevExpress.ExpressApp.Security.Strategy;
// ...
public override void UpdateDatabaseAfterUpdateSchema() {
base.UpdateDatabaseAfterUpdateSchema();
EmployeeRole adminEmployeeRole = ObjectSpace.FirstOrDefault<EmployeeRole>(role => role.Name == SecurityStrategy.AdministratorRoleName);
if (adminEmployeeRole == null) {
adminEmployeeRole = ObjectSpace.CreateObject<EmployeeRole>();
adminEmployeeRole.Name = SecurityStrategy.AdministratorRoleName;
adminEmployeeRole.IsAdministrative = true;
}
Employee adminEmployee = ObjectSpace.FirstOrDefault<Employee>(employee => employee.UserName == "Administrator");
if (adminEmployee == null) {
adminEmployee = ObjectSpace.CreateObject<Employee>();
adminEmployee.UserName = "Administrator";
adminEmployee.SetPassword("");
adminEmployee.EmployeeRoles.Add(adminEmployeeRole);
((ISecurityUserWithLoginInfo)adminEmployee).CreateUserLoginInfo(SecurityDefaults.PasswordAuthentication, ObjectSpace.GetKeyValueAsString(adminEmployee));
}
ObjectSpace.CommitChanges();
}
In the application builder code, set the LockoutOptions.Enabled property to true.
File : MySolution.Blazor.Server\Startup.cs, MySolution.Win\Startup.cs
//...
using DevExpress.ExpressApp.Security;
namespace YourApplicationName.Blazor.Server;
public class Startup {
// ...
public void ConfigureServices(IServiceCollection services) {
services.AddXaf(Configuration, builder => {
//...
builder.Security
.UseIntegratedMode(options => {
options.Lockout.Enabled = true;
})
});
}
}
//...
using DevExpress.ExpressApp.Security;
namespace YourApplicationName.Win;
public class ApplicationBuilder : IDesignTimeApplicationFactory {
public static WinApplication BuildApplication(string connectionString) {
var builder = WinApplication.CreateBuilder();
//...
builder.Security
.UseIntegratedMode(options => {
options.Lockout.Enabled = true;
//...
})
//...
}
}
You can now run the application to see the result. You will see that the Employee objects are utilized as a custom user type.
Apply the ListViewFilterAttribute attributes to the EmployeeTask class to define List View filters. To refer to the current Employee identifier, use the CurrentUserId() function.
using DevExpress.ExpressApp.SystemModule;
// ...
[ListViewFilter("All Tasks", "")]
[ListViewFilter("My Tasks", "[Owner.Id] = CurrentUserId()")]
public class EmployeeTask : Task {
// ...
}
The image below shows the result.
See Also
Implement a Security System User (ApplicationUser) Based on XAF Business Classes