User friendly way to add permissions

XAF Team Blog
25 January 2013

The XAFsecurity system, just like XAF itself is flexible and easily addresses the most complex of scenarios. Depending on the nature of the application at hand, we may wish to modify the default design to make it more user friendly! And that’s exactly that this clog will cover. Check the end of this post for a sample application. 

Let’s assume that we have a simple business object the DashboardDefinition. Our goal is to create a user Role that can Read and Navigate to predefined DashboardDefinition instances. XAF provides a special operator for this the SecurityOperation.ReadOnly.

If we had to use code, we’d use a ModuleUpdater, along with the following snippet.

public class Updater : ModuleUpdater {

    public Updater(IObjectSpace objectSpace, Version currentDBVersion) : base(objectSpace, currentDBVersion) { }

    public override void UpdateDatabaseAfterUpdateSchema() {

        base.UpdateDatabaseAfterUpdateSchema();

 

        var role= ObjectSpace.FindObject<SecuritySystemRole>("Name='User'");

        var criteria = CriteriaOperator.Parse("Oid=?", "TOO hard to know the key value").ToString();

        const string operations = SecurityOperations.ReadOnlyAccess;

        role.AddObjectAccessPermission<DashboardDefinition>(criteria,operations);

 

    }

}

 

The useful AddObjectAccessPermission Role extension method hides the internals, which:

  1. Search for a SecuritySystemTypePermissionObject for the DashboardDefinition type and create it if not found. The SecuritySystemTypePermissionObject is a persistent object with a Type property, used to relate permissions for business objects. Moreover the SecuritySystemTypePermissionObject has a set of properties (AllowRead, AllowNavigate etc.) that are used by the Security System to determine if permissions are granted for the business object.
  2. Create a new SecuritySystemObjectPermissionsObject that holds the Criteria and Operation action and relates it with the SecuritySystemTypePermissionObject from step 1.

Although the AddObjectAccessPermission allows us to write user friendly code there are a few problems:

  1. This post covers a user-friendly way of adding permissions (without utilizing code) and a way of modifying the default XAF UI.
  2. It is difficult to construct the Criteria parameter of the AddObjectAccessPermission method (the developer should be aware of the name and value of the key property).

Let’s now turn to the code snippet displayedto see how it is translated to a XAF UI and what steps are required from the end user to that end.

image

The most difficult step is the Criteria construction (just like it is if you utilize the code approach). Since this step will be used by a business user and not a developer, things will be even harder. This will make even ordinary actions, like identifying a key property, more complex. In addition the end user, will require a lot of time to create permissions for many of the objects.

The solution is to modify the default XAF UI and allow the business user to associate a Role with a DashboardDefinition object instance as displayed bellow:

image

The above UI represents a many-to-many collection between Roles and DashboardDefintion. Since only the link action is available (see left arrow), we can tell that is an M-M relationship., The New DashboardDefinition action is hidden and the creation of the intermediate objects is done “magically” by XAF!

To create the DashboardDefinition collection illustrated in the UI above, we can use the ProvidedAssociationAttribute as discussed in Modifying Business Objects using Attributes post.

image

In this step, by using a simple attribute, we guided XAF to create a completely different UI for associating a Role with DashboardDefintion. What remaining step is to write code that will automatically create the required permissions by extending the SecuritySystemRole class.

Creating a custom Security Module

Extending the SecuritySystemRole class means that we need to create a custom Role class deriving from the SecuritySystemRole. The process is well documented in How to: Implement Custom Security Objects (Users, Roles, Operation Permissions). Since, however a reusable functionality is what we desire, we recommend creating a module to host the custom Role class.

public class XRole:SecuritySystemRole {

    public XRole(Session session) : base(session) {

    }

 

}

 

The next step is to create an attribute with two parameters:

  1. OperationProviderProperty: the name of the property that will provide the SecurityOperation which will be applied to the collection of DashboardDefinition of our XRole.
  2. CollectionName: the name of the dynamically created DashboardDefinition collection member in our XRole.

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]

public class SecurityOperationsAttribute : Attribute {

