Sunday, 13 June 2010

Securing Dynamic Data 4 (Replay)

This is an updated version of the series Securing Dynamic Data Preview 4 from July 2009 here I playnto streamline the class libraries for the RTM version of Dynamic Data 4  and Visual Studio 2010.

This version is mostly the same as in Part 1 except I’ve done a great deal of refactoring and so I will list everything again here. The main difference is that there are now no user controls to replace the Delete buttons. Also I have changed the permissions system to be restrictive by default at Table level i.e. you must have a permission set on every table for the table to be seen, but a Column level you deny columns you don’t want to be seen.

Permissions Enums

The TableActions (renamed from TableDeny) enum Listing 1 has had a CombinedActions class Listing 2 added that combine sets of TableActions into logical security groups (i.e. ReadOnly equates to combining TablesActions Details and List to give an more descriptive was of assigning rights to a security Role).

/// <summary>
/// Table permissions enum, allows different
/// levels of permission to be set for each 
/// table on a per role bassis.
/// </summary>
[Flags]
public enum TableActions
{
    /// <summary>
    /// Default no permissions
    /// </summary>
    None = 0x00,
    /// <summary>
    /// Details page
    /// </summary>
    Details = 0x01,
    /// <summary>
    /// List page
    /// </summary>
    List = 0x02,
    /// <summary>
    /// Edit page
    /// </summary>
    Edit = 0x04,
    /// <summary>
    /// Insert page
    /// </summary>
    Insert = 0x08,
    /// <summary>
    /// Delete operations
    /// </summary>
    Delete = 0x10,
}

Listing 1 – TableActions

/// <summary>
/// Combines Table permissions enums
/// into logical security groups
/// i.e. ReadOnly combines TableActions
/// Details and List
/// </summary>
public static class CombinedActions
{
    /// <summary>
    /// Read Only access 
    /// TableActions.Details or 
    /// TableActions.List
    /// </summary>
    public const TableActions ReadOnly = 
        TableActions.Details | 
        TableActions.List;
    /// <summary>
    /// Read and Write access 
    /// TableActions.Details or 
    /// TableActions.List or
    /// TableActions.Edit
    /// </summary>
    public const TableActions ReadWrite = 
        TableActions.Details | 
        TableActions.List | 
        TableActions.Edit;
    /// <summary>
    /// Read Insert access 
    /// TableActions.Details or 
    /// TableActions.List or 
    /// TableActions.Insert
    /// 
    /// </summary>
    public const TableActions ReadInsert = 
        TableActions.Details | 
        TableActions.List | 
        TableActions.Insert;
    /// <summary>
    /// Read Insert and Delete access 
    /// TableActions.Details or 
    /// TableActions.List or 
    /// TableActions.Insert or 
    /// TableActions.Delete)
    /// </summary>
    public const TableActions ReadInsertDelete = 
        TableActions.Details | 
        TableActions.List | 
        TableActions.Insert | 
        TableActions.Delete;
    /// <summary>
    /// Read and Write access 
    /// TableActions.Details or 
    /// TableActions.List or 
    /// TableActions.Edit or 
    /// TableActions.Insert)
    /// </summary>
    public const TableActions ReadWriteInsert = 
        TableActions.Details | 
        TableActions.List | 
        TableActions.Edit | 
        TableActions.Insert;
    /// <summary>
    /// Full access 
    /// TableActions.Delete or
    /// TableActions.Details or 
    /// TableActions.Edit or 
    /// TableActions.Insert or 
    /// TableActions.List)
    /// </summary>
    public const TableActions Full = 
        TableActions.Delete | 
        TableActions.Details | 
        TableActions.Edit | 
        TableActions.Insert | 
        TableActions.List;
}

Listing 2 – CombinedActions

ColumnActions Listing 3  are used to deny either Write or Read access.

/// <summary>
/// Actions a Column can 
/// have assigned to itself.
/// </summary>
[Flags]
public enum ColumnActions
{
    /// <summary>
    /// Action on a column/property
    /// </summary>
    DenyRead = 1,
    /// <summary>
    /// Action on a column/property
    /// </summary>
    DenyWrite = 2,
}

Listing 3 – ColumnActions

Secure Dynamic Data Route Handler

The SecureDynamicDataRouteHandler has changed very little since the original article all I have added is the catch all tp.Permission == CombinedActions.Full in the if statement to streamline the code.

