Sunday 25 May 2008

DynamicData Attribute Based Security - part 1

Articles in this Series

Permissions Attribute (Metadata) Classes

An attributes class inherits from System.Attribute and must have the following properties set, AttributeUsage with at least one of the 16 possible targets see below and also AllowMultiple set to true so that multiple occurrences of the attribute can decorate a class or property.

AttributeTargets

All

GenericParameter

Assembly

Interface

Class

Method

Constructor

Module

Delegate

Parameter

Enum

Property

Event

ReturnValue

Field

Struct

The AttributeTargets options that the PermissionsAttributes classes will use are class and Property.

Listing 1 shows a simplified attribute that can be applied to a class multiple times see Listing 2 where the attribute is applied to the Orders table once each for "Accounts" and "Sales" roles.

[AttributeUsage(AttributeTargets.class, AllowMultiple = true)]
public class TableDenyWriteAttribute: System.Attribute
{
    ...
}
Listing 1

Note: By convention, the name of the attribute class ends with the word Attribute. While not required, this convention is recommended for readability. When the attribute is applied, the inclusion of the word Attribute is optional.

[TableDenyWrite ("Sales")]
[TableDenyWrite ("Accounts")]
public partial class Order
{
}
Listing 2

Two types of attribute are required for the permissions attribute, Class (Entity/Table) and Property(Field/Column). Looking at Listing 2 this is all right for a simplistic approach but will make extracting the roles for each permission a bit clunky. So a better approach would be to have an attribute that is more flexible see Listing 3. The effective permissions for the class Order shown in Listing 3 are listed in Table 1.

[TablePermissions(TablePermissionsAttribute.Permissions.DenyInserts, "Sales")]
[TablePermissions(TablePermissionsAttribute.Permissions.DenyDelete, "Sales", "Production",)]
[TablePermissions(TablePermissionsAttribute.Permissions.DenyRead, "Accounts", "HR")]
public partial class Order
{
}
Listing 3
Roles Insert Delete Read
Accounts 1n n n
HR n n n
Sales n n y
Production y n y

Table 1

1. Note you will not be able to do an insert if the table cannot be viewed.

Listing 4 contain the finished attribute classes.

The changes in the finished classes include:

  1. A permission parameter
  2. The role parameter has become roles and is a string array
  3. Each attribute class has an enum which contain the table and column permissions.
  4. Helper method HasRole which return true if the role is present.
  5. Helper method HasAnyRole which return true if any of the roles passed in are present.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.DynamicData;

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class TablePermissionsAttribute : System.Attribute
{
    // this property is required to work with "AllowMultiple = true" ref David Ebbo
    // As implemented, this identifier is merely the Type of the attribute. However,
    // it is intended that the unique identifier be used to identify two
    // attributes of the same type.
    public override object TypeId { get { return this; } }

    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="permission"></param>
    /// <param name="roles"></param>
    public TablePermissionsAttribute(Permissions permission, params String[] roles)
    {
        this._permission = permission;
        this._roles = roles;
    }

    private String[] _roles;
    public String[] Roles
    {
        get { return this._roles; }
        set { this._roles = value; }
    }

    private Permissions _permission;
    public Permissions Permission
    {
        get { return this._permission; }
        set { this._permission = value; }
    }

    /// <summary>
    /// helper method to check for roles in this attribute
    /// the comparison is case insensitive
    /// </summary>
    /// <param name="role"></param>
    /// <returns></returns>
    public Boolean HasRole(String role)
    {
        // call extension method to convert array to lower case for compare
        String[] rolesLower = _roles.AllToLower();
        return rolesLower.Contains(role.ToLower());
    }

    /// <summary>
    /// helper method to check for roles in this attribute
    /// the comparison is case insensitive
    /// </summary>
    /// <param name="roles"></param>
    /// <returns></returns>
    public Boolean HasAnyRole(String[] roles)
    {
        // call extension method to convert array to lower case for compare
        foreach (var role in roles.AllToLower())
        {
            if (_roles.AllToLower().Contains(role.ToLower()))
                return true;
        }
        return false;
    }

    /// <summary>
    /// list of Deny permissions as the default is read write on everything
    /// this model apply the most severe permission restriction
    /// </summary>
    public enum Permissions
    {
        DenyRead,
        DenyEdit,
        DenyInserts,
        DenyDelete,
        DenyDetails,
        DenySelectItem, // Don't know wether this will be any use???
    }

}

[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class FieldPermissionsAttribute : System.Attribute
{
    // this property is required to work with "AllowMultiple = true" ref David Ebbo
    // As implemented, this identifier is merely the Type of the attribute. However,
    // it is intended that the unique identifier be used to identify two
    // attributes of the same type.
    public override object TypeId { get { return this; } }

    // Constructor
    public FieldPermissionsAttribute(Permissions permission, params String[] roles)
    {
        this._permission = permission;
        this._roles = roles;
    }

    private String[] _roles;
    public String[] Roles
    {
        get { return this._roles; }
        set { this._roles = value; }
    }

    private Permissions _permission;
    public Permissions Permission
    {
        get { return this._permission; }
        set { this._permission = value; }
    }

    /// <summary>
    /// helper method to check for roles in this attribute
    /// the comparison is case insensitive
    /// </summary>
    /// <param name="role"></param>
    /// <returns></returns>
    public Boolean HasRole(String role)
    {
        // call extension method to convert array to lower case for compare
        String[] rolesLower = _roles.AllToLower();
        return rolesLower.Contains(role.ToLower());
    }

    /// <summary>
    /// helper method to check for roles in this attribute
    /// the comparison is case insensitive
    /// </summary>
    /// <param name="roles"></param>
    /// <returns></returns>
    public Boolean HasAnyRole(String[] roles)
    {
        // call extension method to convert array to lower case for compare
        foreach (var role in roles.AllToLower())
        {
            if (_roles.AllToLower().Contains(role.ToLower()))
                return true;
        }
        return false;
    }

    /// <summary>
    /// list of Deny permissions as the default is read write on everything
    /// this model apply the most severe permission restriction
    /// </summary>
    public enum Permissions
    {
        DenyRead,
        DenyEdit,
    }
}
Listing 4

Next step is the metadata helper classes.

6 comments:

Anonymous said...

Wow Stephen. This is the second time I have stumbled on your blog looking for dynamic data answers. This is also the second time I have been compleeeeeeeetly baffled by your post. Give me an intro, just a paragraph will do. Alternatively, point me to the 'long list of things I assume u know' post.

Baffled

Stephen J. Naughton said...

It's difficult to have any kind of table of contents on blogger, I keep looking for a widget that will do it :( but haven't found one yet. eventually I get the blog hosted and used some other bloggin engine that supports that kind of thing :D

However if you ping me through Digsby I can always have a quick chat with you to clarify anything.

Steve :D

Unknown said...

Steven, I was wondering if you wouldn't be so kind as to post a sample Solution file containing all the features that are discussed on your blog pertaining to Dynamic Data. I've read them all several times over but am still finding it hard to understand. People in my ... situation might benefit from the code samples and/or screenshots of the ideas being applied.

Thanks so much and I hope that's not asking too much!

Stephen J. Naughton said...

Hi wadeo, I do post sample solutions at the end of each article series, I'm also planning to start up a project on Codeplex with project templates that cover most of the features shown on my blog

Steve :D

Anonymous said...

Hi, Steve

Question:

I have error with this code "_roles.AllToLower()"
can you explain this code.

thanks

Stephen J. Naughton said...

Sorry about that the extension method is in the final sample at the end of this series of articles.

Steve :(