lunes, 5 de mayo de 2008

Implementando roles y permisos en ASP.Net con una clase base

En aplicaciones web por más básicas que sean, seguramente se debe implementar un mecanismo de seguridad que permita o restrinja el acceso de ciertos usuarios a determinadas páginas. De esta manera, podemos tener un rol Administrador y otro rol por default Usuario, en el que el primero podrá ingresar a páginas que un usuario común no debería.

Implementar esto en ASP.Net sin usar Personalization, es relativamente fácil. Se debe verificar en el momento en el que se carga la página si el usuario actual está autenticado o autorizado para acceder a ella. Fácil, pero engorroso. Hacer lo mismo para CADA página no es muy divertido que digamos. Sí, se puede usar una clase que use un método estático al que se le pase el nombre de la página actual y devuelva un valor booleano que me indique si tengo acceso o no. Pero igualmente deberíamos llamarlo en cada página.

Existe otro forma, a mi parecer mil veces más convienente. Hacer una clase base que herede de System.Web.Page, de la cual van a heredar en el codebehind las páginas de la aplicación.

Básicamente, lo que hace esta clase es preguntar “este que quiere acceder, tiene los permisos?”. Si es cierto, accede por supuesto, si no ejecuta una acción predeterminada en la misma clase base, que será seteable con una enumeración. Vamos por partes:

Definmos la enumeración de la acción a ejecutar en caso de acceso denegado:

public enum AccessDeniedEnum
{
    NoDefined,
    RedirectToLoginPage,
    ThrowHttpAccessDeniedException
}


Tendremos un método se llamará en el caso de acceso denegado y que preguntará por el valor de la enumeración y realizará una acción distinta en cada caso. El primer valor es el que tomará por defecto:

private void AccessDeniedAction()
{
    if (this._accessDeniedActionEnm == AccessDeniedEnum.NoDefined)
    {
        // acción por defecto
        this._accessDeniedActionEnm = AccessDeniedEnum.ThrowHttpAccessDeniedException;
    }
 
    switch (_accessDeniedActionEnm)
    {
        case WebGenesis.Utils.AppEnums.AccessDeniedEnum.RedirectToLoginPage:
            Response.Redirect("~/loginPage.aspx", true);
            break;
        case WebGenesis.Utils.AppEnums.AccessDeniedEnum.ThrowHttpAccessDeniedException:
            throw new HttpException(403, "No tiene los permisos necesarios para acceder a esta página");
            break;                
    }
}


También un método virtual (sobreescribible :D) que permite devolver directamente un true en caso de que existan páginas que necesitemos que sean de acceso general y no queremos tener que definir los permisos necesarios para todos, ni tampoco queremos perder el tiempo verficándolo. Entonces nuestra página sobreescribirá este método, y solamente retornará true. Los valores de la colección pageForRole son los nombres de las páginas, del tipo “pagina.aspx”, que el usuario actual puede ver.

protected virtual bool IsAutorizedAccess()
{
    if (Session["PagesForRole"] == null)
    {
        LoadRolePermissions();
    }
 
    //Obtengo las páginas autorizadas para el rol            
    List<string> pagesForRole = (List<string>)Session["PagesForRole"];
 
    string currentPageName = GetPageName();
 
    // Si la página está en la lista, el usuario está autorizado
    if (pagesForRole.Contains(currentPageName))
    {
        return true;
    }
 
    return false;
}


Entonces tendremos un método LoadRolePermissions que cargará las páginas para cada rol. El método está completo en el código final, es lo que habría que modificar para cargar las páginas por rol. Tomando los datos de una base de datos, XML, o de lo que sea. Y toma el usuario suponiendo que está cargado en sesión.

Ahora como se implementa esta clase base en las páginas? Simplemente heredándola en vez de la clase System.Web.Page

public partial class ejemplo1Framework : BaseClasses.BasePage

Si quiero hacer que sea se de público acceso: sobreescribo el método IsAutorizedAccess y retorno true.

protected override bool IsAutorizedAccess()
{
return true;
}

Si quiero cambiar lo “que va a hacer” en caso de que tener permiso denegado, como loquearse nuevamente, o mostrar un mensaje de error, etc, seteo la propiedad AccessDeniedActionEnm en el constructor de la página:

public partial class ejemplo2Framework : WebGenesis.BaseClasses.AbstractGeneralPage
{
public ejemplo2Framework()
{
// si necesito cambiar la acción por default de página denegada, lo hago aquí.
this.AccessDeniedActionEnm = AccessDeniedEnum.RedirectToLoginPage;

Page.Init += new EventHandler(Page_Init);
}
}

Este método que implemento es muy fácil de utilizar, y lo quería compartir. Aquí solo se hizo uso de la autorización de usuarios en esta clase base, pero se puede extender a todas las funcionalidades comunes que tendrán las páginas de nuestra aplicación. Espero que me haya explicado bien, como este es un blog, cualquier duda, crítica o amenaza me comentan. Les dejo el código completo de la clase base abajo. Saludos!

using System;
using System.IO;
using System.Data;
using System.Configuration;
using System.Collections.Generic;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
 
namespace BaseClasses
{
    /// <summary>
    /// Clase base de la cual heredarán todas las 
    /// páginas de la aplicación.
    /// Gestiona la autorización de usuarios.
    /// </summary>
    public abstract class BasePage : System.Web.UI.Page
    {
        // _enmAccessDenied se va a utilizar para realizar determinada 
        // una acción al intentar acceder a una página a la cual no se tiene acceso.         
        private AccessDeniedEnum _accessDeniedActionEnm;
        private string _pageName;
 
        public enum AccessDeniedEnum
        {
            NoDefined,
            RedirectToLoginPage,
            ThrowHttpAccessDeniedException
        }
 
        #region Properties
 
        public string PageName
        {
            get { return _pageName; }
        }
 
        public Utils.AppEnums.AccessDeniedEnum AccessDeniedActionEnm
        {
            get { return _accessDeniedActionEnm; }
            set { _accessDeniedActionEnm = value; }
        }
 
        #endregion
 
        #region Constructors
 
        public BasePage()
        {
            this.Init += new EventHandler(BasePage_Init);
            this.Load += new EventHandler(BasePage_Load);
        }
 
        #endregion
 
        #region PageEvents
        private void BasePage_Load(object sender, EventArgs e)
        {
        }
 
        private void BasePage_Init(object sender, EventArgs e)
        {
            if (!IsAutorizedAccess())
            {
                this.AccessDeniedAction();
            }
 
            _pageName = GetPageName();
        }
 
        #endregion
 
        #region Virtual Methods
 
        protected virtual bool IsAutorizedAccess()
        {
            if (Session["PagesForRole"] == null)
            {
                LoadRolePermissions();
            }
 
            //Obtengo las páginas autorizadas para el rol            
            List<string> pagesForRole = (List<string>)Session["PagesForRole"];
 
            string currentPageName = GetPageName();
 
            // Si la página está en la lista, el usuario está autorizado
            if (pagesForRole.Contains(currentPageName))
            {
                return true;
            }
 
            return false;
        }
 
        #endregion
 
        #region Private Methods
        /// <summary>
        /// Acción que se ejecuta al intentar ingresar a una página
        /// a la que no se tiene acceso.
        /// La acción se puede modificar en el constructor
        /// de cada página.
        /// </summary>
        private void AccessDeniedAction()
        {
            if (this._accessDeniedActionEnm == AccessDeniedEnum.NoDefined)
            {
                // acción por defecto
                this._accessDeniedActionEnm = AccessDeniedEnum.ThrowHttpAccessDeniedException;
            }
 
            switch (_accessDeniedActionEnm)
            {
                case WebGenesis.Utils.AppEnums.AccessDeniedEnum.RedirectToLoginPage:
                    Response.Redirect("~/loginPage.aspx", true);
                    break;
                case WebGenesis.Utils.AppEnums.AccessDeniedEnum.ThrowHttpAccessDeniedException:
                    throw new HttpException(403, "No tiene los permisos necesarios para acceder a esta página");
                    break;
            }
        }
 