/// <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
{
    /// <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 httpContext = HttpContext.Current;
        if (httpContext != null && httpContext.User != null)
        {
            var usersRoles = Roles.GetRolesForUser(httpContext.User.Identity.Name);
            var tablePermissions = table.Attributes.OfType<SecureTableAttribute>();

            // if no permission exist then full access is granted
            if (tablePermissions.Count() == 0)
                return null;

            foreach (var tp in tablePermissions)
            {
                if (tp.HasAnyRole(usersRoles))
                {
                    // if no action is allowed return no route
                    var tpAction = tp.Permission.ToString().Split(new char[] { ',', ' ' }, 
                        StringSplitOptions.RemoveEmptyEntries);

                    if (tp.Permission == CombinedActions.Full || tpAction.Contains(action))
                        return base.CreateHandler(route, table, action);
                }
            }
        }
        return null;
    }
}

Listing 4 – Secure Dynamic Data Route Handler

This then covers all Edit, Insert and Details actions but not Delete.

Delete Actions

In the previous article we had a User Control that handled securing the Delete action, here we have a SecureLinkButton. All we do is override the Render method and test to see if the button is disabled via the users security roles.

/// <summary>
/// Secures the link button when used for delete actions
/// </summary>
public class SecureLinkButton : LinkButton
{
    private const String DISABLED_NAMES = "SecureLinkButtonDeleteCommandNames";
    private String[] delete = new String[] { "delete" };

    /// <summary>
    /// Raises the <see cref="E:System.Web.UI.Control.Init"/> event.
    /// </summary>
    /// <param name="e">
    /// An <see cref="T:System.EventArgs"/> 
    /// object that contains the event data.
    /// </param>
    protected override void OnInit(EventArgs e)
    {
        if (ConfigurationManager.AppSettings.AllKeys.Contains(DISABLED_NAMES))
            delete = ConfigurationManager.AppSettings[DISABLED_NAMES]
                .ToLower()
                .Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);

        base.OnInit(e);
    }

    /// <summary>
    /// Renders the control to the specified HTML writer.
    /// </summary>
    /// <param name="writer">
    /// The <see cref="T:System.Web.UI.HtmlTextWriter"/> 
    /// object that receives the control content.
    /// </param>
    protected override void Render(HtmlTextWriter writer)
    {
        if (!IsDisabled())
            base.Render(writer);
        else
            writer.Write(String.Format("<a>{0}</a>", Text));
    }

    /// <summary>
    /// Determines whether this instance is disabled.
    /// </summary>
    /// <returns>
    /// 	<c>true</c> if this instance is 
    /// 	disabled; otherwise, <c>false</c>.
    /// </returns>
    private Boolean IsDisabled()
    {
        if (!delete.Contains(CommandName.ToLower()))
            return false;

        // get restrictions for the current
        // users access to this table
        var table = DynamicDataRouteHandler.GetRequestMetaTable(Context);
        var usersRoles = Roles.GetRolesForUser();
        var tableRestrictions = table.Attributes.OfType<SecureTableAttribute>();

        // restrictive permissions
        if (tableRestrictions.Count() == 0)
            return true;

        foreach (var tp in tableRestrictions)
        {
            // the LinkButton is considered disabled if delete is denied.
            var action = CommandName.ToEnum<TableActions>();
            if (tp.HasAnyRole(usersRoles) && (tp.Actions & action) == action)
                return false;
        }
        return true;
    }
}

Listing 5 – Secure Link Button

In more detail the IsDisabled method check to see if the LinkButtons CommandName is the same as the the “SecureLinkButtonDeleteCommandNames” application setting set in the web.config, note the default is “delete”. And then if the user does not have Delete permission then the button is disabled.

So how do we use this SecureLinkButton we add a tagMapping in the web.config file see Listing 6.

<configuration>
    <system.web>
        <pages>
            <controls>
                <!-- custom tag assignments -->
                <add tagPrefix="asp" namespace="NotAClue.Web.DynamicData" 
                    assembly="NotAClue.Web.DynamicData" />
            </controls>
            <!-- custom tag mappings -->
            <tagMapping>
                <add tagType="System.Web.UI.WebControls.LinkButton"
                    mappedTagType="NotAClue.Web.DynamicData.SecureLinkButton" />
            </tagMapping>
        </pages>
    </system.web>
</configuration>

Listing 6 – Tag Mapping in web.config

This means that our SecureLinkButton will replace the LinkButton throughout the site, however if you do not like this you can just rename each delete <asp:LinkButton to <asp:SecureLinkButton and then you will get the same effect and not add the tagMapping section to the web.config.

The Secure Meta Model