    readonly string _collectionName;

    readonly string _operationProviderProperty;

 

    public SecurityOperationsAttribute(string collectionName, string operationProviderProperty) {

        _collectionName = collectionName;

        _operationProviderProperty = operationProviderProperty;

    }

 

    public string CollectionName {

        get { return _collectionName; }

    }

 

    public string OperationProviderProperty {

        get { return _operationProviderProperty; }

    }

}

 

Now, it’s time to use this SecurityOperationsAttribute in our DashboardDefintion class which does not live in our custom Security module.

 

image

 

The collectionName parameter (DashboardDefinitions) is the name of the collection created from the ProvidedAssociationAttribute discussed in the start of this post. The operationProviderProerty (DashboardOperation) does not yet exist in our XRole class and we need to create it in an abstract way since our Security modules have no knowledge of the DashboardDefinition’s existence. Writing abstract code with XAF is really a piece of cake! Our goal is to enumerate through all PersistentTypes (this includes DashboardDefintion) marked with the SecurityOperationAttribute. Then, for each Persistent type, we need to create a dynamic member in our XRole class to hold the SecurityOperation. Again, note that our module is not even aware of what the Role type is.

 

public sealed partial class MySecurityModule : ModuleBase {

    public override void CustomizeTypesInfo(ITypesInfo typesInfo) {

        base.CustomizeTypesInfo(typesInfo);

        var roleTypeProvider = Application.Security as IRoleTypeProvider;

        if (roleTypeProvider != null) {

            foreach (var attribute in SecurityOperationsAttributes(typesInfo)) {

                CreateMember(typesInfo, roleTypeProvider, attribute);

            }

        }

    }

 

    void CreateMember(ITypesInfo typesInfo, IRoleTypeProvider roleTypeProvider, SecurityOperationsAttribute attribute) {

        var roleTypeInfo = typesInfo.FindTypeInfo(roleTypeProvider.RoleType);

        if (roleTypeInfo.FindMember(attribute.OperationProviderProperty) == null) {

            var memberInfo = roleTypeInfo.CreateMember(attribute.OperationProviderProperty, typeof (SecurityOperationsEnum));

            memberInfo.AddAttribute(new RuleRequiredFieldAttribute());

        }

    }

 

    IEnumerable<SecurityOperationsAttribute> SecurityOperationsAttributes(ITypesInfo typesInfo) {

        var typeInfos = typesInfo.PersistentTypes.Where(info => info.FindAttribute<SecurityOperationsAttribute>() != null);

        return typeInfos.SelectMany(info => info.FindAttributes<SecurityOperationsAttribute>());

    }

 

Using the code above will add a new property to the previous XRole UI.

 

image

 

 

Now, we have already an XRole instance and we created a dynamic collection of DashboardDefinition objects. So we need a method to retrieve the SecurityOperations by checking if our DashboardDefinition object is decorated with SecurityOperationsAttribute.

 

static string GetSecurityOperation(ISecurityRole securityRole, XPMemberInfo memberInfo) {

    var typeInfo = XafTypesInfo.Instance.FindTypeInfo(memberInfo.CollectionElementType.ClassType);

    var roleTypeInfo = XafTypesInfo.Instance.FindTypeInfo(securityRole.GetType());

    var operationsAttribute = typeInfo.FindAttributes<SecurityOperationsAttribute>().FirstOrDefault(attribute => attribute.CollectionName == memberInfo.Name);

    return operationsAttribute != null ? Convert(securityRole, roleTypeInfo, operationsAttribute) : null;

}

 

static string Convert(ISecurityRole securityRole, ITypeInfo roleTypeInfo, SecurityOperationsAttribute operationsAttribute) {

    var value = roleTypeInfo.FindMember(operationsAttribute.OperationProviderProperty).GetValue(securityRole);

    if (value == null || ReferenceEquals(value, ""))

        return null;

    var securityOperations = (SecurityOperationsEnum)value;

    var fieldInfo = typeof(SecurityOperations).GetField(securityOperations.ToString(), BindingFlags.Public | BindingFlags.Static);

    if (fieldInfo != null)

        return fieldInfo.GetValue(null).ToString();

    throw new NotImplementedException(value.ToString());

}

 

Since we have a list of SecurityOperations from the GetSecurityOperation method we can use XAF’s metadata API to create the ObjectOperationPermissions.

 

public static IEnumerable<ObjectOperationPermission> ObjectOperationPermissions(this ISecurityRole securityRole, XPMemberInfo member) {

    var collection = ((XPBaseCollection)member.GetValue(securityRole)).OfType<object>();

    var securityOperation = GetSecurityOperation(securityRole, member);

    if (!string.IsNullOrEmpty(securityOperation)) {

        foreach (var operation in securityOperation.Split(ServerPermissionRequestProcessor.Delimiters, StringSplitOptions.RemoveEmptyEntries)) {

            foreach (var obj in collection) {

                yield return ObjectOperationPermissions(member, obj, operation);

            }

        }

    }

}

 

static ObjectOperationPermission ObjectOperationPermissions(XPMemberInfo member, object obj, string securityOperation) {

    return new ObjectOperationPermission(member.CollectionElementType.ClassType, Criteria(obj, member.CollectionElementType), securityOperation);

}

 

static string Criteria(object obj, XPClassInfo classInfo) {

    var keyProperty = classInfo.KeyProperty;

    var keyValue = keyProperty.GetValue(obj);

    return CriteriaOperator.Parse(keyProperty.Name + "=?", keyValue).ToString();

}

 

Finally we put all these methods to a class SecuritySystemRoleExtensionsand override our custom XRole GetPermissionsCore method as discussed in  How to: Implement Custom Security Objects (Users, Roles, Operation Permissions).

 

public class XRole : SecuritySystemRole {

