Monday 13 July 2009

Securing Dynamic Data Preview 4 Refresh – Part 1

This article is another stab at creating a simple framework to add a simple security layer to Dynamic Data. This time I’ve based my first level of security on the sample posted by Veloce here Secure Dynamic Data Site. And getting column level security using the Great Buried Sample in Dynamic Data Preview 4 – Dynamic Data Futures I found last month.

Like my previous security layers for Dynamic Data I’m going to use a permissive system because you have to add these attributes to the metadata classes (and that can be a laborious task) and so I though is would be better under these circumstances to just remove access at individual tables and columns, rather than having to add attributes to every table and column to set the security level.

Things we will need to Do

  • Dynamic Data Route Handler
  • Remove Delete Link from List and Details pages
  • Secure Meta model classes
  • Make columns read only using Entity Templates.

Dynamic Data Route Handler

Firstly again we must thank Veloce for his Secure Dynamic Data Site see his blog for morebits (pun intended) of Dynamic Data goodness. So what I decided to do was cut out a load of stuff from his example and pare it down to something that is easy to modify and understand.

/// <summary>
/// The SecureDynamicDataRouteHandler enables the 
/// user to access a table based on the following:
/// the Roles and TableDeny values assigned to 
/// the SecureTableAttribute.
/// </summary>
public class SecureDynamicDataRouteHandler : DynamicDataRouteHandler
{
    public SecureDynamicDataRouteHandler() { }

    /// <summary>
    /// Creates the handler.
    /// </summary>
    /// <param name="route">The route.</param>
    /// <param name="table">The table.</param>
    /// <param name="action">The action.</param>
    /// <returns>An IHttpHandler</returns>
    public override IHttpHandler CreateHandler(
        DynamicDataRoute route,
        MetaTable table,
        string action)
    {
        var usersRoles = Roles.GetRolesForUser();
        var tableRestrictions = table.Attributes.OfType<SecureTableAttribute>();

        // if no permission exist then full access is granted
        if (tableRestrictions.Count() == 0)
            return base.CreateHandler(route, table, action);

        foreach (var tp in tableRestrictions)
        {
            if (tp.HasAnyRole(usersRoles))
            {
                // if any action is denied return no route
                if ((tp.Restriction & TableDeny.Read) == TableDeny.Read)
                    return null;
                if ((tp.Restriction & TableDeny.Write) == TableDeny.Write &&
                    ((action == "Edit") || (action == "Insert")))
                    return null;
            }
        }

        return base.CreateHandler(route, table, action);
    }
}

Listing 1 – SecureDynamicDataRouteHandler

This route handler is called each time a route is evaluated see listing 2 where we have added RouteHandler = new SecureDynamicDataRouteHandler() to the default route.

routes.Add(new DynamicDataRoute("{table}/{action}.aspx")
{
    Constraints = new RouteValueDictionary(new { action = "List|Details|Edit|Insert" }),
    RouteHandler = new SecureDynamicDataRouteHandler(),
    Model = DefaultModel
});

Listing 2 – Default route in Global.asax.cs

Now look at the CreateHandler method of Listing 1 1st we get users roles and the tables permissions, then if the table has no restrictions we just return a handler from the base DynamicDataRouteHandler class. After this we check each table restriction to see if this user is in one of the roles in the restriction, then is the user has one of the roles in the restriction we check the restrictions (most restricting first) for a match and then deny the route by returning null appropriately.

[Flags]
public enum TableDeny
{
    Read    = 1,
    Write   = 2,
    Delete  = 4,
}

[Flags]
public enum DenyAction
{
    Delete = 0x01,
    Details = 0x02,
    Edit = 0x04,
    Insert = 0x08,
    List = 0x10,
}

Listing 3 – Security enums

Note: You don’t need to have the values appended to the enum i.e. Delete = 0x01 but it’s worth noting that if you don’t set the first value to 1 it will default to 0 and any failed result will match 0 i.e. tp.Restriction & DenyAction.List if enum then even it tp.Restriction does not AND with DenyAction.List the result will be 0

