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"/>
<uc1:DeleteButton
ID="DetailsItemTemplate1"
runat="server" />
<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.