Monday, 26 May 2008

DynamicData - Generate Columns/Rows (using IAutoFieldGenerator) - Part 5

Articles in this Series

IAutoFieldGenerator

This is the heart of controlling which fields are shown in the List, Details, Edit views etc. Listing 1 is a skeleton of the Column/Row Generator, as you can see it is a class that is passed a table and has a loop which returns a ICollection of DynamicFields. The foreach loop is where the columns are processed similarly to the if (!column.Scaffold) which tests to see if the column to scaffolded.

using System.Collections.Generic;
using System.Web.DynamicData;

public class FilteredFieldsManager : IAutoFieldGenerator
{
    protected MetaTable _table;

    public FilteredFieldsManager(MetaTable table)
    {
        _table = table;
    }

    public ICollection GenerateFields(Control control)
    {
        List<DynamicField> oFields = new List<DynamicField>();

        // where do I put this test I think in the page?
        foreach (MetaColumn column in _table.Columns)
        {
            // carry on the loop at the next column 
            // if scaffold column is set to false
            if (!column.Scaffold)
                continue;

            // create new DynamicField
            DynamicField newField = new DynamicField();

            // assign column name and add it to the collection
            newField.DataField = column.Name;
            oFields.Add(newField);
        }
        return oFields;
    }
}

Listing 1

The class is used like:

// code to add column level security
table = GridDataSource.GetTable();
GridView1.ColumnsGenerator = new FilteredFieldsManager(table);

Listing 2

The code in Listing 2 is added to the Page_Init event, this is all that is needed in the pages code behind to add column level security.

Customising the FieldGenerator class

The first change is to add a new member variable _roles and change the Constructor to take an array of Strings.

protected MetaTable _table;
protected String[] _usersRoles;

public FilteredFieldsManager(MetaTable table, params String[] roles)
{
    _table = table;
    _usersRoles = roles;
}

Listing 3

Then in the GenerateFields method the permission for the current column will be assigned a local variable and then add a test to the if (!column.Scaffold) so that if the column is DenyRead then the column will also be skipped see Listing 4.

// get permissions for current column for current user roles.
var fieldPermissions = column.GetFieldPermissions(Roles.GetRolesForUser());

// carry on the loop at the next column 
// if scaffold table is set to false or DenyRead
if (!column.Scaffold  fieldPermissions.Contains(FieldPermissionsAttribute.Permissions.DenyRead))
    continue;

Listing 4

The next step is to add a DynamicReadonlyField class so that id a field is marked DenyEdit then a read only field can be returned instead of the regular DynamicField see Listing 5.

public class DynamicReadonlyField : DynamicField
{
    public override void InitializeCell(
        DataControlFieldCell cell,
        DataControlCellType cellType,
        DataControlRowState rowState,
        int rowIndex)
    {
        if (cellType == DataControlCellType.DataCell)
        {
            var control = new DynamicControl() { DataField = DataField };

            // Copy various properties into the control
            control.UIHint = UIHint;
            control.HtmlEncode = HtmlEncode;
            control.NullDisplayText = NullDisplayText;

            // this the default for DynamicControl and has to be
            // manually changed you do not need this line of code
            // its there just to remind us what we are doing.
            control.Mode = DataBoundControlMode.ReadOnly;

            cell.Controls.Add(control);
        }
        else
        {
            base.InitializeCell(cell, cellType, rowState, rowIndex);
        }
    }
}

Listing 5 - Thanks to David Ebbo for this class and the explanation here on the DynamicData forum.

Now if we add a test for the column has a DenyEdit security attribute assigned then the field can now be set to an DynanicReadOnlyField. In Listing 6 the fieldPermissions variable can be tested to see if it contains a DenyEdit permission.

DynamicField f;
if (fieldPermissions.Contains(FieldPermissionsAttribute.Permissions.DenyEdit))
{
    f = new DynamicReadonlyField();
}
else
{
    f = new DynamicField();
}

f.DataField = column.Name;
oFields.Add(f);

Listing 6

Listing 7 shows the code to test for foreign Key tables parent and child table permissions and if DenyRead do not show the field for the column.

//if foreign key table is hidden then hide the column that references it in this table
if (column.GetType() == typeof(MetaChildrenColumn))
{
    // Get permissions for current columns child table
    var childTablePermissions = column.GetChildrenTablePermissions(Roles.GetRolesForUser());

    // carry on the loop at next column
    if (childTablePermissions.Contains(TablePermissionsAttribute.Permissions.DenyRead))
        continue;
}

//if foreign key table is hidden then hide the column that references it in this table
if (column.GetType() == typeof(MetaForeignKeyColumn))
{
    // Get permissions for current columns parent table
    var parentTablePermissions = column.GetFkTablePermissions(Roles.GetRolesForUser());

    // carry on the loop at next column
    if (parentTablePermissions.Contains(TablePermissionsAttribute.Permissions.DenyRead))
        continue;
}

Listing 7

Put it all together in Listing 8.

public class FilteredFieldsManager : IAutoFieldGenerator
{
    protected MetaTable _table;
    protected String[] _usersRoles;

    public FilteredFieldsManager(MetaTable table, params String[] roles)
    {
        _table = table;
        _usersRoles = roles;
    }

