Tuesday, 14 July 2009

Securing Dynamic Data Preview 4 Refresh – Part 2

Continuing from the previous article Securing Dynamic Data Preview 4 Refresh – Part 1 we will proceed to complete the second two items in the to do list below:

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.

Secure Meta model classes

It would have been nice if I could have overridden the Scaffold and IsReadOnly methods of the MetaColumn class for this there would have been less code peppered around Dynamic Data but we can hope for the future.

public class SecureMetaModel : MetaModel
{
    /// <summary>
    /// Creates the metatable.
    /// </summary>
    /// <param name="provider">The metatable provider.</param>
    /// <returns></returns>
    protected override MetaTable CreateTable(TableProvider provider)
    {
        return new SecureMetaTable(this, provider);
    }
}

Listing 1 – SecureMetaModel class

public class SecureMetaTable : MetaTable
{
    /// <summary>
    /// Initializes a new instance of the <see cref="SecureMetaTable"/> class.
    /// </summary>
    /// <param name="metaModel">The meta model.</param>
    /// <param name="tableProvider">The table provider.</param>
    public SecureMetaTable(
        MetaModel metaModel, 
        TableProvider tableProvider) :
        base(metaModel, tableProvider) { }

    protected override void Initialize()
    {
        base.Initialize();
    }

    /// <summary>
    /// Gets the scaffold columns.
    /// </summary>
    /// <param name="mode">The mode.</param>
    /// <param name="containerType">Type of the container.</param>
    /// <returns></returns>
    public override IEnumerable<MetaColumn> GetScaffoldColumns(
        DataBoundControlMode mode, 
        ContainerType containerType)
    {
        return from column in base.GetScaffoldColumns(mode, containerType)
               where column.SecureColumnVisible()
               select column;
    }
}

Listing 2 – SecureMetaTable class

/// <summary>
/// Secures the column visible.
/// </summary>
/// <param name="column">The column.</param>
/// <returns></returns>
public static Boolean SecureColumnVisible(this MetaColumn column)
{
    var userRoles = Roles.GetRolesForUser();
    var activeDenys = column.GetColumnPermissions(userRoles);
    if (activeDenys.Contains(ColumnDeny.Read))
        return false;
    else
        return true;
}

Listing 3 – SecureColumnVisible extension method

[Flags]
public enum ColumnDeny
{
    Read    = 1,
    Write   = 2,
}

Listing 4 – ColumnDeny enum

So in the above four listings we have the simplified solution to hide columns based on user roles.

How it works

Listing 1 is required to let us include the SecureMetaTable in the default model Listing 2 is the SecureMetaTable all we do here is filter the columns based on user roles see Listing 3 SecureColumnVisible which hides columns based on ColumnDeny.Read this very easily and cleanly done thanks to the ASP.Net team and letting us derive from the meta classes.

Make columns read only using Entity Templates

Now to make columns read only in Edit or Insert modes based on use roles, for this we will modify tow of the default EntityTemplates Default_Edit.ascx.cs and Default_Insert.ascx.cs in these template we add the code in listing 5 to the DynamicControl_Init event handler.

if (currentColumn.SecureColumnReadOnly())
    dynamicControl.Mode = DataBoundControlMode.ReadOnly;
else
    dynamicControl.Mode = DataBoundControlMode.Edit; //Insert for the insert template
Listing 5 – Code to make a field read only in Edit or Insert modes
protected void DynamicControl_Init(object sender, EventArgs e)
{
    DynamicControl dynamicControl = (DynamicControl)sender;
    dynamicControl.DataField = currentColumn.Name;

    if (currentColumn.SecureColumnReadOnly())
        dynamicControl.Mode = DataBoundControlMode.ReadOnly;
    else
        dynamicControl.Mode = DataBoundControlMode.Edit; //Insert for the insert template
}

Listing 6 – finished DynamicControl_Init for the Edit Entity Template

