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 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.
[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?");' />
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>
<%@ 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.
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:
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.
If you e-mail me (e-mail address top right of site) I'll send you the code for that :D
Steve
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
See
DynamicData: Database Based Permissions - Part 1
Happy New Year :D
Steve
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.
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
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
Hi Martin, part two has a download that has the source in it :)
Steve
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
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;
}
Post a Comment