Listing 3 shows the security enums used in this sample however the DenyAction is not used I include it here as an option I considered in place of TableDeny enum. Let me explain you could replace the code inside the foreach loop of the route handler with Listing 4.

// alternate route handler code
if ((tp.Restriction & DenyAction.List) == DenyAction.List &&
    action == "List")
    return null;
if ((tp.Restriction & DenyAction.Details) == DenyAction.Details &&
    action == "Details")
    return null;
if ((tp.Restriction & DenyAction.Edit) == DenyAction.Edit &&
    action == "Edit")
    return null;
if ((tp.Restriction & DenyAction.Insert) == DenyAction.Insert &&
    action == "Insert")
    return null;  

Listing 4 – alternate route handler code

This would allow you to deny individual actions instead of Read or Write as in basic form of the route handler.

Note: You should also note the use of the [Flags] attribute on the enums as this allows this sort of declaration in the metadata:
[SecureTable(TableDeny.Write | TableDeny.Delete, "Sales")]
which is the reason why we have the test in the form of:
(tp.Restriction & DenyAction.List) == DenyAction.List 
and not
tp.Restriction == DenyAction.List

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class SecureTableAttribute : 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 SecureTableAttribute(TableDeny 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 TableDeny _permission;
    public TableDeny Restriction
    {
        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());
    }
}

Listing 5 -  SecureTableAttribute

The TableDenyAttribute is strait forward two properties and a methods to check if a roles is in the Roles property.

