Subpanels

En el anterior post hice una preview (y breve introducción) de una funcionalidad que me ha resultado útil en una aplicación web y espero lo sea en otras. Se trata de subpaneles que permiten agregar elementos durante la carga de otros elementos, por ejemplo permiten agregar Categorías durante el alta de un Libro, o Tiendas durante la carga de una Compra, o Ciudades en Provincias en Países durante la carga de un Cliente, etc.

Algunas características

  • Carga de subforms asincrónica y solo cuando se abre el subpanel (ajax).
  • Eventos para poder enlazar el subpanel con la página contenedora (callbacks).
  • Permite anidar subpaneles (recursive).
  • Integramente escrito con jquery y jquery-ui (client-side).

Usabilidad

Consideremos la siguiente pantalla:
subpanels-6
Cuando configuramos un subpanel, lo único que vemos es que se agrega un botón [+] a la pantalla, como el siguiente:
subpanels-1
Si presionamos este botón [+] se abrirá un subpanel que si estuviese vacío tendría un aspecto similar a:
subpanels-2
pero en nuestro caso tiene el contenido de la url configurada y se ve como el siguiente:
subpanels-5
cuando hagamos el POST del form del subpanel este se procesará, luego se llamará a la función configurada como success (si es que hay una) y, a menos que esta devuelva false, cerrará el subpanel. La función configurada como success recibirá como parámetro la respuesta del POST al servidor y se puede utilizar para mostrar mensajes de validación, actualizar otros controles enlazados al subpanel, etc.

Código

Veamos como logramos estos, considerando la primer imagen de este post, el html contendría algo parecido a lo siguiente:

<body>
  <div class='ui-widget ui-state-default ui-corner-all'>
    <div class='ui-widget-content ui-corner-all'>
      <h1>Compra de libros</h1>
      <form action='/compra' method='POST'>
        <p style="display: inline">Vendedor: 
        <select id="vendedor" name="vendedor" style="vertical-align: top;">
        </select>
        <div id="pnvendedor" style="display: inline"></div></p>
        <p>Fecha: <input id='fecha' name='fecha' type="text" /></p>
        <p style="display: inline">Libro: 
        <select id="libro" name="libro" style="vertical-align: top;">
        </select>
        <div id="pnlibro" style="display: inline"></div></p>
        <p>Cantidad: <input name='cantidad' type="text" /></p>
        <p><input type='submit' value='Guardar'/></p>
      </form>
    </div>
  </div>
</body>
como podemos observar, seguido al elemento select para el combo de vendedor (o de libro) tenemos un div donde se configurará nuestro subpanel. En estos divs se cargaran los botones [+] y [-] que abrirán y cerrarán el panel respectivamente.
para que un div se transforme en un subpanel debemos llamar a la función subpanel(…) como muestra el siguiente código:
$("#pnvendedor").subpanel({ 
  url: 'vendedor.html',
  success: function(data) { addToSelectElement($("#vendedor"), data); },
  imageLoding: '/css/loader.gif'
});
en este código estamos seleccionado el div cuyo id es pnvendedor y transformándolo en un subpanel, cuyo contenido está dado por vendedor.html y se invocará a la función addToSelectElement luego de que el POST del form del subpanel (es decir el subform) sea compleado.

Subpaneles anidados

También podemos anidar subpaneles, en el ejemplo, al abrir el subpanel de Libro tiene otro [+] para agregar Categoría:
subpanels-3
que cuando se presiona en este [+] se abre el otro subpanel  para agregar una Categoría.
subpanels-4

Descargas:

Archivos para usar los subpanels en cualquier sitio (solo js y css)
Archivos del entorno de desarrollo (incluye tests con qunit y servidor con node.js)
Código del ejemplo (incluye html y servidor en node.js)

Subpaneles – Preview

Sistema de subpaneles (o subformulario) que funcionan del lado del browser comunicándose mediante request asincrónicos al servidor (ajax). Originalmente fue desarrollado y usado en el sitio de la comunidad alt.net hispano. Luego generalizado en este ejemplo, testeado con qunit y con node.js del lado del servidor.
Unable to display content. Adobe Flash is required.
El código es algo como:
$("#pnvendedor").subpanel({
                    successful: function(data) { addToSelectElement(data, $("#vendedor")); },
                    imageLoding: "/css/loader.gif"
                    url: "vendedor.html" 
                });
donde pnvendedor es un contendor (div) donde se ubicarán los botones [+] y [–], por ejemplo:
<div id="pnvendedor" style="display: inline"></div>

Ir al post de subpanels

ASP.NET MVC - ModelMetadata basada en convenciones y recursos

Cuando trabajamos con mvc, posiblemente tengamos nuestros models plagados de strings para definir nombres, mensajes de validación, nombres de recursos y posiblemente estos se repitan en varios models. Este ejemplo intenta mostrar una forma de resolverlo mediante convenciones y recursos.