Here the two main parts are the SecureMetaTable and the three MetaColumn types (SecureMetaColumn, SecureMetaForeignKeyColumn and SecureMetaChildrenColumn)

SecureMetaTable

In the SecureMetaTable we override the GetScaffoldColumns method and filter the column list to where columns do not have a DenyRead action applied for any of the current users security roles.

SecureMetaColumn, SecureMetaForeignKeyColumn and SecureMetaChildrenColumn

With these types we do have to repeat ourselves a little as we override the IsReadOnly property to check to see if the column has a DenyWrite action applied for one of the users roles.
Note: Thanks to the ASP.NET team for listening and making this property virtual.

There is one issue I found and that is the default FieldTemplateFactory caches the DynamicControl model (ReadOnly, Edit and Insert) I did toy with adding the relevant code the default EntityTemplates see Listing 7, but decided again it.

protected void DynamicControl_Init(object sender, EventArgs e)
{
    DynamicControl dynamicControl = (DynamicControl)sender;
    dynamicControl.DataField = currentColumn.Name;

    // test for read-only column
    if (currentColumn.IsReadOnly)
        dynamicControl.Mode = DataBoundControlMode.ReadOnly;
}


Listing 7 – adding control mode code to the default EntityTemplates

Instead I decided to use a custom FieldTemplateFactory see Listing 8

public class SecureFieldTemplateFactory : FieldTemplateFactory
{
    public override IFieldTemplate CreateFieldTemplate(MetaColumn column,
        DataBoundControlMode mode, 
        string uiHint)
    {
        // code to fix caching issue
        if (column.IsReadOnly)
            mode = DataBoundControlMode.ReadOnly;

        return base.CreateFieldTemplate(column, mode, uiHint);
    }
}

Listing 8 – Secure Field Template Factory

The code here is simple we just check to see if the column is read-only (remembering that the SecureMetaColumns are already checking this for us) then set the Mode to DataBoundControlMode.ReadOnly. This nicely keeps our code DRY.

Secure Table and Column Attributes

These are essentially unchanged from the previous series of articles with just a little refactoring to make the code more readable.

!Important: For code see sample at end of article.

Putting It Together

Nearly all the work to get Secure Dynamic Data working is done simply in the Global.asax file.

Note: There are some changes you need to make to add Login etc but that is standard ASP.Net and specific to Dynamic Data.

Adding Security to Dynamic Data

Figure 1 – Adding Security to Dynamic Data

Also you need the tag mapping from Listing 6, there are some more bits we need to do but they are standard ASP.Net Security, so let’s get that done next.

!Important: To use this as we currently are you will need SQL Server 200x Express installed otherwise you will need to add a specific connection string and use Creating the Application Services Database for SQL Server to make your ASPNETDB database for membership and roles.
<authentication mode="Forms">
    <forms loginUrl="~/Login.aspx" protection="All" defaultUrl="~/Default.aspx" path="/"/>
</authentication>
<authorization>
    <deny users="?"/>
</authorization>
<membership>
    <providers>
        <remove name="AspNetSqlMembershipProvider"/>
        <add name="AspNetSqlMembershipProvider"
            type="System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
            connectionStringName="LocalSqlServer"
            enablePasswordRetrieval="false"
            enablePasswordReset="true"
            requiresQuestionAndAnswer="true"
            applicationName="/"
            requiresUniqueEmail="false"
            passwordFormat="Hashed"
            maxInvalidPasswordAttempts="5"
            minRequiredPasswordLength="7"
            minRequiredNonalphanumericCharacters="0"
            passwordAttemptWindow="10"
            passwordStrengthRegularExpression=""/>
    </providers>
</membership>
<roleManager enabled="true" />

Listing 9 – Adding standard ASP.Net security to web.config

<location path="Site.css">
    <system.web>
        <authorization>
            <allow users="*"/>
        </authorization>
    </system.web>
</location>

Listing 10 – Allowing access to style sheet.

With SQL Server 200x Express edition installed you will get the ASPNETDB created automatically.

Note: I generally do this to create the ASPNETDB then move it to where I want it and setup a specific connection string. Also you can use the ASP.Net Configuration utility to create users and roles.
ASP.Net Configuration Utility
Figure 1 - ASP.Net Configuration Utility

Downloads

I think that is about it, so here is the download it contains three projects the Class Library and two sample projects one Entity Framework and one Linq to SQL. Have fun.

40 comments:

Joe said...

