Friday, January 25, 2013

User friendly way to add permissions

XAFsecurity system is really flexible and can easily address complex scenarios. Depending on the nature of our application (e.g. geek level of business users Smile) we may want to modify the default design to make it more user friendly! This will be a detailed discussion on all the steps involved in doing so. In addition a sample application is available for download at the end of the post.

The goal is to create a user Role that will be able to Read and Navigate to predefined DashboardDefinition instances (SecurityOperation.ReadOnly). The DashboardDefinition is a simple business object.

If we had to use code we use a ModuleUpdater and 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 handy AddObjectAccessPermission Role extension method hides the internals which are:

  1. Searches if a SecuritySystemTypePermissionObject exists for the DashboardDefinition type and creates it accordingly. The SecuritySystemTypePermissionObject is a persistent object with a Type property which is used to relate the permission with business objects. Moreover SecuritySystemTypePermissionObject has a set of properties (AllowRead, AllowNavigate etc.) used by the Security System to determine if permissions are granted for a business object.
  2. Creates a new SecuritySystemObjectPermissionsObject which 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 is about a friendly (non code) way to add permissions and our goal is to modify 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 first see how the above code snippet is translated to a XAF UI and what steps needed from the end user.

image

The most difficult step as with the code approach is the Criteria construction. This time is even harder since we are not a developer any more but a business user. This means that even simple stuff like identifying the key property may look like a mountain.  In addition the end user needs a huge amount of time for creating permissions for a lot of objects.

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

image

The above UI represents a many collection between Roles and DashboardDefintion. We can tell that is an M-M relation because only the link action is available (see left arrow)., The New DashboardDefinition action is hidden and the creation of the intermediate objects is done magically from XAF!

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

image

In this step using a simple attribute we guided XAF to create a totally different UI for associating a Role with a DashboardDefintion. What remains 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 How to: Implement Custom Security Objects (Users, Roles, Operation Permissions). However since we want a reusable functionality we recommend to create a module to host the custom Role class. XAF follows this recommendation with the Security module, our community project eXpandFrameWork with the XpandSecurityModule.

public class XRole:SecuritySystemRole {

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

    }

 

}

 

Next step is to create an attribute with two parameters:

  1. OperationProviderProperty: Is the name of the property that will provide the SecurityOperation which will be applied to the collection of DashboardDefinition of our XRole.
  2. CollectionName: Is 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 its 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 as discussed in the start of the 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 has no knowledge of the DashboardDefinition 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 is the Role type.

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>());

    }

 

With the above code a new property will be added to the previously XRole UI.

 

image

 

 

Now we need a method to get the SecurityOperations given an XRole instance and the dynamic collection of DashboardDefinition objects. Note that the name property that provides these values exist in the SecurityOperationsAttribute marking our DashboardDefinition object:

 

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());

}

 

Having a list of SecurityOperations from the GetSecurityOperation method we can use XAF’s metadata API to create the ObjectOperationPermissions as simple as:

 

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 SecuritySystemRoleExtensions  and override our custom XRole GetPermissionsCore method as discussed in  How to: Implement Custom Security Objects (Users, Roles, Operation Permissions). So, in simple English this can b said: For each collection member in our XRole that his collection element type is marked with a SecurityOperationsAttribute call the above ObjectOperationPermissions extension method to get the permissions and add them to the list of XRole’s permission. XAF’s language does not differ much from English Smile, so this  will be:

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 simply mark our BO as shown:

 

image

 

Note that even if DashboardDefintion class may live in a module we do not have source code, XAF will not sweat at all! 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"));

}

 

The credits for this post go first to XAF with its unbelievable flexible API, second to a great XAFer named Stephen Manderson and third to me that wrote this post Smile. Moreover Stephen shared with us his Dashboard module which is the most wanted integration of XAF and our new Dashboard tool!

 

Next post we be all about Stephen’s Dashboard module, in the meantime let us know your thoughts in everything you heard today.

 

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

 

Until next time, Happy XAFing!

Subscribe to XAF feed
Subscribe to community feed

DiggIt!

0 comments:

Post a Comment