Pages

Wednesday, 30 May 2012

MVC - Displaying CheckBoxLists for Generic Lists

When developing an application, you may encounter a scenario where you have, for example, a customer record and that customer may be a member of several (but not all) customer lists in your application. 


All fairly straightforward. In an ASP.NET WebForms application for a customer management page where you want to select or un-select the lists a customer is in, you would typically bind the available customer lists to a CheckBoxList and in the DataBound event for this CheckBoxList control you would then mark checkboxes as checked for the lists your customer is in.


Depending on how you structure your application, this could involve two calls to your data store - one to get the available customer lists and one to get the customer details.


With MVC, the approach to this scenario takes a little adjustment!


The MVC Approach



There are several ways to handle displaying checkbox lists, however when I am developing MVC applications  I prefer to work solely with the strongly typed ViewModel I have created for my view. When doing this however, it is important to get the data in your ViewModel in a certain way.


Taking the example scenario above, in my application I want a customer edit view where I can edit the customer details and select or un-select the lists the customer is a member of. My customer entity looks something like:


    public class Customer
    {


        public Guid Id { get; set; }


        [Required(ErrorMessage = "A first name is required"), Display( Name = "First name")]
        public string Firstname { get; set; }


        [Required(ErrorMessage = "A surname is required"), Display(Name = "Surname")]
        public string Surname { get; set; }


        [Required(ErrorMessage = "An email address is required"), Display(Name = "Email address")]
        public string EmailAddress { get; set; }


        [Display(Name = "Added on")]
        public DateTime AddedOn { get; set; }


        [Display(Name = "Modified on")]
        public DateTime ModifiedOn { get; set; }


        [Display(Name = "Customer lists")]
        public List<CustomerList> Lists { get; set; }
    }

As you can see, I am including the lists a customer is a member of when I return an instance of a customer. My CustomerList entity is:



    public class CustomerList
    {


        public Guid Id { get; set; }


        [Required(ErrorMessage = "A name is required"), Display(Name = "Name")]
        public string Name { get; set; }


        [Display(Name = "Created on")]
        public DateTime CreatedOn { get; set; }


        [Display(Name = "Modified on")]
        public DateTime ModifiedOn { get; set; }


        public bool Checked { get; set; }
    }



In my customer repository, I have a GetCustomerById method to return a customer entity populated with the details for the customer:


        public Customer GetById(Guid id, string connectionString)
        {
            Customer customer = null;
            SQLQuery query = new SQLQuery("Customers.GetById", CommandType.StoredProcedure, connectionString);
            query.AddParameter("@Id", SqlDbType.UniqueIdentifier, id, ParameterDirection.Input);
            DataTableReader reader = query.ExecuteDataSet().Tables[0].CreateDataReader();


            if (reader.HasRows)
            {
                while (reader.Read())
                {
                    customer = new Customer
                                   {
                                       AddedOn = (DateTime) reader["AddedOn"],
                                       EmailAddress = (string) reader["EmailAddress"],
                                       Firstname = (string) reader["Firstname"],
                                       Id = new Guid(reader["Id"].ToString()),
                                       ModifiedOn = (DateTime) reader["ModifiedOn"],
                                       Surname = (string) reader["Surname"], 
                                       Lists = GetListsByCustomerId(id, connectionString)
                                   };
                }
            }
            return customer;
        }

The Lists property of my Customer entity is populated by another method called GetListsByCustomerId, which takes the Id of the customer and returns a list of CustomerLists to populate my Customer Lists property:


        public List<CustomerList> GetListsByCustomerId(Guid id, string connectionString)
        {
            List<CustomerList> lists = new List<Domain.Entities.CustomerList>();
            SQLQuery query = new SQLQuery("Customers.GetListsByCustomerId", CommandType.StoredProcedure, connectionString);
            query.AddParameter("@Id", SqlDbType.UniqueIdentifier, id, ParameterDirection.Input);
            DataTableReader reader = query.ExecuteDataSet().Tables[0].CreateDataReader();


            if (reader.HasRows)
            {
                while (reader.Read())
                {
                    CustomerList customerList = new Domain.Entities.CustomerList
                    {
                        CreatedOn = (DateTime)reader["CreatedOn"],
                        Name = (string)reader["Name"],
                        Id = new Guid(reader["Id"].ToString()),
                        ModifiedOn = (DateTime)reader["ModifiedOn"],
                        Checked = Convert.ToBoolean(reader["CustomerInList"])
                    };
                    lists.Add(customerList);
                }
            }
            return lists;
        }


Note: The SQLQuery object in the above sample is a custom SQLQuery component - if you are wanting to copy this code, you will need to change this appropriately to your own code to return a DataTableReader.

You'll have noticed in my CustomerList object, I have a property called Checked. This is because in my stored procedure to return the lists for a customer, I am returning all lists and not just the lists for that customer. In my stored procedure I am returning a value to indicate whether the customer is in the list or not, and setting the Checked property appropriately from my query value CustomerInList. This allows for the available lists and the lists a customer is in to be returned in one recordset, saving a database call. My stored procedure is:


DECLARE @temp TABLE ( Id uniqueidentifier, Name nvarchar(50), CreatedOn datetime, ModifiedOn datetime, CustomerInList bit)
INSERT INTO @temp (Id, Name, CreatedOn, ModifiedOn, CustomerInList)
SELECT 
Lists.Id, 
Lists.Name, 
Lists.CreatedOn, 
Lists.ModifiedOn,
(SELECT COUNT(Id) FROM Customers.CustomerLists WHERE ListId = Lists.Id AND CustomerId = @Id)
FROM Customers.Lists

SELECT * FROM @temp




The MVC View

In my Edit view, I now want to display my list of CustomerLists as checkboxes, with lists my customer is in marked as checked.


The best way to do this is with a PartialView and include this in your edit page:

                @model Services.ViewModels.EditCustomer 



                @foreach (var list in Model.Details.Lists)
                {
                    @Html.EditorFor(x => list, "CustomerLists")
                }
                <div>@Html.ValidationMessageFor(m => m.Details.EmailAddress)</div>

Note: The EditCustomer model is myViewModel which has a property of details. My populated customer entity is returned in this property

My CustomerList partical view is:


@model Domain.Entities.CustomerList


@Html.HiddenFor(x => x.Id)
@Html.HiddenFor(x => x.Name)
<div>
    @Html.CheckBoxFor(x => x.Checked)
    @Html.LabelFor(x => x.Checked, Model.Name)
</div>




When the view is rendered, I will now have a list of checkboxes, with one for each of my CustomerLists, and checked if my customer is a member of that list.


Hope this helps - you can download a sample MVC3 application from here





Friday, 25 May 2012

MVC3 - Securing with Authentication and Anti-forgery by Default

Following on from my post about securing MVC3 application by use of global filters, I have released today a package on nuget.org that can be installed to do this automatically.


In addition to requiring to set explicit anonymous access for controller actions you want to be accessible anonymously, there is also ad additional filter for anti-forgery tokens. The additional filter requires you to pass an anti-forgery token in every form post.


Please download the package and let me know what you think! 


https://nuget.org/packages/ValidationAndAuthenticationByDefault.MVC3/1.0.0 

Wednesday, 2 May 2012

Securing Your MVC Intranet Applications - Security by Default

The standard way to secure your MVC Intranet applications is to use the [Authorize] attribute for controller actions you want to secure. The controller is the resource you're actually trying to protect and any security decisions should be done at the controller level rather than at the route level.


For example using the default Intranet Application template in MVC3, you will have a Home Controller that looks like the following:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;


namespace MvcAuthenticationSample.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            ViewBag.Message = "Welcome to ASP.NET MVC!";


            return View();
        }


        public ActionResult About()
        {
            return View();
        }
    }
}

To force users to log on when they access /Home/, you would add the [Authorize] attribute to your Index ActionResult, like this:

[Authorize]

public ActionResult Index()
        {
            ViewBag.Message = "Welcome to ASP.NET MVC!";


            return View();
        }

If you are using the built in Membership provider included in the Intranet Application template and set the [Authorize] attribute on the Index() ActionResult above, your users will be redirected to the LogOn action in your Account Controller. If a user accesses /Home/About/, they will not be redirected.


Hopefully you can see the flaw in this approach for an Intranet Application as a controller action may be created where the [Authorize] attribute is not set where it should be. You might think this will be mitigated by testing, but it can be easily overlooked.


The best way to secure your MVC Intranet Application is to require have all controller actions by default  require authorisation. You can then open up access to controller actions you want users to be able to access anonymously, which leads to a much better approach to security.


One way of doing this is to create a base controller with the [Authorize] attribute set, and have all your controllers derive from this base controller. However, with this approach there is nothing to stop a controller being created that doesn't derive from the base controller you have created.


A much better way to do this is to use Global Filters. This allows you to set a global filter for all controllers in your application, which means they will have the [Authorize] attribute set by default, whether or not they derive from a base controller. Coupled with this, you can then create a custom attribute which you set explicitly for controller actions that do not require authorisation.


To do this, first you need to create your custom allow anonymous attribute:


using System;


[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class AllowAnonymousAttribute : Attribute { }

You will also need to create a filter that derives from AuthorizeAttribute:

public sealed class LogonAuthorize : AuthorizeAttribute
    {
        public override void OnAuthorization(AuthorizationContext filterContext)
        {
            bool skipAuthorization = filterContext.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true) || filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true);
            if (!skipAuthorization)
            {
                base.OnAuthorization(filterContext);
            }
        }
    }

You will then need to register this new filter in RegisterGlobalFilters in global.asax:

        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());
            filters.Add(new LogonAuthorize());
        }

And voila! Your controllers will require authorisation by default. However...

With this filter registered, you will not be able to log on! This is where you need to set you AllowAnonymous attribute in your Account Controller to allow users to access actions associated with logging in to your application:

        [AllowAnonymous]
        public ActionResult LogOn()
        {
            return View();
        }

        //
        // POST: /Account/LogOn

        [HttpPost]
        [AllowAnonymous]
        public ActionResult LogOn(LogOnModel model, string returnUrl)
        {
            if (ModelState.IsValid)
            {
                if (Membership.ValidateUser(model.UserName, model.Password))
                {
                    FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
                    if (Url.IsLocalUrl(returnUrl) && returnUrl.Length > 1 && returnUrl.StartsWith("/")
                        && !returnUrl.StartsWith("//") && !returnUrl.StartsWith("/\\"))
                    {
                        return Redirect(returnUrl);
                    }
                    else
                    {
                        return RedirectToAction("Index", "Home");
                    }
                }
                else
                {
                    ModelState.AddModelError("", "The user name or password provided is incorrect.");
                }
            }

            // If we got this far, something failed, redisplay form
            return View(model);
        }

I would personally like to see future version of MVC intranet application templates implement a secure by default approach to controllers. Inadvertently creating security holes in applications can be a big problem that is difficult to rectify.

A sample project can be downloaded from here.