La historia de mis desventuras

Palabras más, palabras menos sobre desarrollo de software.

Autorización configurable en ASP.NET MVC

Posted by Jhonny López Ramírez en 26 diciembre 2009

En esta entrada construiremos un prototipo de configuración de autorización para ASP.NET MVC. En el Web.config de la aplicación asociaremos las acciones del controlador con los roles que estarían autorizados. Para este caso tendremos una aplicación con tres roles: Administrator, Advanced, Limited.

Lo primero que crearemos será la sección de configuración que nos permitirá especificar la autorización a los distinos roles de la aplicación. Para ello, en el apartado <configsections> de nuestro web.config crearemos una sección de configuración:

<configSections>
 <section name="MyMvcAuthorization"
  type="System.Configuration.NameValueSectionHandler,System,Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
</configSections>
A continuación y para simular el comportamiento de una aplicación crearemos un Controller, una clase de modelo y unos métodos de acción. A continuación cada uno:

El modelo

En la carpeta models de nuestro proyecto agregaré un archivo Model.cs y escribiré el siguiente código:

namespace MVCAuthorization
{    
     public class Person    
    {        
          public string Name { get; set; }
          public int Id { get; set; }    
     }
}

Como ven, se trata de un POCO bastante sencillo. Vamos con el controller:

El controller

Una clase controladora sencilla:

namespace MVCAuthorization.Controllers

{

    public class PersonsController : MyBaseController

    {

        List<Person> persons = new List<Person>();

        //

        // GET: /Persons/

        [Authorize()]

        public ActionResult Index()

        {



            Person firstPerson = new Person

            {

                Id = 1,

                Name = "Fulano de Tal"

            };


            Person secondPerson = new Person

            {

                Id = 2,

                Name = "Sutano de Tal"

            };


            persons.Add(firstPerson);

            persons.Add(secondPerson);


            return View();

        }


        //

        // GET: /Persons/Details/5

        [Authorize()]

        public ActionResult Details(int id)

        {

            Person person = persons

                .FirstOrDefault(it => it.Id == id);


            return View(person);

        }


        //

        // GET: /Persons/Create

        [Authorize()]

        public ActionResult Create()

        {

            return View();

        }


        //

        // POST: /Persons/Create


        [AcceptVerbs(HttpVerbs.Post)]

        public ActionResult Create(Person person)

        {

            try

            {

                // TODO: Add insert logic here

                persons.Add(person);

                return RedirectToAction("Index");

            }

            catch

            {

                return View();

            }

        }


        //

        // GET: /Persons/Edit/5

        [Authorize()]

        public ActionResult Edit(int id)

        {

            return View();

        }


        //

        // POST: /Persons/Edit/5


        [AcceptVerbs(HttpVerbs.Post)]

        public ActionResult Edit(int id, FormCollection collection)

        {

            try

            {

                // TODO: Add update logic here


                return RedirectToAction("Index");

            }

            catch

            {

                return View();

            }

        }

    }

}

De esta clase controladora quiero que notemos dos cosas interesantes: en primer lugar, que la clase hereda de MyBaseController. La implementación de esa clase base la pondré aquí más adelante, dado que es esta clase la que se encargará de verificar si una acción está autorizada a un usuario en un rol determinado. En segundo lugar, que cada método (puntualmente cada GET) está decorado con el atributo [Authorize()]. Esto obligará a los usuarios no autenticados a loguearse. Y obligará, también, a que la clase base, como lo veremos más adelante, verifique para dichos métodos la autorización.

Lo que se hará a continuación es establecer, de esta clase controladora, los permisos de los roles frente a los métodos de acción. Para ello volvemos al web.config y agregamos la sección MyMvcAuthorization, que hemos definido anteriormente y que es un Dictionary tradicional:

<MyMvcAuthorization>

    <add key="PersonsController.Index" value="Administrator,Advanced,Limited"/>

    <add key="PersonsController.Create" value="Administrator"/>

    <add key="PersonsController.Edit" value="Administrator,Advanced"/>

    <add key="PersonsController.Details" value="Administrator,Advanced,Limited"/>

</MyMvcAuthorization>