    public XRole(Session session)

        : base(session) {

    }

 

    protected override IEnumerable<IOperationPermission> GetPermissionsCore() {

        var operationPermissions = base.GetPermissionsCore();

        return OperationPermissionCollectionMembers().Aggregate(operationPermissions, (current, xpMemberInfo) => current.Union(this.ObjectOperationPermissions(xpMemberInfo).Cast<IOperationPermission>()));

    }

 

    IEnumerable<XPMemberInfo> OperationPermissionCollectionMembers() {

        return ClassInfo.OwnMembers.Where(info => info.IsAssociationList && info.CollectionElementType.HasAttribute(typeof(SecurityOperationsAttribute)));

    }

Today, we discussed how to mix BO’s metadata with instance data using a simple attribute in order to avoid tedious and repetitive work. To summarize when we want to create user friendly ObjectAccessPermissions  we can mark our BO as shown.

 

image

 

Note that even if the DashboardDefintion class may live in a module, we do not have source code, It is really easy to dynamically replace attributes adjusting to your own preferences (see also How to customize a Business Model at runtime (Example)).

 

public override void CustomizeTypesInfo(ITypesInfo typesInfo) {

    base.CustomizeTypesInfo(typesInfo);

    var typeInfo = (TypeInfo) typesInfo.FindTypeInfo(typeof (DashboardDefinition));

    var memberInfo = typeInfo.FindMember("Roles");

 

    //replace ProvidedAssociationAttribute in Roles collection

    memberInfo.RemoveAttributes<ProvidedAssociationAttribute>();

    memberInfo.AddAttribute(new ProvidedAssociationAttribute("DashboardDefinition-Roles","MyDashboardDefintions",RelationType.ManyToMany, null));

 

    //replace SecurityOperationsAttribute

    typeInfo.RemoveAttributes<SecurityOperationsAttribute>();

    typeInfo.AddAttribute(new SecurityOperationsAttribute("MyDashboardDefintions", "MyDashboardOperation"));

}

 

My next post will cover Stephen’s Dashboard module, In the meantime let us know your thoughts about today’s blog.

 

The sample covering today’s discussion can be downloaded from here and is built against XAF v12.2.5.

 

Until next time, Happy XAFing!

Subscribe to XAF feed
Subscribe to community feed

no comments
No Comments

Please login or register to post comments.