Metadata basada en DataAnnotacion básica

Partiendo del siguiente ejemplo podemos observar varios strings hardcode

public class RegisterModel {
    [Required(ErrorMessage = "The User name is required")]
    [Display(Name = "User name")]
    public string UserName { get; set; }

    [Required(ErrorMessage = "The Email address is required")]
    [DataType(DataType.EmailAddress)]
    [Display(Name = "Email address")]
    public string Email { get; set; }

    [Required(ErrorMessage = "The Password is required")]
    [ValidatePasswordLength]
    [DataType(DataType.Password)]
    [Display(Name = "Password")]
    public string Password { get; set; }

    [DataType(DataType.Password)]
    [Display(Name = "Confirm password")]
    [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
    public string ConfirmPassword { get; set; }
}

uno de los problemas que plantea esto es la internacionalización

Metadata basada en recursos

Para solucionar la internacionalización es bien conocido el uso de recursos, para esto indicamos los textos como recursos:
Screenshot
y nuestro model debería ser algo parecido a:

public class RegisterModel
{
    [Required(ErrorMessageResourceType = typeof(Resources), ErrorMessageResourceName = "UserNameRequired")]
    [Display(ResourceType = typeof(Resources), Name = "UserName")]
    public string UserName { get; set; }

    [Required(ErrorMessageResourceType = typeof(Resources), ErrorMessageResourceName = "EMailRequired")]
    [DataType(DataType.EmailAddress)]
    [Display(ResourceType = typeof(Resources), Name = "EMail")]
    public string Email { get; set; }

    [Required(ErrorMessageResourceType = typeof(Resources), ErrorMessageResourceName = "PasswordRequired")]
    [ValidatePasswordLength]
    [DataType(DataType.Password)]
    [Display(ResourceType = typeof(Resources), Name = "Password")]
    public string Password { get; set; }

    [DataType(DataType.Password)]
    [Display(ResourceType = typeof(Resources), Name = "ConfirmPassword")]
    [Compare("Password", ErrorMessageResourceType = typeof(Resources), ErrorMessageResourceName = "ConfirmPasswordRequired")]
    public string ConfirmPassword { get; set; }
}

con esto resolvimos la internacionalización, ahora tenemos los mensajes en un archivo de recursos, pero seguimos teniendo muchos strings hardcode que ahora hasta parecen seguir un determinado patrón. Además, si tuviésemos otro model con la propiedad Email o UserName tendríamos que volver a definir todos los recursos.

Metadata basada en convenciones

La propuesta de “Metadata basada en convenciones” intenta buscar el nuestro model se parezca a algo como lo siguiente:

public class RegisterModel
{
    [Required]
    public string UserName { get; set; }

    [Required]
    [DataType(DataType.EmailAddress)]
    public string Email { get; set; }

    [Required]
    [ValidatePasswordLength]
    [DataType(DataType.Password)]
    public string Password { get; set; }

    [DataType(DataType.Password)]
    [Compare("Password")]
    public string ConfirmPassword { get; set; }
}
y esto se logra implementando un custom ModelMetadataProvider como el siguiente:
public class DataAnnotationAndResourcesModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
    private List<Attribute> _attributeList;

    protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
    {
        _attributeList = new List<Attribute>(attributes);

        var modelMetadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);

        if (propertyName != null)
        {
            if (string.IsNullOrWhiteSpace(modelMetadata.DisplayName))
                modelMetadata.DisplayName = SearchResource(propertyName);

            if (string.IsNullOrWhiteSpace(modelMetadata.Description))
                modelMetadata.Description = SearchResource(propertyName + "Description");

            var validators = _attributeList.OfType<ValidationAttribute>();
            foreach (var validator in validators)
            {
                if (string.IsNullOrWhiteSpace(validator.ErrorMessage) &&
                    string.IsNullOrWhiteSpace(validator.ErrorMessageResourceName))
                {
                    var resourceName = propertyName + validator.GetType().Name;
                    if (resourceName.EndsWith("Attribute"))
                        resourceName = resourceName.Substring(0, resourceName.Length - 9);
                    var resourceType = validator.ErrorMessageResourceType ?? typeof(Resources);
                    var prop = resourceType.GetProperty(resourceName);
                    if (prop != null)
                    {
                        validator.ErrorMessageResourceType = resourceType;
                        validator.ErrorMessageResourceName = resourceName;
                    }
                }
            }
        }

