How to: Implement a Custom Security System User Based on an Existing Business Class

  • 11 min to read

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 (e.g., to define a "tasks assigned to me" List View filter).

Tip

A complete sample project is available in the DevExpress Code Examples database at http://www.devexpress.com/example=E4160.

Note

Initial Business Model

Start with a new XAF solution. Add the following Employee and Task business classes to the module project.

[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); }
    }
}

Support the ISecurityUser Interface

Add a reference to the DevExpress.ExpressApp.Security.v19.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
    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.

Support the IAuthenticationStandardUser Interface

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
    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]
    protected 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.

Support the IAuthenticationActiveDirectoryUser Interface

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 {
    // ...
}

The IAuthenticationActiveDirectoryUser.UserName property declared by this interface has already been implemented in your code as a part of the ISecurityUser interface.

Support the ISecurityUserWithRoles Interface

Extend the Employee class with the following code.

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;
// ...
[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>(); }
    } 
}

Support the IPermissionPolicyUser Interface

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.

Support the ICanInitialize Interface

The ICanInitialize.Initialize method is used to assign the default role when you use the AuthenticationActiveDirectory authentication and set the AuthenticationActiveDirectory.CreateUserAutomatically to true. If you do not need to support users 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 = (EmployeeRole)objectSpace.FindObject<EmployeeRole>(
            new BinaryOperator("Name", security.NewUserRoleName));
        if (newUserRole == null) {
            newUserRole = objectSpace.CreateObject<EmployeeRole>();
            newUserRole.Name = security.NewUserRoleName;
            newUserRole.IsAdministrative = true;
            newUserRole.Employees.Add(this);
        }
    }
    #endregion
}

Configure the Security System in Order to Utilize Custom User and Role Types

Invoke the Application Designer, and drag the SecurityStrategyComplex component from the Toolbox to the Security pane. Then, place the AuthenticationStandard or AuthenticationActiveDirectory component near the SecurityStrategyComplex. To use the custom EmployeeRole role and custom Employee user instead of the default role and user, modify the SecurityStrategyComplex.RoleType and SecurityStrategy.UserType values in the Properties window. This step should be executed in WinForms, ASP.NET and Mobile applications.

EmployeeAsUserExample_Designer

Create an Administrative Account

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(Updater.vb) 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.FindObject<EmployeeRole>(
        new BinaryOperator("Name", SecurityStrategy.AdministratorRoleName));
    if (adminEmployeeRole == null) {
        adminEmployeeRole = ObjectSpace.CreateObject<EmployeeRole>();
        adminEmployeeRole.Name = SecurityStrategy.AdministratorRoleName;
        adminEmployeeRole.IsAdministrative = true;
        adminEmployeeRole.Save();
    }
    Employee adminEmployee = ObjectSpace.FindObject<Employee>(
        new BinaryOperator("UserName", "Administrator"));
    if (adminEmployee == null) {
        adminEmployee = ObjectSpace.CreateObject<Employee>();
        adminEmployee.UserName = "Administrator";
        adminEmployee.SetPassword("");
        adminEmployee.EmployeeRoles.Add(adminEmployeeRole);
    }
    ObjectSpace.CommitChanges();
}

Run the Application

You can now run the Windows Forms or ASP.NET application to see the result. You will see that the Employee objects are utilized as a custom user type.

EmployeeAsUserExample_Runtime

Filter Tasks that Belong to the Current Employee

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.Oid] = CurrentUserId()")]
public class EmployeeTask : Task {
    // ...
}

The image below shows the result.

EmployeeAsUserExample_RuntimeFilters