        private void LoadRolePermissions()
        {
            //cargo en una lista las páginas autorizadas
            //para el rol
            List<string> pagesForRole = new List<string>();
 
            pagesForRole.Add("default.aspx");
            Session.Add("PagesForRole", pagesForRole);
 
            // implemento tomando los permisos desde la BD
            DataSet ds = new DataSet();
 
            //TODO hago algo inteligente aquí...
            if (ds.Tables.Count < 1)
                return;
            foreach (DataRow dr in ds.Tables[0].Rows)
            {
                pagesForRole.Add(dr["pageName"].ToString());
            }
            Session.Add("PagesForRole", pagesForRole);
            //
 
            return;
        }
 
        private string GetPageName()
        {
            //Obtengo el nombre de la página actual            
            FileInfo fi = new FileInfo(this.Page.Request.FilePath);
            return fi.Name.ToLower();
        }
 
        #endregion
    }
 
}

miércoles, 16 de abril de 2008

C# - Convertir números a letras

Esta debe ser una de las funciones más solicitadas por los desarrolladores, consiste en convertir un número dado a letras. Esta función recibe el número como parámetro, hagan un ToString() antes, porque lo recibe como string, y devuelve el resultado en letras.

Modifiqué el método y lo hice estático para no tener que instanciar un objeto para su uso.

Ejemplos de retornos:
8794 --> "OCHO MIL SETECIENTOS NOVENTA Y CUATRO"
97494131 --> "NOVENTA Y SIETE MILLONES CUATROCIENTOS NOVENTA Y CUATRO MIL CIENTO TREINTA Y UNO"
59843.4 --> "CINCUENTA Y NUEVE MIL OCHOCIENTOS CUARENTA Y TRES CON 40/100"

La clase:

using System;
using System.Collections.Generic;
using System.Text;
 
namespace Utilidades 
{
   public class Conversiones
   {
       public static string NumeroALetras(string num)
       {
           string res, dec = "";
           Int64 entero;
           int decimales;
           double nro;
 
           try
           {
               nro = Convert.ToDouble(num);
           }
           catch
           {
               return "";
           }
 
           entero = Convert.ToInt64(Math.Truncate(nro));
           decimales = Convert.ToInt32(Math.Round((nro - entero) * 100, 2));
 
           if (decimales > 0)
           {
               dec = " CON " + decimales.ToString() + "/100";
           }
 
           res = Utilidades.Conversiones.NumeroALetras(Convert.ToDouble(entero)) + dec;
           return res;
       }
 
       private static string NumeroALetras(double value)
       {
           string Num2Text = "";
           value = Math.Truncate(value);
 
           if (value == 0) Num2Text = "CERO";
           else if (value == 1) Num2Text = "UNO";
           else if (value == 2) Num2Text = "DOS";
           else if (value == 3) Num2Text = "TRES";
           else if (value == 4) Num2Text = "CUATRO";
           else if (value == 5) Num2Text = "CINCO";
           else if (value == 6) Num2Text = "SEIS";
           else if (value == 7) Num2Text = "SIETE";
           else if (value == 8) Num2Text = "OCHO";
           else if (value == 9) Num2Text = "NUEVE";
           else if (value == 10) Num2Text = "DIEZ";
           else if (value == 11) Num2Text = "ONCE";
           else if (value == 12) Num2Text = "DOCE";
           else if (value == 13) Num2Text = "TRECE";
           else if (value == 14) Num2Text = "CATORCE";
           else if (value == 15) Num2Text = "QUINCE";
           else if (value < 20) Num2Text = "DIECI" + toText(value - 10);
           else if (value == 20) Num2Text = "VEINTE";
           else if (value < 30) Num2Text = "VEINTI" + toText(value - 20);
           else if (value == 30) Num2Text = "TREINTA";
           else if (value == 40) Num2Text = "CUARENTA";
           else if (value == 50) Num2Text = "CINCUENTA";
           else if (value == 60) Num2Text = "SESENTA";
           else if (value == 70) Num2Text = "SETENTA";
           else if (value == 80) Num2Text = "OCHENTA";
           else if (value == 90) Num2Text = "NOVENTA";
 
           else if (value < 100) Num2Text = toText(Math.Truncate(value / 10) * 10) + " Y " + toText(value % 10);
           else if (value == 100) Num2Text = "CIEN";
           else if (value < 200) Num2Text = "CIENTO " + toText(value - 100);
           else if ((value == 200) || (value == 300) || (value == 400) || (value == 600) || (value == 800)) Num2Text = toText(Math.Truncate(value / 100)) + "CIENTOS";
 
           else if (value == 500) Num2Text = "QUINIENTOS";
           else if (value == 700) Num2Text = "SETECIENTOS";
           else if (value == 900) Num2Text = "NOVECIENTOS";
           else if (value < 1000) Num2Text = toText(Math.Truncate(value / 100) * 100) + " " + toText(value % 100);
           else if (value == 1000) Num2Text = "MIL";
           else if (value < 2000) Num2Text = "MIL " + toText(value % 1000);
           else if (value < 1000000)
           {
               Num2Text = toText(Math.Truncate(value / 1000)) + " MIL";
               if ((value % 1000) > 0) Num2Text = Num2Text + " " + toText(value % 1000);
           }
 
           else if (value == 1000000) Num2Text = "UN MILLON";
           else if (value < 2000000) Num2Text = "UN MILLON " + toText(value % 1000000);
           else if (value < 1000000000000)
           {
               Num2Text = toText(Math.Truncate(value / 1000000)) + " MILLONES ";
               if ((value - Math.Truncate(value / 1000000) * 1000000) > 0) Num2Text = Num2Text + " " + toText(value - Math.Truncate(value / 1000000) * 1000000);
           }
           else if (value == 1000000000000) Num2Text = "UN BILLON";
           else if (value < 2000000000000) Num2Text = "UN BILLON " + toText(value - Math.Truncate(value / 1000000000000) * 1000000000000);
           else
           {
               Num2Text = toText(Math.Truncate(value / 1000000000000)) + " BILLONES";
               if ((value - Math.Truncate(value / 1000000000000) * 1000000000000) > 0) Num2Text = Num2Text + " " + toText(value - Math.Truncate(value / 1000000000000) * 1000000000000);
           }
 
           return Num2Text;
       }
   }
}