Man...You have answers on all my questions... I write dynamic data asp.net application for final exam on college and here I found all the answers I need... You are excellent! My concerns was about roles and Dynamic Data ASP.NET Web Linq to SQL application, customizing field templates,... MSDN helps too, but You are much concrete..Thank You a lot... In my country said that we are young that much are thoughts are young...You're daughters are wrong, You cant be that old, You are still pretty much in shape.. Sorry about bad English... Keep with good work...

Stephen J. Naughton said...

Thanks Joe :)

Steve

Ahmed said...

hi,
I downloaded this sample and change connection string in EF web config and i get this Exception after i logged in "There are no accessible tables. Make sure that at least one data model is registered in Global.asax and scaffolding is enabled or implement custom pages"

Seif

Stephen J. Naughton said...

There must be an issue with your connection string sorry about that.

Steve

Ahmed said...

Thanks Stephen for this great blog,it contains very helpful articles,thanks again for this great work.
I solve the previous issue,but i has another issued,i trying Walkthrough sample for filtering Rows in Tables That Have a Parent-Child Relationship (http://msdn.microsoft.com/en-us/library/dd985039.aspx ),I get this Exception Unable to cast object of type 'NotAClue.Web.DynamicData.SecureMetaColumn' to type 'System.Web.DynamicData.MetaForeignKeyColumn'

Seif

Stephen J. Naughton said...

Hi Seif, not sure what is going ont here as I cannot think of a reason why that cast would occur.

Steve :(

Anonymous said...

Hi steve, would it be possible to do this with inline editing enabled?

Stephen J. Naughton said...

Hi Mr Anonymous :) it should just work as is as long as your InLine Edit page get's it's columns the normal way. I have tested this method with the Telerik RadGrid and that works fine, and I should be blogging about that as soon as time allows.

Steve

Eddy said...

Hi Steve, thanks for your quick response for Mr. Anonymous =)

Mind checking to see if i'm doing anything wrong? "User" role are still able to edit/delete (through inline editing). However everything works if i went back into separate-page mode.

routes.Add(new DynamicDataRoute( {table}/ListDetails.aspx") {
Action = PageAction.List,
ViewName = "ListDetails",
RouteHandler = new SecureDynamicDataRouteHandler(),
Model = DefaultModel
});

routes.Add(new DynamicDataRoute("{table}/ListDetails.aspx") {
Action = PageAction.Details,
ViewName = "ListDetails",
RouteHandler = new SecureDynamicDataRouteHandler(),
Model = DefaultModel
});

unless i putting the "routehandler" at the wrong place.

Stephen J. Naughton said...

Hi Eddy that looks fine the issue will be with the control you are using. By default all the pages in DD4 use LinkButton or DynamicHyperLink I've made sure that LinkButton is fixed but I had'nt noticed the use of the DynamicHyperLink this will probably be the issue.

Steve

Eddy said...

Hi Steve, everything has been on default. I had manually changed LinkButton to SecureLinkButton. Do not see any DynamicHyperLink. Using DD4 & EF. All controls are default. When i put my mouse over Edit/Delete, the hyperlinks appear to be "javascript:__doPostBack('ctl00$ContentPlaceHolder1$GridView1','Delete$0')" & " javascript:__doPostBack('ctl00$ContentPlaceHolder1$GridView1','Edit$0')"

Stephen J. Naughton said...

I will try to test it with the standard List DetailsPage as soon as I get the time

Steve

Eddy said...

any luck??? =/

Stephen J. Naughton said...

Have you tried running the sample on it's own?

Steve

Nima said...

Hi Steve, thank you for your great work.
Have you released a improved version for your database based secure DD?

Stephen J. Naughton said...

Not yet, but I do play to for DD4

Steve

Ken said...

I tried this and got the following error message.
Any clues?
The database '5E98F77B1B99BCB7839B5BA9D7C561BC_RE DYNAMIC DATA\SECURINGDYNAMICDATA - 2010-06-13A\SECURINGDYNAMICDATA_L2S\APP_DATA\ASPNETDB.MDF' cannot be opened because it is version 661. This server supports version 612 and earlier. A downgrade path is not supported.

Stephen J. Naughton said...

you will need to delete the ASPNET DB and create again as it is for SQL Server 'Express 2008 R2

Steve

Anonymous said...

Hi Steve,

Do you have an updated version of this? I might be running into the same issue as Eddy above. Everything worked beautifuly when i was using the List.aspx Page template but after switching over to ListDetails.aspx..not so much. I have specified the 2 different TableActions (ReadOnly for one role and Full for another role) on the samet table and everything seems to go pear shaped ... I always get full access..i haven't stepped into the code yet but does this framework currently support "stacking" table actions for different roles? Let me know.