    public ICollection GenerateFields(Control control)
    {
        List<DynamicField> oFields = new List<DynamicField>();

        // Get table permissions
        var tablePermissions = _table.GetTablePermissions(this._usersRoles);

        // if table is DenyRead then do not output any fields
        if (!tablePermissions.Contains(TablePermissionsAttribute.Permissions.DenyRead))
        {
            foreach (MetaColumn column in _table.Columns)
            {
                // get permissions for current column for current user roles.
                var fieldPermissions
                    = column.GetFieldPermissions(Roles.GetRolesForUser());

                // carry on the loop at the next column 
                // if scaffold table is set to false or DenyRead
                if (!column.Scaffold  fieldPermissions
                    .Contains(FieldPermissionsAttribute.Permissions.DenyRead))
                    continue;

                // if foreign key table is hidden then hide
                // the column that references it in this table
                if (column.GetType() == typeof(MetaChildrenColumn))
                {
                    // Get permissions for current columns child table
                    var childTablePermissions
                        = column.GetChildrenTablePermissions(Roles.GetRolesForUser());

                    // carry on the loop at next column
                    if (childTablePermissions
                        .Contains(TablePermissionsAttribute.Permissions.DenyRead))
                        continue;
                }

                // if foreign key table is hidden then hide
                // the column that references it in this table
                if (column.GetType() == typeof(MetaForeignKeyColumn))
                {
                    // Get permissions for current columns parent table
                    var parentTablePermissions
                        = column.GetFkTablePermissions(Roles.GetRolesForUser());

                    // carry on the loop at next column
                    if (parentTablePermissions
                        .Contains(TablePermissionsAttribute.Permissions.DenyRead))
                        continue;
                }

                DynamicField f;
                if (fieldPermissions
                    .Contains(FieldPermissionsAttribute.Permissions.DenyEdit))
                {
                    f = new DynamicReadonlyField();
                }
                else
                {
                    f = new DynamicField();
                }

                f.DataField = column.Name;
                oFields.Add(f);
            }
        }
        return oFields;
    }
}

Listing 8

having done all this remember all you have to add to the page is:

// code to add column level security
table = GridDataSource.GetTable();
GridView1.ColumnsGenerator = new FilteredFieldsManager(table, Roles.GetRolesForUser());

SQL Server 2005

SQL Server 2008

the sample already has Login and Roles setup (Account details below).

Website users:

  • admin
  • fred
  • sue
  • pam

all passwords are: password

8 comments:

Cormac said...

Hi Steve,
I finally got around to looking into table and column permissions for my application. Your sample code is great. Thanks for that. One question though. I noticed when I was running up the sample app that although I logged in as Fred (which shouldn't have read permissions on the order_detail table) that if I manually manipulated the url then I could get access to the that table. It seems to me that marking metadata on tables and columns is a tool to aid in the display of database tables/columns and not to 100% lock out access. Is this correct?
Cormac

Steve said...

Hi Cormac, if you look at part 4 and the section entitled "Some Error Handling for Pages Reached with Tables that are DenyRead" you will see the code that need to be added to each page to stop what you said is happening. So it's a bug, I will review the download and update where nessacery.

Steve
P.S. have a look at this new series here DynamicData: Database Based Permissions - Part 1 on my blog :D

Anonymous said...

Steve,

You mentioned that you are using a Deny Pattern for your permissions, where you limit functionality as additional attributes are tagged.

I setup my application with users and roles as follows:


Roles: Admin, Consumer, Buyer

User A is an Admin, Consumer, and Buyer

User B is a Consumer and a Buyer

User C is a Consumer


... you can see where I'm going.


When I ran the application User A was being denied access to functionality, this is because one of the other roles this person had was denied functionality. To get everything working with role based permissions setup like I just described I had to refactor the HasAnyRole method in the FieldPermissionsAttribute class to HasAllRoles:

public Boolean HasAllRoles(String[] roles)
{
foreach (var role in roles)
{
if (!HasRole(role))
return false;
}

return true;
}


Thoughts on this?

Steve said...

Yep thats the way I made it, of course you could make the logic more complicated, but I was just showing how and that level of complexity will suit more peoples uses :D

Steve

Anonymous said...

Steve,

Thanks for your excellent work. Your explanations and code help me to understand DD a lot better :-)

Thanks again,

Sylvester

SuicideSquad said...

Steve,

This is the init of list.aspx.cs:
protected void Page_Init(object sender, EventArgs e) {
table = DynamicDataRouteHandler.GetRequestMetaTable(Context);
GridView1.SetMetaTable(table);
GridDataSource.EntityTypeFilter = table.EntityType.Name;

table = GridDataSource.GetTable();
GridView1.ColumnsGenerator = new FilteredFieldsManager(table);
}

I added the class to the project as well, But my app does not go into public ICollection GenerateFields(Control control).
What is going wrong?

Steve said...

Hi Suicidesquad, from the code you have posted it looks like you are using either Preview 4 or Beta 1 VS2010, I have not tested in this environment yet so I'm not sure what the reason is but I will look into it ASAP.

Steve :D

Steve said...

Aparently there IAutoFieldGenerator works the same as in V1 according to my contacts. So I'll have a quick look later and see what happens. I'll use Preview4 and Beta 1 to test this.

Steve :D

Post a Comment