Fuente: msmvps.com

C# - Cargar un comboBox desde una enumeración

Como cargar un comboBox (DropDownList Control) desde una enumeración. Hay varias formas de hacerlo, pero esta es la que uso por su simplicidad.

En el ejemplo cargo los colores definidos en la librería System.Drawing.

Para el ejemplo importar:

using System.Drawing;


Cargo el Combobox:

this.comboBox1.DisplayMember = "key";
this.comboBox1.ValueMember = "value";
this.comboBox1.DataSource = Enum.GetValues(typeof(KnownColor));


Obtengo el ítem seleccionado:

private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
{
    KnownColor colorSelected;
    colorSelected = (KnownColor)this.comboBox1.SelectedValue;
}


Como verán, primero lo casteo a la enumeración con lo cual lo cargo, para manipular luego el ítem como tal. Por supuesto que el método también aplica para VB.NET. Simple y eficaz.

lunes, 14 de abril de 2008

Crear y leer archivos ZIP en aplicaciones .Net

DotNetZip es un biblioteca open source que permite comprimir y extraer archivos zip. A diferencia de la librería System.IO.Compression, es más versátil y muy fácil de usar. Lo único que se necesita es bajar el archivo binario (DLL, menos de 20k) o el código fuente y agregarlo a nuestra solución.
Soporta el framework 2.0, 3.0 y 3.5.

Ejemplos

Comprimir un directorio (recursivo, incluye subdirectorios):

using (ZipFile zip = new ZipFile(args[0]))
{
  zip.AddDirectory(DirectoryName);
  zip.Save();
}
 


Comprimir múltiples archivos en diferentes directorios:

using System;
using Ionic.Utils.Zip; 
 
public class Example2
{
  public static void Main(String[] args)
  {
    try
    {
      using (ZipFile zip = new ZipFile("test2.zip"))
      {
 
        zip.AddItem("c:\\downloads\\vacaciones01.jpg", "images");
        zip.AddItem("c:\\Trabajo\\Specs_Lic2008.pdf", "files\\documents");
        zip.AddItem("test2.cs", "files\\text");
    
        zip.Save();
      }
    }
    catch (System.Exception ex1)
    {
      System.Console.Error.WriteLine("exception: " + ex1);
    }
  }
}
 
 


Extraer todo el contenido de un archivo .ZIP

using (ZipFile zip = ZipFile.Read(zipfile))
{
  zip.ExtractAll();
}