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
    }
 
}

jueves, 17 de abril de 2008

Buscar un texto en todas las bases de datos

En este artículo publican un stored procedure para SQL Server 2005 que permite buscar texto en todas las bases de datos del servidor.
Simplemente se le pasa el texto a buscar, y opcionalmente el nombre de la base de datos.

Ejemplo de uso:

exec dbo.proc_search_for_text 'Nathan'
exec dbo.proc_search_for_text 'Everett', 'AdventureWorks'


Links:
Search for a text in all databases

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();
}
 
 

ASP.Net - Leer el Web.Config desde una Class Library

Para leer el archivo Web.Config desde un proyecto del tipo Biblioteca de Clases, o Class Library, se necesita agregar una referencia a System.Web en el proyecto y cargarlo como un XML ordinario.
La manera de obtener el path correcto es mediante la clase System.Web.HttpRuntime.AppDomainAppPath.

[código]

// Obtengo el path de la raíz del sitio
string currentPath = System.Web.HttpRuntime.AppDomainAppPath.ToString(); 
string _xmlConfigurationFullPath = Path.Combine(currentPath, "Web.Config");
// cargo el Web.Config 
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.Load(_xmlConfigurationFullPath);


Se necesita importar los namespaces System.IO y System.XML.
Notarán que uso el método estático Combine de la clase Path para abstraerme de la combinación de un path y un nombre de archivo, con lo cual se evita la validación de, por ejemplo, preguntar si el path es raíz de unidad o no (y su particular "\" al final). No es mucho, pero el Namespace System.IO tiene clases de gran utilidad para la manipulación de archivos y directorios. Muy cómodo la verdad.

domingo, 13 de abril de 2008

No enviar tu propio artículo a Menéame

Por supuesto que alguna vez había escuchado de Menéame, pero nunca había entrado. Se me ocurrió registrarme y ver de que se trataba. Para los que no saben de que se trata, así lo define Wikipedia.

Y no tuve mejor idea que enviar un artículo propio, apenas unos minutos después de crear el blog y todavía menos de registrarme en Menéame. Y eso no fue muy bien recibido por la comunidad del sitio.

Moraleja: es obvia no?

No enviar tu propio artículo a Menéame
Links:
Los comentarios en Meneame

sábado, 12 de abril de 2008

Script SQL con datos de una tabla

Este es un script enormement útil que sirve para exportar una tabla solamente utilizando un script SQL. Por ejemplo el contenido de una tabla podría quedar de la siguiente manera:

INSERT INTO [Provincia] ([ID],[Nombre])VALUES(1,'BUENOS AIRES')
INSERT INTO [Provincia] ([ID],[Nombre])VALUES(2,'CATAMARCA')
INSERT INTO [Provincia] ([ID],[Nombre])VALUES(3,'CHACO')
INSERT INTO [Provincia] ([ID],[Nombre])VALUES(4,'CHUBUT')
INSERT INTO [Provincia] ([ID],[Nombre])VALUES(5,'CORDOBA')
INSERT INTO [Provincia] ([ID],[Nombre])VALUES(6,'CORRIENTES')
INSERT INTO [Provincia] ([ID],[Nombre])VALUES(7,'ENTRE RIOS')
INSERT INTO [Provincia] ([ID],[Nombre])VALUES(8,'FORMOSA')
INSERT INTO [Provincia] ([ID],[Nombre])VALUES(9,'JUJUY')
INSERT INTO [Provincia] ([ID],[Nombre])VALUES(10,'LA PAMPA')
INSERT INTO [Provincia] ([ID],[Nombre])VALUES(11,'LA RIOJA')
INSERT INTO [Provincia] ([ID],[Nombre])VALUES(12,'MENDOZA')
INSERT INTO [Provincia] ([ID],[Nombre])VALUES(13,'MISIONES')
INSERT INTO [Provincia] ([ID],[Nombre])VALUES(14,'NEUQUEN')
INSERT INTO [Provincia] ([ID],[Nombre])VALUES(15,'RIO NEGRO')
INSERT INTO [Provincia] ([ID],[Nombre])VALUES(16,'SALTA')
INSERT INTO [Provincia] ([ID],[Nombre])VALUES(17,'SAN JUAN')
INSERT INTO [Provincia] ([ID],[Nombre])VALUES(18,'SAN LUIS')
INSERT INTO [Provincia] ([ID],[Nombre])VALUES(19,'SANTA CRUZ')
INSERT INTO [Provincia] ([ID],[Nombre])VALUES(20,'SANTA FE')
INSERT INTO [Provincia] ([ID],[Nombre])VALUES(21,'SANTIAGO DEL ESTERO')
INSERT INTO [Provincia] ([ID],[Nombre])VALUES(22,'TIERRA DEL FUEGO')
INSERT INTO [Provincia] ([ID],[Nombre])VALUES(23,'TUCUMAN')
Para realizar esto Vyaskn creó un stored procedure al que se le pasa como parámetro el nombre de la tabla. El SP está sobrecargado y permite otros diez parámetros opcionales para afinar los resultados.

Lo único que se tiene que hacer es crear el SP (para SQL 2000 ó para SQL 2005), ver los resultados como texto, no como grilla y listo.

Una cosa, si no tienen permisos para exportar datos no hagan mal uso de este script :-D


Links:
GenerateInserts Stored Procedure para SQL 2000
GenerateInserts Stored Procedure para SQL 2005

viernes, 11 de abril de 2008

Script SQL de provincias y localidades argentinas

Programando, muchas veces tuve la necesidad de presentar al usuario un listado de provincias, ciudades y localidades, pero lo tenía que implementar presentando al usuario las provincias y dejarle la tarea al usuario de ingresar las ciudades/localidades mediantes respectivos ABMs.

La solución, hice un un script SQL para hacerme la vida más fácil obteniendo datos de excel y archivos de texto.
El script es lo que yo denomino un "insert script", o sea un "insert into.." para cada registro.

Los datos están separados en tres tablas [Provincia] -> [Departamento] -> [Localidad]

Cantidad de registros:
Provincias: 23
Departamentos: 574
Localidades: 5439

Descargar archivo