Articles in this Series
- Introduction - A DynamicData Attribute Based Permission Solution using User Roles.
- Part 1 - Permissions Attribute (Metadata) Classes.
- Part 2 - Sample Metadata for project.
- Part 3 - The Helper Extension Methods.
- Part 4 - Limit Tables shown on Default page and List, Edit & Details etc.
- Part 5 - Generate Columns/Rows (using IAutoFieldGenerator)
- Part 6 - Miscellaneous bits
- Part 7 - Updating the ListDetails Page
- DynamicData - Limit the Filter Fields
- DynamicData - Automatic Column Update
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; } }
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; }
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
27 comments:
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
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
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?
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
Steve,
Thanks for your excellent work. Your explanations and code help me to understand DD a lot better :-)
Thanks again,
Sylvester
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?
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
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
Hi Steve
Is there a way to add a further column to aps:DetailsView using IAutoFieldGenerator interface?
What I want to achieve is, that a new column with units or status information is generated on detailsview.
I think you could but there is probably a better way of doing this, can you be more specific.
Steve ;D
Okay
I have an Attribute like this
[AttributeUsage(AttributeTargets.Property)]
public class UnitAttribute : Attribute
{
public String Unit { get; private set; }
public UnitAttribute()
{
Unit = "-";
}
public UnitAttribute(String UnitExpression)
{
Unit = UnitExpression;
}
public static UnitAttribute Default = new UnitAttribute();
}
I can use this attribute to decorate my column with. What i now want to see on my detailsview on dynamic data is a third column containing this unit attribute.
I have now idea what happens in the background of "RowsGenerator" if i set
DetailsView1.RowsGenerator = new AutoFieldsManager(table, PageTemplate.Details, Roles.GetRolesForUser());
So, how can i achieve it?
Thanks for your help
Have a look at Listing 8 FilteredFieldManager for details of what goes on in side an IAutoFieldGenerator.
Steve :D
Hi Steve
Sorry for my late reply.
Hm, I'm sorry, but i don't get the answer from your last post? *confused
So let me ask in another way. Is it possible to return a 2-dimensional list of DynamicField from GenerateFields function (Listing 8) to force DetailsView.RowsGenerator to create a third column on detailsview?
Sorry, for my bad english. I hope it's now clearer.
Thanks for your help
Loopmaster
The IAutoFieldGenerator returns a list ot type DynamicFiled your problem is that there is no column so it may be possible to add an extra DynamicField that does not match a Column and map it to a FieldTemplate via UIhint.
I will give it a try. At least it could work for a gridview. But I don't think, that I can use this I idea for a detailsview, because there a table column becomes a row. That's not what I wanted a achieve.
Anyway. Thanks for your help
Loopmaster
Hi Steve,
How would you do the same without IAutoFieldGenerator (using FormView in Detail, Edig and Insert pages)...
Thanks!!
Hi there :) I have an article here Dynamic/Templated FromView this generates the Templates for a FormView. If you are using VS2010 then look at this article here
A New Way To Do Column Generation in Dynamic Data 4 Hope this helps.
Steve :D
I'm using vs2010 (also use CustomMetaTable and CustomMetaModel like you use in "A New Way To Do Column Generation in Dynamic Data 4"), but I don 't know how to asign DynamicReadonlyField/DynamicField in Fields of FormView
Sorry for my bad english.
Thanks again !!
Your English is better than my say French :) Have a look at this old post based on Preview 4 here Securing Dynamic Data Preview 4 Refresh I am working on a RTM Sample at the moment :)
Steve
How to add 2 IAutoFieldGenerator on Page_Init?
GridView1.ColumnsGenerator = new HideColumnFieldsManager(table, PageTemplate.List);
GridView1.ColumnsGenerator = new FilteredFieldsManager(table, Roles.GetRolesForUser());
You can't as they return a list of Fileds not columns.
If your using DD4 and VS2010 then I have a better way that can be chained see
A New Way To Do Column Generation in Dynamic Data 4 you couls use a delegate to generate columns and you could also allow then to be chained if you wanted.
Steve :D
Hi Steve,
Below is my code for Page_Init in List.aspx -
table = DynamicDataRouteHandler.GetRequestMetaTable(Context);
GridView1.SetMetaTable(table, table.GetColumnValuesFromRoute(Context));
GridDataSource.EntityTypeFilter = table.EntityType.Name;
table = GridDataSource.GetTable();
GridView1.ColumnsGenerator = new FilteredFieldsManager(table, Session["dept"].ToString());
But it is not calling GenerateFields() from FilteredFieldsManager.
Please suggest asap.
You may need to move the line to the Page_Load
GridView1.ColumnsGenerator = new FilteredFieldsManager(table, Session["dept"].ToString());
Steve
Thnks Steve. Its working.
But in Edit page fields are coming.
For that I am using same code with Formview1, but RowGenerator is not coming.
Please reply
Hi vivek, e-mail me direct not sure what your issue is, my e-mail is in the top right of my blog.
Steve
Steve,
My issue is how to implement same like what we did in list.apsx for Edit.aspx page. so that those columns should not come for particular role.
Sorry I can't mail you from here.
I think you are on .Net 4 so you will need to look at my new Column generation here A New Way To Do Column Generation in Dynamic Data 4 this gets rid of the requirement to use field generators and simplifies everything. Also you cant use column generators with the FormView.
Steve
Post a Comment