        return modelMetadata;
    }

    private static string SearchResource(string resourceName)
    {
        string displayName = null;
        var resourceType = typeof(Resources);
        var prop = resourceType.GetProperty(resourceName);
        if (prop != null)
        {
            var value = prop.GetValue(resourceType, null);
            displayName = value != null ? value.ToString() : resourceName;
        }
        return displayName;
    }
}
Para que mvc utilice este provider, debemos indicárselo en ModelMetadataProviders.Current, por ejemplo en el Application_Start de Global.asax:
ModelMetadataProviders.Current = new DataAnnotationAndResourcesModelMetadataProvider();
Este ModelMetadataProvider primero crea la metadata de la forma tradicionar (basándose en DataAnnotation), si esta no indica los nombres, mensajes, etc. se basa en convenciones de nombres para buscar en los recursos:
  • Si no está indicado el DisplayName, busca en los Resources uno que tenga el mismo nombre de la propiedad.
  • Si no está indicada la Description, busca en los Resources uno que tenga el mismo nombre de la propiedad y que termine en “Description”.
  • Para cada ValidationAttribute busca entre los Resources uno que comience con el nombre de la propiedad y termine con el nombre del attribute, por ejemplo: UserNameRequired para la propiedad UserName que tiene indicada la validación Required.

Y si tenemos otros models con las propiedades UserName, EMail, etc. Los recursos serán reutilizados cuando sea necesario si necesidad de definirlo.

Nota: con esto resolvemos nombres, mensajes, etc. Es decir los textos de comunicación con el usuario, no definimos comportamiento, para esto último seguimos utilizando DataAnnotations.
Una implementación real (y mas completa) de esto se puede ver en el proyecto que estamos desarrollando en la comunidad alt.net hispano para la gestión administrativa de eventos haciendo clic aquí.

NHibernate y el patrón State

Este es un ejemplo de como persistir con NHibernate una propiedad cuyo tipo de dato es otra clase no persistente, en este caso el Estado de un Evento.

El dominio

Domain

En este dominio el único objeto que se persiste es Evento, el mismo tiene una propiedad Estado de tipo EventoState que es una clase abstracta cuyas posibles implementaciones son las que se ven heredando de esta.

Para el caso del estado no seteado contamos con la clase EventoNullState que tiene el comportamiento correspondiente a este estado del evento:

public virtual EventoState Estado
{
    get { return _estado ?? EventoNullState.GetInstance(); }
    set { _estado = value; }
}

Objetivo

Persistir en una columna de la tabla Evento el Estado que tiene el objeto y que cuando este sea recuperado de la base de datos se cargue el Estado correspondiente.

Persistencia

Para configurar NHibernate utilizamos ConfOrm de la siguiente manera:

var orm = new ObjectRelationalMapper();
orm.TablePerClass<Evento>();

orm.Complex<Evento>(e => e.Estado);

var mapper = new Mapper(orm);
mapper.AddPropertyPattern(p => p.DeclaringType == typeof(Evento) && p.Name == "Estado", a => a.Type<EventoStateType>());

como podemos observar, definimos un patrón para la propiedad Estado diciendo que utilice una instancia de tipo EventoStateType para pasar de objeto a base de datos o viceversa. En esta clase estamos extendiendo la clase GenericWellKnownInstanceType que podemos referenciar desde unhaddins o simplemente tomarla “prestada” (haga clic aquí para ver la clase original) ya que esta clase no depende de otras.

EventoStateType

public class EventoStateType : GenericWellKnownInstanceType<EventoState, string> 
{
    public static IEnumerable<EventoState> All
    {
        get
        {
            return new[]
                   {
                       EventoNullState.GetInstance(), EventoPropuestoState.GetInstance(), EventoAgendadoState.GetInstance(),
                       EventoConfirmadoState.GetInstance(), EventoPublicadoState.GetInstance(), EventoCanceladoState.GetInstance(),
                       EventoDescartadoState.GetInstance()
                   };
        }
    }

    public EventoStateType()
        : base(All, (state, id) => state.Descripcion == id, state => state.Descripcion)
    {
    }

    public override SqlType[] SqlTypes
    {
        get { return new[] {SqlTypeFactory.GetString(25)}; }
    }
}

En el constructor de EventoStateType podemos observar la llamada al constructor de GenericWellKnownInstanceType al que le pasamos los siguientes valores:

    All Una lista con todos los posibles estados.
    (state, id) => state.Descripcion == id una función que será evaluada para relacionar el objeto (state) con su identificador en la base de datos (id).
    state => state.Description una función que, dado un objeto (state), devuelva el identificador con que será persistido.

Y para terminar, en la tabla donde se persiste cada Evento vamos a necesitar un campo con el tipo correspondiente a un string de 25, según SqlTypeFactory.GetString(25) en el cual se persistirá el valor de pa propiedad Descripcion de cada estado.

El código completo podés verlo en el proyecto que estamos desarrollando en la comunidad alt.net hispano para la gestión administrativa de eventos haciendo clic aquí.