/// <summary>
/// Secures the column read only.
/// </summary>
/// <param name="column">The column.</param>
/// <returns></returns>
public static Boolean SecureColumnReadOnly(this MetaColumn column)
{
    var userRoles = Roles.GetRolesForUser();
    var activeDenys = column.GetColumnPermissions(userRoles);
    if (activeDenys.Contains(ColumnDeny.Write))
        return true;
    else
        return false;
}

Listing 7 – SecureColumnReadOnly extension method

/// <summary>
/// Get a list of permissions for the specified role
/// </summary>
/// <param name="attributes">
/// Is a AttributeCollection taken 
/// from the column of a MetaTable
/// </param>
/// <param name="role">
/// name of the role to be matched with
/// </param>
/// <returns>A List of permissions</returns>
public static List<ColumnDeny> GetColumnPermissions(this MetaColumn column, String[] roles)
{
    var permissions = new List<ColumnDeny>();

    // you could put: 
    // var attributes = column.Attributes;
    // but to make it clear what type we are using:
    System.ComponentModel.AttributeCollection attributes = column.Attributes;

    // check to see if any roles passed
    if (roles.Count() > 0)
    {
        // using Linq to Object to get 
        // the permissions foreach role
        permissions = (from a in attributes.OfType<SecureColumnAttribute>()
                       where a.HasAnyRole(roles)
                       select a.Permission).ToList();
    }
    return permissions;
}

Listing 8 – GetColumnPermissions extension method

The above code Listings 5 & 6 simply test to see if the column is restricted to read only based on users roles and uses Listing 7 & 8 extension methods to achieve this.

And finally to make this work with Dynamic Data we need to modify the Global.asax

public class Global : System.Web.HttpApplication
{
    private static MetaModel s_defaultModel = new SecureMetaModel();
    public static MetaModel DefaultModel
    {
        get
        {
            return s_defaultModel;
        }
    }

    public static void RegisterRoutes(RouteCollection routes)
    {
        DefaultModel.RegisterContext(
            typeof(Models.NorthwindEntities), 
            new ContextConfiguration() { ScaffoldAllTables = true });

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

    void Application_Start(object sender, EventArgs e)
    {
        RegisterRoutes(RouteTable.Routes);
    }
}

Listing 9 – adding the SecureMetaModel to the Globalasax

Once we have a declared  our metamodel property in the Global.asax we can just reference it for the app by using it to register the context.

Download

Note: This has an ASPNETDB database which requires SQL Express 2008 and a connection to Northwind you will need to edit the connection string for Northwind.

I’m working on better hyperlinks for another post to follow shortly, these hyperlinks will offer the option of:

  • Hiding the link if it is disabled
  • Showing plain text if disabled

via an property at design time.

4 comments:

Michael Witt said...

Hi Steve, Just wondering if you have a good method of mixing your overrides of the MetaModel/MetaTable. I was already using the SecureMetaModel and then I wanted to use the HideColumnIn attribute which requires another MetaModel/MetaTable override. Seemed like I might be able to do a union of the two, but I just wondered if you had any suggestions.

Thanks and Cheers, Mike

Stephen J. Naughton said...

Hi Michael, have a look at http://csharpbits.notaclue.net/2010/06/securing-dynamic-data-4-replay.html

I believe if you mix that with

http://csharpbits.notaclue.net/2010/02/new-way-to-do-column-generation-in.html

you should have what you want.

Michael Witt said...

Thanks Steve, I'll give it a try.

Evaldas said...

Hi Steve,
I've been using secure meta model for a while, but i had to add another model to DD and used EFCodeFirstDataModelProvider. My my registration:
var model1 = new MetaModel();

model1.RegisterContext(new EFCodeFirstDataModelProvider(() => new Models.RVS_logistikaEntities()),
new ContextConfiguration() { ScaffoldAllTables = true });
model1.RegisterContext(new EFCodeFirstDataModelProvider(() => new Models.NoringeEntities()),
new ContextConfiguration() { ScaffoldAllTables = true });
model1.RegisterContext(new EFCodeFirstDataModelProvider(() => new Models.PasaraiGamybaEntities()),
new ContextConfiguration() { ScaffoldAllTables = true });

everything works but secure meta model isn't working. any clues?