public static class SecurityExtensionMethods
{
    /// <summary>
    /// Returns a copy of the array of string 
    /// all in lowercase
    /// </summary>
    /// <param name="strings">Array of strings</param>
    /// <returns>array of string all in lowercase</returns>
    public static String[] AllToLower(this String[] strings)
    {
        String[] temp = new String[strings.Count()];
        for (int i = 0; i < strings.Count(); i++)
        {
            temp[i] = strings[i].ToLower();
        }
        return temp;
    }

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

Listing 6 – some extension methods

These extension methods in Listing 6 are used to make the main code more readable.

Remove Delete Link from List and Details pages

I’m including this with this first article because it will give you a complete solution at table level.

<%@ Control 
    Language="C#" 
    AutoEventWireup="true" 
    CodeBehind="DeleteButton.ascx.cs" 
    Inherits="DD_EF_SecuringDynamicData.DeleteButton" %>
<asp:LinkButton 
    ID="LinkButton1" 
    runat="server" 
    CommandName="Delete" Text="Delete"
    OnClientClick='return confirm("Are you sure you want to delete this item?");' />

Listing 7 – DeleteButton.ascx

public partial class DeleteButton : System.Web.UI.UserControl
{
    protected void Page_Load(object sender, EventArgs e)
    {
        var table = DynamicDataRouteHandler.GetRequestMetaTable(Context);

        var usersRoles = Roles.GetRolesForUser();
        var tableRestrictions = table.Attributes.OfType<SecureTableAttribute>();
        if (tableRestrictions.Count() == 0)
            return;

        foreach (var tp in tableRestrictions)
        {
            if (tp.HasAnyRole(usersRoles) &&
                (tp.Restriction & TableDeny.Delete) == TableDeny.Delete)
            {
                LinkButton1.Visible = false;
                LinkButton1.OnClientClick = null;
                LinkButton1.Enabled = false;
            }
        }
    }
}

Listing 8 – DeleteButton.ascx.cs

This user control in Listing 8 is used to replace the delete button on the List.aspx and Details.aspx pages, this code is very similar to the code in the route handler. We first check each restriction to see if the user is in one of its roles and then if the restriction is TableDeny.Delete and then disable the Delete button.

<ItemTemplate>
    <table id="detailsTable" class="DDDetailsTable" cellpadding="6">
        <asp:DynamicEntity runat="server" />
        <tr class="td">
            <td colspan="2">
                <asp:DynamicHyperLink 
                    runat="server" 
                    Action="Edit" 
                    Text="Edit" />
                <uc1:DeleteButton 
                    ID="DetailsItemTemplate1" 
                    runat="server" />
            </td>
        </tr>
    </table>
</ItemTemplate>

Listing 9 – Details.aspx page

<Columns>
    <asp:TemplateField>
        <ItemTemplate>
            <asp:DynamicHyperLink 
                runat="server" 
                ID="EditHyperLink" 
                Action="Edit" 
                Text="Edit"/>&nbsp;
            <uc1:DeleteButton 
                ID="DetailsItemTemplate1" 
                runat="server" />&nbsp;
            <asp:DynamicHyperLink 
                ID="DetailsHyperLink" 
                runat="server" 
                Text="Details" />
        </ItemTemplate>
    </asp:TemplateField>
</Columns>

Listing 10 – List.aspx page

<%@ Register src="../Content/DeleteButton.ascx" tagname="DeleteButton" tagprefix="uc1" %>

You will also need to add this line after the Page directive at the top of both the List.aspx and Details.aspx pages.

Note: I will present a slightly more complex version of this hyperlink user control in the next article which will allow the any hyperlink  as an option to remain visible but be disabled.

And that’s it for this article next we will look at using the Great Buried Sample in Dynamic Data Preview 4 – Dynamic Data Futures I mentioned earlier to allow us to hide column based on user’s roles and also add the feature to make columns read only based on user’s roles.

10 comments:

Anonymous said...

What would you need to do in the code to invert the base permissions? For example instead of granting everyond permissions initially & then denying permissions for each table/column how would you make it so the default is to deny everyone initial permission & then only grant specific people permission to the tables they need to access. This seems more logical.

Stephen J. Naughton said...

If you e-mail me (e-mail address top right of site) I'll send you the code for that :D

Steve

Chip said...

How to set the Attribute of the Table using programmed and stored in the database?
I was thinking a lot about your post.
If you have answers or suggestions for me.
Regards

Stephen J. Naughton said...

See
DynamicData: Database Based Permissions - Part 1

Happy New Year :D

Steve

vivek said...

Hi Steve,

how to use session instead of Roles.GetRolesForUser()
eg -
var usersRoles = System.Web.HttpContext.Current.Session["dept"].ToString();

It is working for first time i.e.

to hide the table from list like this -
[SecureTable(TableDeny.Read, "1","2")]

but when clicking on any table name link throwing
session error - object refernce null.

Please provide solution. I have to uses role from session only.

Stephen J. Naughton said...

Hi vivek, email me direct with samples of your code and I will have a look for you. my e-mail is in the top right of the site :)

Steve

Martin Ransome said...

Hi Steve,

I am new to this whole idea of Dynamic Data and Role Based Security. Please post in your code the using statements that are required. I copied and pasted the SecureDynamicDataRouteHandler class and got a number of reference errors and was stopped there.

Thank you for your kind assistance.

martin.s.ransome@gmail.com

Stephen J. Naughton said...

Hi Martin, part two has a download that has the source in it :)

Steve

Unknown said...

Hello

I'm also trying to secure my DD webSite with SecureAttributes

but i also want to use session to avoid many calls to the DB

but the HttpContext.Current.Session is null in CustomDynamicDataRouteHandler !

Is there any solution ?

Thanks a lot

Stephen J. Naughton said...

Hi Marwen, I usually use this method to get the user ID so I’m not sure why you would have an issue with Session as this works.
public static String GetUserId()
{
var context = System.Web.HttpContext.Current;
var userId = String.Empty;
if (context != null && context.User != null && context.User.Identity.IsAuthenticated)
{
userId = context.User.Identity.Name;
return userId;
}
return userId;
}