Cambios en el acceso a mi sitio web al implementar Autenticación y Autorización por ADFS

Este post es parte de una serie de artículos complementarios a la VAN sobre Identity Providers.

Escenario

A continuación veremos el flujo de request que ocurren en el browser cuando ingresamos a nuestro sitio web después de implementar la seguridad con ADFS, pero antes voy a hacer un cuadro con los actores que intervienen en este ejemplo.

Actores

testigoadfs.neluz.int Es nuestra aplicación web, en la cual queremos delegar la autenticación y autorización a ADFS.
testigomvc.neluz.int Es otra aplicación que utiliza ADFS, en este caso para ejemplificar el single sign on.
adfs.neluz.int Es el sitio web de ADFS, que cumple el rol de emisor de confianza (Issuer).
starter-sts.neluz.int Es el Identity Provider, encargado de autenticar al usuario mediante credenciales (usuario y password).

 

Flujo del primer ingreso

De nuestro sitio web a la pantalla de login

Cuando ingresamos al sitio web testigoadfs.neluz.int sucede lo siguiente:

flow-adfs-1

El HttpModule de WIF redirecciona a adfs.neluz.int (Issuer) ya que no encuentra la información emitida por ADFS que autorice el ingreso al sitio. Este, a su vez redirecciona a issue.aspx de starter-sts.neluz.int (el IP-STS) ya que no encuentra información de autenticación. Issue.aspx no tiene información de que el usuario esté autenticado (Single Sign On) por lo que redirecciona a login.aspx dentro de starter-sts para que el usuario ingrese su username y password.

De la pantalla de login a nuestro sitio web

Luego de ingresar las credenciales correctamente, sucede lo siguiente:

flow-adfs-2

flow-adfs-3a

flow-adfs-3

Hacemos un post en login.aspx de starter-sts el cual valida el usuario y password ingresados y, como en este caso son válidos, redirecciona a issue.aspx, es decir que empieza a “desandar” el camino. Luego issue.aspx emite la información correspondiente a la autenticación para ADFS y hace un POST a este donde se genera la información de autorización para el módulo de WIF en nuestra aplicación que la recibe en el POST de nuestro sitio y ahora si podemos ingresar (es decir que el httpModule de WIF nos deja pasar).

Flujo visto como un diagrama de secuencia

flow-sequence-1 y 2

Navegando a otra página dentro del mismo sitio

Cuando intentamos acceder a otra página de nuestro sitio web vemos lo siguiente

flow-adfs-6

Vemos que no ocurre nada de los anterior, porque el HttpModule de WIF encuentra la información correspondiente a la autorización emitida por ADFS y directamente nos deja pasar.

Contenido del mensaje emitido por ADFS

flow-adfs-claims

Segunda aplicación (SSO)

Cuando ingresamos a otra aplicación que delega la autenticación y autorización a ADFS vemos lo siguiente:

adfs-flow-4

flow-adfs-5

Como podemos observar se repite el camino casi como el primer ingreso, es decir, WIF no encuentra la información de autorización para ingresar a la aplicación y redirecciona a adfs.neluz.int, este a issue.aspx de starter-sts pero, a diferencia del primer ingreso, este si encuentra que el usuario está autenticado por lo que directamente emite la información de autenticación necesaria para ADFS. Ahora si ADFS encuentra la información y realiza la autorización para ingresar a la aplicación, finalmente WIF nos deja pasar. En este caso no tuvo que intervenir el usuario, a lo sumo notó algún cambio en las url del browser.

Flujo visto como un diagrama de secuencia

flow-sequence-3

Seguir leyendo otros artículos de la serie

Manejando Roles por aplicación mediante ADFS

Este post es parte de una serie de artículos complementarios a la VAN sobre Identity Providers.

Escenario

Comúnmente, en asp.net, utilizamos la siguiente sintaxis para preguntar si una persona tiene asignado un determinado rol:

HttpContext.Current.User.IsInRole("Administrators")

o, en MVC, con el atributo:

[Authorize(Roles = "Administrators")]

Configurando ADFS

Con ADFS hacemos lo mismo. El tema está en configurar correctamente el issuer (ADFS) para que emita los claims correspondientes, para esto debemos configurar una “Claim Rule” que genere tantos claims con el Type “http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name” como roles tenga la persona en la aplicación, en el Value de cada uno de estos claims irá el nombre de cada rol.

Ejemplo de Claim Rule