Stephen J. Naughton said...

Sorry no I will look at doing somthing about the ListDetaisl page though in the future.

Steve

Enrico said...

Hi Steve, Thanks for your great work.
I configured my Application to run with SecureMetaModel and now I would like to see TableActions and ColumnAction enums selected from a DropDownList. How I can do this with Entity Framework?

Enrico

Stephen J. Naughton said...

Sorry Enrico, not aure what you are after this version only has static metadata?

Steve

Enrico said...

Ok, now I have it working. I had to retrieve authorization settings from a Sql db; I tried to use some ideas from "DynamicData: Database Based Permissions - Part 2". But I need to load also the 'CombinedActions' joined with 'TableActions'.
:-D

Enrico said...

Ok, with [FilterUIHint("Enumeration")] and
[EnumDataType(typeof(SelectedTableActions))] before "permission" I have solved those issues with enum data type. Thank you and Dynamic Data 4 team!

KOsmix said...

Hi Stephen,
Could you provide us a newer version of the populated AspNetDB.mdf with the 2 users (Admin,User)?
Thank you.

Stephen J. Naughton said...

Just user ASP.Net Configuration from VS to configure this.

Steve

KOsmix said...

Thanks Steve you have been a great help for me because I've used DD for my master thesis.

Stephen J. Naughton said...

Hi KOsmix, thanks glad I was able to help.

Steve

Amy said...

Hi Steve,

I'm having some issues with using "DenyWrite" on columns. I can set up table permissions just fine and DenyRead on columns works, but if I set DenyWrite, I get errors. The edit screen displays correctly (showing the field as read only), but once I click update, I'm getting errors about how the read only field can't be null, even though it was populated in the edit screen with a value.

I've looked at the OSM in the SavingChanges method, and for some reason, the entity shows up with all null values (except its ID number). As such, it's throwing exceptions because required fields are null. It's not just the fields that have the SecureColumn attribute with DenyWrite in the metadata - if any field has it, all of the fields show up as null in the object state manager.

I'm using the DLL from the NotAClue.DynamicData project in the sample.

Any ideas why this might be happening?

Thanks.

Stephen J. Naughton said...

Hi Amy, I've only had this sort of issue once and is was a bug in EF when I used mapped SPROCS for CRUD in the EF model, I reproed it with normal webformas pages. if your not using mapped SPROCs for CRUD I don't know what is happening.

Steve

Amy said...

That's exactly what it was. Removed the SQL stored procedures and it works fine. I suppose I need to go incorporate those into code now, but at least it works.

Thanks for your help!

Stephen J. Naughton said...

Yes that is a nast bug and it only shows up when you have a column missing from the edit or read only :(

Steve

Tillmann said...

That is really fantastic! Thanks Steve for this useful code. Just one more little question: i would like to hide the hyperlinks "edit" and "delete" in case of CombinedActions.ReadOnly. The delete-link ist rendered in SecureLinkButton.cs, therefor i replaced "writer.Write(String.Format("{0}", Text))" by "writer.Write(String.Format("", Text))". But what would be the best approach to render the "edit" hyperlink?

Regards Tillmann

Stephen J. Naughton said...

Hi Tillmann, direct e-mail and we can discuss in detail.

Steve

Miloš said...

Hi Steve,
Great code again! Do you have implemented your securing solution in AD environment? I am trying to modify your solution, but I have a problem to access session object from SecureDynamicDataRouteHandler. I store users AD roles in session object. Thanks

Stephen J. Naughton said...

Hi Miloš, yes it works fine with the AD provider, your only issue is the Roles, if you want to use AD Groups the you would need an AD Roles provider and that has issues. I rolled my own and is does caching of users Roles as AD so slow it really slowed down my web site. If you want the code please send me an e-mail direct my address in on my blog.

Steve

Miloš said...

Exactly, my problem is unacceptable slow performance of web application after adding AD security. I have send you an email with my details. Thanks

Miloš said...

Thats it Steve, thanks for your final code! My AD rolled application is now working like a charm!

Sabrina said...

Hi Steve,

I am unable to see your downloads. Any other way I could get them for reference?

Thanks!

Stephen J. Naughton said...

Hi Sabrina, you will have to search my One Drive public folder here
https://onedrive.live.com/?cid=96845E7B0FAC1EED&id=96845E7B0FAC1EED%21112

to find the download Microsoft did a change a while back to SkyDrive that messedup ALL my downloads, but they are still there on One Drive :)

Steve