En esta sección de configuración estamos diciendo algo sencillo: cada método de acción (el key) será autorizado a unos roles determinados (el value). Es así que Index está autorizado a todos los roles de la aplicación, lo cual no ocurre con Create o Edit, asociados a Administrator y Advanced y Administrator respectivamente. También aprovecharemos para activar la sección CustomErrors del mismo archivo:

<customErrors mode="On" defaultRedirect="GenericErrorPage.htm">

 <error statusCode="403" redirect="NoAccess.htm" />

 <error statusCode="404" redirect="FileNotFound.htm" />

</customErrors>

Ahora vamos con la clase MyBaseController:

namespace MVCAuthorization.Controllers

{

    [HandleError()]

    [HandleError(ExceptionType = typeof(SecurityException), View = "URNoAuthorized")]

    public class MyBaseController : Controller

    {


        protected override void OnActionExecuting(ActionExecutingContext filterContext)

        {

            string rolesByMethod = GetRolesByMethod(filterContext.ActionDescriptor.ActionName);

            bool isAutenticated = User.Identity.IsAuthenticated;

            bool authorize = false;

            bool requiresAuthorization = false;


            MemberInfo[] members = this.GetType().GetMember(filterContext.ActionDescriptor.ActionName);


            foreach (MemberInfo item in members)

            {

                if (item is MethodInfo)

                {

                    foreach (Attribute att in item.GetCustomAttributes(false))

                    {

                        if ((att is AuthorizeAttribute))

                        {

                            requiresAuthorization = true;

                            break;

                        }

                        if (requiresAuthorization) break;

                    }

                }

            }


            if (!requiresAuthorization)

                return;


            string[] roles = Roles.GetRolesForUser(filterContext.HttpContext.User.Identity.Name);


            foreach (var item in roles)

            {

                if (rolesByMethod.Contains(item))

                    authorize = true;

            }


            authorize = authorize && isAutenticated;


            if (requiresAuthorization & !authorize)

                throw new SecurityException();


        }


        private string GetRolesByMethod(string p)

        {

            NameValueCollection config =

                (NameValueCollection)System.Configuration.ConfigurationManager.GetSection("MyMvcAuthorization");


            string roles = config[this.GetType().Name + "." + p];


            if (!string.IsNullOrEmpty(roles))

                return roles;

            else

                return "";

        }

    }

}

Esta clase está compuesta por dos métodos: el primero sobreescribe a OnActionExecuting y cuenta con la lógica de verificar si un método requiere autorización y, de requerirla, si el usuario que solicita la acción tiene los permisos suficientes. El segundo método (GetRolesByMethod) va simplemente a nuestro archivo de configuración, en la sección que creamos, y trae los métodos asociados al método de acción. Es importante señalar también que la clase está decorada con el atributo [HandleError] y que aquí se dice que, si se presenta una SecurityException (arrojada por el controlador cuando el usuario que solicita no está autorizado), se redirigirá a la vista URNoAuthorized. Crearemos esa vista en la carpeta Views/Shared de nuestro website:
image
Simplemente tendrá por código una advertencia de no autorización. Sobre el resto de cosas no diremos mucho: una vista index, un create, un edit, un details, en fin, nada que no encuentren en otro tutorial sobre ASP.NET MVC. También he configurado para la aplicación la autenticación basada en Forms, he agregado los roles (Administrator, Advanced, Limited) y he agregado usuarios con para cada rol. Ahora probemos el comportamiento:
Usuario no autenticado intenta acceder a método que requiere autenticación

El atributo Authorize sobre el método dirigirá al usuario a la vista de autenticación:

image

Usuario autenticado accede a método autorizado

El usuario con role Limited accede a un método de acción que se le ha autorizado:

image

Usuario autenticado accede a método no autorizado
El usuario con rol Limited accede a un método no autorizado:
image

Y bueno, con un par de cosas que adornar por aquí y por allá tendremos una aplicación ASP.NET MVC con autorización configurable.

Descargue la solución haciendo clic aquí.