c:[Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"]
=> issue(store = "AttributeStore", types = ("http://schemas.microsoft.com/ws/2008/06/identity/claims/role"), query = "SELECT Role FROM Roles WHERE UserName = {0} AND RelyingParty = {1}", param = c.Value, param = "https://aplicacionweb.neluz.int/");

Con la siguiente Claim Rule estamos haciendo un select sobre una tabla llamada Roles donde tenemos un registro por cada Rol (Role) que tiene asignado el usuario (UserName) en la aplicación a la que está queriendo ingresar (RelyintParty), todo esto sobre la base de datos configurada en AttributeStore.

Nota: una ventaja que nos da ADFS en este esquema es que los roles son por Usuario y por Aplicación, por sobre el caso de usar los grupos de AD donde los roles son solo por Usuario.

Seguir leyendo otros artículos de la serie

Controlando el acceso a mi sitio web con autorizaciones mediante ADFS

Este post es parte de una serie de artículos complementarios a la VAN sobre Identity Providers.

Escenario

Para decidir cual usuario ingresa y cual no a una aplicación debemos configurarlo en ADFS, es decir que si el usuario no tiene autorización para ingresar a la aplicación, ADFS nunca va a redireccionar nuevamente al sitio que lo invocó.

Configurando ADFS

Lo que debemos configurar se llama “Issuance Authorization Rule” y lo encontramos entre las “Claims Rule” en la consola de ADFS. debemos crear una que emita un token de cuyo type debe ser: “http://schemas.microsoft.com/authorization/claims/permit” y, en caso de que tenga el acceso permitido, el valor de dicho claim debe ser “true”.

Ejemplo de Authorization Rule

autorizando-adfs-1   autorizando-adfs-2

Si vemos un caso donde se permita la entrada a todos los usuarios (el valor por defecto), veremos que se trata de una rule como la siguiente:

=> issue(Type = "http://schemas.microsoft.com/authorization/claims/permit", Value = "true");

mientras que si controlamos el acceso, vamos a tener algo mas parecido a:

c:[Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"]
=> issue(store = "AttributeStore", types = ("http://schemas.microsoft.com/authorization/claims/permit"), query = "EXEC sp_Is_Allow_Access {0}, {1}", param = c.Value, param = "https://testigomvc.neluz.int/");

en este caso estamos invocando al stored procedure “sp_Is_Allow_Access” mediante la conexión configurada en “AttributeStore” con el nombre del usuario autenticado (c.value) y con el identificador de la aplicación en cuestión: “https://testigomvc.neluz.int/” como parámetros. Si este stored procedure devuelve un registro con el valor “true” entonces se permitirá el acceso, en caso contrario se denegará.

De esta manera y sin cambiar nada en nuestra aplicación controlamos quien puede ingresar y quien no.

Seguir leyendo otros artículos de la serie

Login y Logout explícitos en un sitio web con ADFS

Este post es parte de una serie de artículos complementarios a la VAN sobre Identity Providers.

Escenario

Hay veces que podemos querer que manejar el login y logout del usuario independientemente de la página que esté navegando, es decir que una misma página se comporta de una manera si el usuario está autenticado y de otra cuando no lo está, pero la página es la misma.

El ejemplo que vamos a ver es en una aplicación MVC, por lo que a continuación veremos las acciones de Login y Logout, pero fácilmente se podría adaptar a otros escenarios con asp.net.

Actions

public void Logout()
{
    WSFederationAuthenticationModule authModule = FederatedAuthentication.WSFederationAuthenticationModule;
    string signoutUrl =
        (WSFederationAuthenticationModule.GetFederationPassiveSignOutUrl(authModule.Issuer, authModule.Realm, null));

    WSFederationAuthenticationModule.FederatedSignOut(new Uri(signoutUrl),
                                                      new Uri(authModule.Realm));
}

public ActionResult Login()
{
    WSFederationAuthenticationModule authModule = FederatedAuthentication.WSFederationAuthenticationModule;
    var signinUrl = authModule.CreateSignInRequest("passive", authModule.Realm, false);
    return Redirect(signinUrl.RequestUrl);
}

Breve explicación

Para hacer en forma explícita el login y logout tenemos que redireccionar a otro sitio web (con ciertos parámetros en la url), justamente al sitio de ADFS y una buena forma de determinar la url completa a la que redireccionar es usas los servicios que nos provee WIF para tal fin, estos servicios armarán la url según la configuración en la sección microsoft.IdentityModel.

Un detalle aquí es que debemos permitir el acceso a usuarios no autenticados a nuestro sitio, es decir, debemos modificar la configuración por defecto que crea Federation Utility respecto a:

<authorization>
    <allow users="*"/>
</authorization>

De esta forma, el login y logout del sitio no queda atado a las páginas sino a la intención manifiesta del usuario.

Seguir leyendo otros artículos de la serie

Sección pública y sección privada en un sitio web con autenticación mediante ADFS

Este post es parte de una serie de artículos complementarios a la VAN sobre Identity Providers.

Para manejar partes de un sitio web como una zona pública y otra para usuario autenticados es que podemos usar el tag location en el web.config, de la misma manera que se hace sin ADFS.

<location path="Styles">
    <system.web>
        <authorization>
            <allow users="*" />
        </authorization>
    </system.web>
</location>
<location path="Scripts">
    <system.web>
        <authorization>
            <allow users="*" />
        </authorization>
    </system.web>
</location>
<location path="Default.aspx">
    <system.web>
        <authorization>
            <allow users="*" />
        </authorization>
    </system.web>
</location>

mientras que el default sigue siendo:

<system.web>
    <
authorization>
        <
deny users="?" />
    </
authorization>
    <
authentication mode="None" />
</system.web>

Seguir leyendo otros artículos de la serie

Pasos para delegar la autenticación de mi sitio web a ADFS

Este post es parte de una serie de artículos complementarios a la VAN sobre Identity Providers.

El siguiente artículo trata de sitios web desarrollados con ASP.NET (WebForms, MVC, etc)

Pre-requisitos:

  • Disponer de una instancia de ADFS donde delegar la autenticación.
  • Tener instalado WIF SDK en el ambiente de desarrollo.

Pasos:

  1. Abrir nuestro proyecto web en Visual Studio.
  2. Sobre el proyecto, hacemos clic derecho y elegimos la opción: “Add STS reference” que tendremos disponible si hemos instalado WIF SDK. Esto nos abre la pantalla de Federation Utility donde debemos completar los datos de la aplicación:integrando-adfs-1_thumb[3]
  3. En el siguiente paso, elegimos la opción “Use an existing STS” e indicamos la url de la FederationMetadata de dicho ADFS.integrando-adfs-2_thumb[1]
  4. El resto de los valores los aceptamos por defecto.

Cambios realizados

Esto va a haber realizado varios cambios en nuestra aplicación, cambios que si no tenemos instalado WIF SDK los tendríamos que hacer manualmente. Ahora vamos a analizar dichos cambios.

  • Agregó un archivo FederationMetadata.xml con la información necesaria para agregar el Relying Party en ADFS con un simple Import y sin necesidad de configurarlo manualmente.
  • Agregó, en el web.config, varias líneas, y quizás modificó otras, principalmente:
    • agregó la sección: microsoft.identityModel con toda la configuración referente WIF.
    • denegó el ingreso para personas no autenticadas: <authorization><deny users="?" /></authorization>
    • configuró el modo de autenticación en none: <authentication mode="None" />
    • Agregó 2 HttpModules: WSFederationAuthenticationModule y SessionAuthenticationModule. Nota: si ejecutás en IIS 7 y tenés un error 500, probá de quitar los httpModules definidos en system.web.

Deploy, problemas comunes y posibles soluciones

Ahora estamos en condiciones de hacer el deploy de nuestra aplicación a un IIS, esto es necesario para manejar los certificados, puede ser un IIS local o en un servidor, en mi caso va a ser en el servidor ya que no tengo un IIS local en la máquina de desarrollo.

Si accedemos al sitio en este momento y no teníamos la misma configurada con https (SSL)probablemente tengamos el siguiente error: “ID1059: Cannot authenticate the user because the URL scheme is not https and requireSsl is set to true in the configuration, therefore the authentication cookie will not be sent. Change the URL scheme to https or set requireSsl to false on the cookieHandler element in configuration.”. Para esto, lo mejor va a ser utilizar https y así también subir el nivel de seguridad de nuestra aplicación por lo que, en nuestro ambiente de desarrollo, creamos un certificado firmado por nosotros mismos y configuramos el Binding del IIS en https con el certificado que acabamos de crear.

Si ahora accedemos al sitio, probablemente nos pida autenticarnos y luego nos de un error, esto se debe a que aun no configuramos la aplicación en ADFS. Esto lo podemos confirmar abriendo el EventViewer del servidor donde está instalado ADFS y, en la rama de ADFS/Admin vamos a encontrar un error que dice algo de: “MSIS7007: The requested relying party trust 'https://aplicacionweb.neluz.int/' is unspecified or unsupported”. Para agregar la aplicación a ADFS abrimos la consola de administración de ADFS y en “Trust Relationships” / “Relying Party Trust” vamos a “Add Relying Party Trust…” y seleccionamos la FederationMetadata.xml que habíamos creado con “Federation Utility”

integrando-adfs-4_thumb[2]

Luego de completar el wizard vamos a “Edit Claims Rules” donde creamos, al menos una regla como la siguiente:

integrando-adfs-5_thumb[2] integrando-adfs-6_thumb[2]

Guardamos y volvemos a ingresar a nuestro sitio.

Ahora podemos obtener un mensaje como: “A potentially dangerous Request.Form value was detected from the client (wresult="<t:RequestSecurityTo...").”. Si esto ocurre debemos agregar en nuestro web.config el siguiente tag “<httpRuntime requestValidationMode="2.0"/>” dentro de “<system.web>”.

Ahora si nuestra aplicación debería autenticar con ADFS.

En mi caso, la aplicación es muy simple y, en Default.aspx tiene una línea que dice:

<%= HttpContext.Current.Request.IsAuthenticated ? HttpContext.Current.User.Identity.Name : "El usuario no está autenticado" %>

por lo que al ingresar al sitio primero me pide que me autentique y luego se ve como la siguiente imagen:

integrando-adfs-7_thumb[1]

Seguir leyendo otros artículos de la serie

Identity Providers

El Sábado 18 de junio del 2011, junto a @carlospeix, estuvimos como presentadores en una VAN sobre delegación de Autenticación y Autorización en aplicaciones web en la comunidad altnet hispano.

En la primer parte, Carlos habló sobre autenticación con Identity Providers públicos como twitter, facebook y OpenID y sobre el acceso a recursos en dichos servicios como la lista de contactos de GMail. También revisó el protocolo OAuth 1.0a.

En la segunda parte yo mostré ejemplos de otro escenario, el de Identity Providers privados o empresariales como una suerte de continuación de los temas tratados en la VAN sobre ADFS y WIF que presentaron Eugenio Pace y Carlos Peix el 18 de Septiembre del 2010.

En la presentación nos quedamos cortos de tiempo, por lo que escribí esta serie de artículos como complemento, con temas que quedaron fuera de la presentación o que pasamos muy rápido en la misma. Si no tienen conocimientos de ADFS es recomendable que vean las grabaciones de dichos eventos como base para esta serie.

Artículos de la serie:

Otros recursos online

Adfs content map: http://social.technet.microsoft.com/wiki/contents/articles/2735.aspx

Starter STS: http://startersts.codeplex.com/ este es el IP-STS usado en la presentación y en estos ejemplos, contiene una serie de muy buenos videos de como implementarlo.

WIF: www.microsoft.com/wif y http://msdn.microsoft.com/en-us/security/aa570351

WIF - White paper for developers: http://download.microsoft.com/download/7/D/0/7D0B5166-6A8A-418A-ADDD-95EE9B046994/WindowsIdentityFoundationWhitepaperForDevelopers-RTW.pdf

Claims–based Identity and Access Control: http://msdn.microsoft.com/en-us/library/ff423674.aspx

Implementando OAuth en:
Google: http://code.google.com/intl/es-419/apis/accounts/docs/OAuth2.html
Facebook: http://developers.facebook.com/docs/authentication/
Twitter: http://dev.twitter.com/
LinkedIn: http://developer.linkedin.com/docs/DOC-1008

OpenID Foundation website: http://openid.net/
OAuth Community Site: http://oauth.net/ y http://hueniverse.com/oauth/

Implementaciones de OAuth: http://www.dotnetopenauth.net/ y http://code.google.com/p/socialauth-net/

Retrospectiva de mi primer año (y unos meses) como desarrollar freelance)

Antes de comenzar a enumerar aspectos que me gustaría mantener y aspectos que quisiera mejorar, quiero destacar que este año fue muy positivo profesionalmente para mi. Aunque también debo admitir que varias veces sentí el vértigo de ir muy rápido, de estar aprendiendo en el momento de estar aplicando el conocimiento.

Para mantener

  • Involucrarme en la comunidad alt.net hispano.
  • Trabajar con @carlospeix (http://carlospeix.com).
  • Incremento de temas aprendidos.
  • Poder dedicarle el tiempo a eventos de formación profesional.
  • Disponer de tiempo libre para otras actividades no relacionadas con lo laboral.
  • Cumplir con los plazos propuestos.
  • Aprender de casi todos los proyecto en los que participé.
  • Haber logrado no disminuir los ingresos de cuando trabajaba en relación de dependencia (con una gran ganancia de tiempo).
  • Pagar todos mis impuestos y tributos.
  • Virtualizar entornos de trabajo (con Linux como SO host).
  • Mantener los socios/clientes con los que he trabajado.

Para mejorar

  • Continuidad en las publicaciones en el blog.
  • Aumentar la interacción con los integrantes de los equipos de trabajo que participo.
  • Disminuir mi tendencia a abarcar demasiados roles en el proceso de desarrollo, lo que pienso que baja la calidad del producto.
  • No sentirme un nabo por pagar todos mis impuestos y tributos.
  • Imagen sobredimencionada acerca de mi en algunos colegas.
  • Manejo comercial de algunas situaciones que así lo requieren, no solo ver la parte técnica.
  • Mejorar el manejo del tiempo que dedico a la investigación.

 

Vamos por otro año!

Domain sin Ids (pero con ConfOrm)

Preparando un ejemplo para una nueva VAN de alt.net hispano en la que, al igual que la VAN anterior, vamos a usar ConfOrm me encontré con algo curioso, ¡¿ mi objectos de dominio no tienen Ids pero mis tablas si ?!

Siempre hablamos de que los Id de los objetos (PK a nivel de tablas) no deben ser datos significativos para el usuario por varias razones que no vamos a discutir ahora. Siempre decimos que los Id son para el sistema pero hoy me encontré habiendo generado un Domain sin Id, podría decirse: “los Id son para la base de datos”.

Las clases del Domain son las siguientes:

public class Cliente
{
  private readonly IList<Extraccion> _extracciones;
 
  public Cliente()
  {
    _extracciones = new List<Extraccion>();
  }
 
  public virtual string Nombre { get; set; }
  public virtual Cuenta Cuenta { get; set; }
 
  public virtual IEnumerable<Extraccion> Extracciones
  {
    get { return _extracciones; }
  }
 
  public virtual bool RetirarPlata(int importe)
  {
    if (Cuenta != null && Cuenta.Saldo > importe)
    {
      Cuenta.Saldo -= importe;
      Extraccion extraccion = new Extraccion { Fecha = DateTime.Now, Importe = importe };
      _extracciones.Add(extraccion);
      extraccion.Cliente = this;
      return true;
    }
    return false;
  }
}
 
public class Extraccion
{
  public virtual Cliente Cliente { get; set; }
  public virtual DateTime Fecha { get; set; }
  public virtual int Importe { get; set; }
}
 
public class Cuenta
{
  public virtual int Saldo { get; set; }
  public virtual int Version { get; set; }
}

Los Mappings los generamos con ConfOrm:

ObjectRelationalMapper orm = new ObjectRelationalMapper();
 
orm.TablePerClass<Cliente>();
orm.TablePerClass<Cuenta>();
orm.TablePerClass<Extraccion>();
orm.Cascade<Cliente, Cuenta>(Cascade.All);
orm.VersionProperty<Cuenta>(c => c.Version);
 
var mapper = new Mapper(orm);
mapper.Customize<Cliente>(c => c.Property(p => p.Nombre, m => m.Unique(true)));
mapper.Customize<Cliente>(c => c.Property(p => p.Nombre, m => m.NotNullable(true)));
 
HbmMapping mapping = mapper.CompileMappingFor(typeof(Cliente).Assembly.GetTypes());
return mapping;

Si queremos escribir los mappings como xml a partir de la información generada por ConfOrm podemos, el código sería algo como:

var setting = new XmlWriterSettings { Indent = true };
var serializer = new XmlSerializer(typeof(HbmMapping));
 
var fileInfo = new FileInfo("../../mappings.hbm.xml");
 
using (TextWriter sw = new StreamWriter(fileInfo.FullName))
{
  var xw = XmlWriter.Create(sw, setting);
  serializer.Serialize(xw, mapping);
}

y el resultado:

<?xml version="1.0" encoding="utf-8"?>
<hibernate-mapping xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" assembly="EjemploObtenerXmlDesdeConfOrm" xmlns="urn:nhibernate-mapping-2.2">
  <class name="EjemploObtenerXmlDesdeConfOrm.Domain.Cliente">
    <id type="Guid">
      <generator class="guid.comb" />
    </id>
    <property name="Nombre" unique="true" />
    <many-to-one name="Cuenta" cascade="all" />
    <bag name="Extracciones" access="field.camelcase-underscore" inverse="true" cascade="all,delete-orphan">
      <key column="Cliente" on-delete="cascade" />
      <one-to-many class="EjemploObtenerXmlDesdeConfOrm.Domain.Extraccion" />
    </bag>
  </class>
  <class name="EjemploObtenerXmlDesdeConfOrm.Domain.Extraccion">
    <id type="Guid">
      <generator class="guid.comb" />
    </id>
    <many-to-one name="Cliente" />
    <property name="Fecha" />
    <property name="Importe" />
  </class>
  <class name="EjemploObtenerXmlDesdeConfOrm.Domain.Cuenta">
    <id type="Guid">
      <generator class="guid.comb" />
    </id>
    <version name="Version" />
    <property name="Saldo" />
  </class>
</hibernate-mapping>

Recién en los mappings aparecen los Ids (pero sin tener propiedad en el objeto), quizás para muchos es algo de su día a día, pero en mi caso fue la primera vez que me doy cuenta de esto.

El DER de la base de datos, generada con SchemaExport, es:

DER

Gracias ConfOrm por seguirme enseñando sobre mappings, ORMs y OOP en general.

Con este blog-post no intento decir que es una buena práctica, seguramente que buscar nuestros objetos en la db por valores numéricos es mucho mas performante que por los datos significativos para el usuario como puede ser el nombre en este caso, pero si me hace reflexionar sobre el uso y abuso de los Ids.