10 comentarios to “Autorización configurable en ASP.NET MVC”

  1. Pablo said

    Hola, estaba leyendo el articulo y me resultó muy interesante. Me surgen algunas dudas que quisiera preguntarte a ver si podes ayudarme, si yo quisiera que en vez de asociar los permisos a los roles en un archivo XML hacerlo en la base de datos, podria extender la pagina ASP .NET Configuration para hacer esto? como se haria?

    Desde ya muchas gracias.
    Saludos!

    • Sí, claro, podrías, en ese caso tendrías que modificar el método GetRolesByMethod del MyBaseController para consultar en la base de datos y no en en el archivo de configuración.

      • Pablo said

        Jhonny ante todo muchas gracias por responder. Por esto que me decis te hago una consulta mas: Como podría integrar la vista que controle esta asociación entre roles y permisos de función, en las paginas de administracion de usuarios y roles provistas por ASP .NET ?

        Saludos y gracias!

      • Pablo, se me ocurren un par de cosas al respecto. Si me das un par de días podría tenerte un ejemplo de cómo implementarlo, pero antes te hago una contrapregunta. A qué tipo de vistas te refieres cuando hablas de las provistas por ASP.NET? Lo digo porque me queda la duda de si tienes presente que este ejemplo se trata de ASP.NET MVC y no de ASP.NET WebForms. Es decir, aquí no tendríamos los controles de membresía que tenemos en el ASP.NET tradicional. Pero en un par de días (o antes si algo extraordinario ocurre) podría tenerte un ejemplo de cómo implementar lo que deseas.

  2. Pablo said

    Jhonny no te hagas problema lo que menos quisisera es hacerte trabajar de mas, si llegas a tener un link donde sepas que hay algo avisame, yo no encontré…pero no te pediria que hagas el ejemplo, no hace falta.
    Desde ya muchas gracias por tus respuestas.

    Saludos!

  3. Pablo said

    La verdad es que no encontré nada similar en la web, muchos articulos explican como usar el membership que te permite asociar usuarios a roles, ahora si lo que necesitas (como es mi caso en este momento) es ademas poder autorizar a usuarios a funciones especificas y todo eso poder administrarlo dinamicamente y guardarlo en un sql no encontré nada… y me parece que hoy en dia casi cualquier aplicacion que se desarrolle para una empresa tiene que usar un esquema similar, o sea que estamos reinventando la rueda constantemente, cuando esto deberia ser un problema resuelto…
    Si podes hacer ese aporte creo que seria muy util para mucha gente..

    Saludos

  4. Invitado said

    OK, EL POST ESTA MUY BUENO, PERO TENGO UNA DUDA, MIRA QUIERO HACER ESTO, EN UNA BASE DE DATOS TENGO MI TABLA DE USUARIOS,UNA TABLA CON NOMBRES DE MODULOS DE LA PAGINA, Y OTRA INTERMEDIA QUE ASOCIA A LOS USUARIOS CON LOS MODULOS A LOS QUE TIENE ACCESO, COMO HAGO PARA ADAPTAR TU EJEMPLO A LO QUE QUIERO SIN DEJAR DE LADO LA SEGURIDAD, PENSABA QUE CADA QUE SE HICIERA LA PETICION DE UNA VISTA SE TUVIERA QUE VALIDAR EN LA BASE DE DATOS SI EL USUARIO TIENE PERMISO PARA ACCEDER A DICHA VISTA Y SI NO QUE TE MANDARA UN MENSAJE X , PERO KREO K ESA NO ES LA MEJOR SOLUCION,NO ESTOY SEGURO DE LO QUE DEBO HACER ME PODRIAS SUGERIR ALGO?

  5. Pablo said

    Hola, como un complemento a tu entreda cabe señalar que también es posible utilizar un ticket de autorización a fin de extender la informacion de la cookie de autorización, luego a lo mas que se llega es a utilizar Identity en el sistema operativo, me gusta tu ejemplo pero no estoy de acuerdo en especificar roles en el Web.config ni menos utilizar Membership

    • Claro, completamente de acuerdo contigo. En defensa de esta entrada diré que es del año 2009 y bastante agua ha pasado por debajo de ambos puentes: el mío y el de ASP.NET MVC. Gracias por tu aporte. Saludos.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

 
A %d blogueros les gusta esto: