Implémenter une sécurité à base de rôles avec Windows Authentication & SQL Server

Publié par Fabrice Michellonet sous le(s) label(s) le 14 octobre 2011

Ces derniers jours j'ai été confronté à une problématique très intéressante; j'aimerais la partager avec vous aujourd'hui et vous soumettre la solution que j'ai imaginé pour y répondre.

Mon client souhaitait restreindre l'accès à son site/application intranet aux seuls membres d'un groupe Windows prédéfini.
Le hic est que le site fonctionne avec des rôles définis en base de données, laissant ainsi libre champs aux admins de l'appli d'attribuer et révoquer des droits aux autres utilisateurs. Évidement, il était hors de question de devoir passer par l'IT pour changer les droits des utilisateurs à l'avenir.

Si l'on transpose la demande en termes techniques, on a besoin du WindowsTokenRoleProvider pour autoriser l'accès au site et pour tout le reste il faut se reposer sur un SqlRoleProvider.
Comme vous le savez, ce genre de configuration n'est pas prise en charge par le modèle de provider tel que nous le connaissons jusqu'à présent.

En cherchant sur la toile si ce genre de situation à déjà été traitée (ne réinventons pas la roue carrée), vous tomberez très certainement sur un article de Scott Guthrie qui traite d'un problème très similaire. Dans l'article l'homme à la chemise rouge nous montre comment, sans code, utiliser les rôles issus de la base de données et n'autoriser l'accès au site qu'aux utilisateurs identifiés. Et c'est bien sur ce dernier point que nos problématiques diffèrent car dans mon cas, il ne suffit pas d'être identifié sur le domaine, mais il faut aussi faire partie d'un groupe d'utilisateurs.

J'ai donc remonté mes manches, et quitte à écrire du code j'ai tenté de trouver une solution générique permettant de combiner des RoleProvider.

Commençons par définir un RoleProviderDecorator qui comme son nom l'indique suit le pattern Decorator.
public class RoleProviderDecorator<TSurrogateRoleProvider> : RoleProvider
        where TSurrogateRoleProvider : RoleProvider, new()
    {

        protected TSurrogateRoleProvider _surrogate;

        public override void Initialize(string name, NameValueCollection config)
        {
            if (config == null)
                throw new ArgumentNullException("config");

            base.Initialize(name, config);

            _surrogate = new TSurrogateRoleProvider();

            InitializeInnerProvider(_surrogate, name, config);
        }

        protected virtual void InitializeInnerProvider(RoleProvider innerProvider, string name, NameValueCollection config)
        {
            innerProvider.Initialize(name, config);
        }
        
        public override bool IsUserInRole(string username, string roleName)
        {
            return _surrogate.IsUserInRole(username, roleName);
        }
        
        public override string[] GetRolesForUser(string username)
        {
            return _surrogate.GetRolesForUser(username).ToArray();
        }
        
        public override void CreateRole(string roleName)
        {
            _surrogate.CreateRole(roleName);
        }
        
        public override bool DeleteRole(string roleName, bool throwOnPopulatedRole)
        {
            return _surrogate.DeleteRole(roleName, throwOnPopulatedRole);
        }
        
        public override bool RoleExists(string roleName)
        {
            return _surrogate.RoleExists(roleName);
        }
        
        public override void AddUsersToRoles(string[] usernames, string[] roleNames)
        {
            _surrogate.AddUsersToRoles(usernames, roleNames);
        }
        
        public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames)
        {
            _surrogate.RemoveUsersFromRoles(usernames, roleNames);
        }
        
        public override string[] GetUsersInRole(string roleName)
        {
            return _surrogate.GetUsersInRole(roleName).ToArray();
        }
        
        public override string[] GetAllRoles()
        {
            return _surrogate.GetAllRoles().ToArray();
        }
        
        public override string[] FindUsersInRole(string roleName, string usernameToMatch)
        {
            return
                _surrogate.FindUsersInRole(roleName, usernameToMatch).ToArray();
        }
        
        public override string ApplicationName
        {
            get { return _surrogate.ApplicationName; }
            set { _surrogate.ApplicationName = value; }
        }
    }

L'idée du décorateur, vous l'aurez compris, est de déléguer l’exécution des méthodes au vrai RoleProvider passé sous forme de Generic.

Maintenant définissons un RoleProvider qui se charge d'utiliser de manière sous-jacente deux RoleProvider :

public abstract class UnionRoleProvider<TPrimaryRoleProvider, TSecondaryRoleProvider> : RoleProviderDecorator<TPrimaryRoleProvider>
        where TPrimaryRoleProvider : RoleProvider, new()
        where TSecondaryRoleProvider : RoleProvider, new()
    {
        private TSecondaryRoleProvider _secondSurrogate;

        public override void Initialize(string name, NameValueCollection config)
        {
            if (config == null)
                throw new ArgumentNullException("config");

            base.Initialize(name, config);

            _secondSurrogate = new TSecondaryRoleProvider();
            InitializeInnerProvider(_secondSurrogate, name, config);
        }

        public override bool IsUserInRole(string username, string roleName)
        {
            return _surrogate.IsUserInRole(username, roleName) || _secondSurrogate.IsUserInRole(username, roleName);
        }

        public override string[] GetRolesForUser(string username)
        {
            return _surrogate.GetRolesForUser(username).Union(_secondSurrogate.GetRolesForUser(username)).ToArray();
        }

        public override void CreateRole(string roleName)
        {
            _surrogate.CreateRole(roleName);
            _secondSurrogate.CreateRole(roleName);
        }

        public override bool DeleteRole(string roleName, bool throwOnPopulatedRole)
        {
            return _surrogate.DeleteRole(roleName, throwOnPopulatedRole) && _surrogate.DeleteRole(roleName, throwOnPopulatedRole);
        }

        public override bool RoleExists(string roleName)
        {
            return _surrogate.RoleExists(roleName) || _secondSurrogate.RoleExists(roleName);
        }

        public override void AddUsersToRoles(string[] usernames, string[] roleNames)
        {
            _surrogate.AddUsersToRoles(usernames, roleNames);
            _secondSurrogate.AddUsersToRoles(usernames, roleNames);
        }

        public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames)
        {
            _surrogate.RemoveUsersFromRoles(usernames, roleNames);
            _secondSurrogate.RemoveUsersFromRoles(usernames, roleNames);
        }

        public override string[] GetUsersInRole(string roleName)
        {
            return _surrogate.GetUsersInRole(roleName).Union(_secondSurrogate.GetUsersInRole(roleName)).ToArray();
        }

        public override string[] GetAllRoles()
        {
            return _surrogate.GetAllRoles().Union(_secondSurrogate.GetAllRoles()).ToArray();
        }

        public override string[] FindUsersInRole(string roleName, string usernameToMatch)
        {
            return
                _surrogate.FindUsersInRole(roleName, usernameToMatch)
                    .Union(_secondSurrogate.FindUsersInRole(roleName, usernameToMatch))
                    .ToArray();
        }
    }

Dans mon cas, seul un des deux RoleProvider doit pouvoir écrire dans son médium de stockage.
L'autre doit être en lecture seule. Pour ce faire, j'introduis un ReadOnlyRoleProvider :

public class ReadOnlyRoleProvider<TSurrogateRoleProvider> : RoleProviderDecorator<TSurrogateRoleProvider>
        where TSurrogateRoleProvider : RoleProvider, new()
    {

        public override void CreateRole(string roleName){}

        public override bool DeleteRole(string roleName, bool throwOnPopulatedRole)
        {
            return true;
        }

        public override void AddUsersToRoles(string[] usernames, string[] roleNames){}

        public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames){}

Ça y est on a fait 90% du boulot.

Dans le fichier web.config on veut écrire ceci :

<roleManager enabled="true" defaultProvider="MyRoleProvider">
      <providers>
        <clear/>
        <add name="MyRoleProvider " type="Providers.MyRoleProvider " connectionStringName="cnx" applicationName="MyApp" />
      </providers>
    </roleManager>

Étant donné que l'on ne peut pas utiliser de classe générique dans la config, on va simplement en spécialiser une :

public class MyRoleProvider : UnionRoleProvider<SqlRoleProvider, SilentWindowsTokenRoleProvider<WindowsTokenRoleProvider>>
    {

        protected override void InitializeInnerProvider(RoleProvider innerProvider, string name, NameValueCollection config)
        {
            NameValueCollection cfg = config;
            if (innerProvider is ReadOnlyRoleProvider<WindowsTokenRoleProvider>)
            {
                cfg = new NameValueCollection(config);
                cfg.Remove("connectionStringName");
            }
            
            base.InitializeInnerProvider(innerProvider, name, cfg);
        }
    }

Ouf, 99% du boulot. Vous pouvez maintenant exécuter et ça va fonctionner... jusqu'à ce que vous utilisiez une des méthodes suivantes :
  • GetUsersInRole
  • GetAllRoles
  • FindUsersInRole

Tout simplement parce que le WindowsTokenRoleProvider ne supporte pas ces opérations.
Ok, voici le tout dernier % à réaliser :

public class SilentWindowsTokenRoleProvider<TSurrogateRoleProvider> : ReadOnlyRoleProvider<TSurrogateRoleProvider>
        where TSurrogateRoleProvider : RoleProvider, new()
    {

        public override string[] GetUsersInRole(string roleName)
        {
            return new string[]{};
        }

        public override string[] GetAllRoles()
        {
            return new string[]{};
        }

        public override string[] FindUsersInRole(string roleName, string usernameToMatch)
        {
            return new string[]{};
        }
    }

Voila 100% de la solution; beaucoup de code aujourd'hui, j'espère que ça ne vous a pas trop démotivé.

Si vous avez une solution autre, ou contestez mon approche n'hésitez pas à laisser un commentaire.

Entity Framework et AOP

Publié par Fabrice Michellonet sous le(s) label(s) , , , le 7 octobre 2011

Cela fait des mois que je repousse la publication d'un post sur l'AOP, car je ne voulais pas vous resservir le sempiternel exemple de mise en place d'une gestion de log applicatif simplifiée.
Si vous voulez vous rafraichir les idées sur le sujet je vous conseille de jeter un œil sur l'article d'Ayende Rahien sur le sujet

Avec le framework .NET, il existe au moins 6 façons différentes d'ajouter un brin d'AOP dans vos programmes;
Pour mémoire il s'agit de :
  • Remoting Proxies
  • Dériver votre classe de ContextBoundObject
  • Passer par un dynamique proxy ( ex : Castle Dynamic Proxy)
  • Utiliser l'API de profiling de .NET
  • Injection d'IL après compilation
  • Injection d'IL au runtime

Dans cet article on s'intéressera particulièrement a l'injection post compilation, tout simplement car c'est la plus performante (le code lié à l'aspect est directement inscrit dans l'assembly finale et rien ne le distingue du reste du code) et aussi car c'est la façon la plus sexy a mon gout de faire de l'AOP (c'est une raison comme une autre, non?).

Bon revenons à nos moutons.

Sur presque tous les projets sur lesquels je suis intervenu ces dernières années, lorsqu'on modélise les entités qui devront être persistées en base, on leur adjoint au moins quatre propriétés :
  • Created By (string)
  • Created (datetime)
  • Last Updated By (string)
  • Last Updated (datetime)

J'ai pour habitude de nommer cette construction, une entité "Auditable", ce qui se traduit en code par l'interface suivante :
public interface IAuditable
    {
        string CreatedBy { get; set; }
        DateTime Created { get; set; }
        string UpdatedBy { get; set; }
        DateTime? Updated { get; set; }
    }

Vous l'aurez compris, l'idée ici, est de stocker la date et l'utilisateur ayant créé ou modifié l'entité en question et ceci a chaque accès base.
Je vous laisse imaginer le travail rébarbatif que cela peut vite devenir si l'on doit tout gérer à la main et si notre modèle est composé de dizaines voire de centaines d'entités.

Je vous propose donc une idée afin de se faciliter la vie grâce à l'AOP.
Il est à noter que bien que l'exemple ci-dessous s’appuie sur Entity Framework, le mécanisme est très certainement transposable (avec adaptation) aux autres ORM.

On commence par utiliser Entity Framework Code First, et on ajoute notre framework AOP préféré
Install-Package EntityFramework
Install-Package Afterthought

En attendant qu'un de mes patch soit accepté et intégré à Afterthought, il vous faudra remplacer la dll d'Afterthought par la mienne disponible ici.

Nuget a rajouté quelques dll et références dans votre projet et modifié également le post build event de votre projet;
Désormais à chaque compilation, Afterthought scannera les assemblies à la recherche de taches d'injection d'IL à effectuer.

Voyons comment demander à Afterthought d'injecter l'interface IAuditable sur nos entités.

public class AuditableAmender<T> : Amendment<T, T>
    {

        public AuditableAmender()
        {
            Properties.Add<string>("CreatedBy");
            Properties.Add<DateTime>("Created");
            Properties.Add<string>("UpdatedBy");
            Properties.Add<DateTime?>("Updated");

            Implement<IAuditable>();
        }
    }

Le code me semble assez clair sans avoir a revenir longuement dessus; On demande simplement à Afterthought d'injecter les propriétés nécessaires à l'implémentation de l'interface IAuditable.

Bien créons un attribut de marquage, que nous placerons sur nos entités :

[AttributeUsage(AttributeTargets.Class)]
    public class AuditableAttribute : Attribute { }

il nous reste encore deux tâches à réaliser;
  • Préciser les assemblies à introspecter
  • Permettre à Afterthought de découvrir les classes qui doivent être modifiées et surtout comment.
Cela se fait dans une même classe :

[AttributeUsage(AttributeTargets.Assembly)]
    public class AmendAttribute : Attribute, IAmendmentAttribute
    {
        IEnumerable<ITypeAmendment> IAmendmentAttribute.GetAmendments(Type target)
        {
            if (target.GetCustomAttributes(typeof(AuditableAttribute), true).Length > 0)
            {
                ConstructorInfo constructorInfo = typeof(AuditableAmender<>).MakeGenericType(target).GetConstructor(Type.EmptyTypes);
                if (constructorInfo != null)
                    yield return (ITypeAmendment)constructorInfo.Invoke(new object[0]);
            }
        }
    }
En clair, pour chaque classe qui implémente IAuditable, on va faire appel à la classe AuditableAmender (créée précédemment) pour modifier la classe.
En appliquant cet attribut sur l'assembly qui contient vos entités, Afterthought effectuera son travail d'injection.

Désormais si l'on applique l'attribut Auditable sur une de nos entités comme suit :
[Auditable]
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

    public class Context : DbContext
    {
        public virtual DbSet<Product> Products { get; set; }

        public Context()
        {
            Database.SetInitializer(new DropCreateDatabaseIfModelChanges<Context>());
        }
    }

Ce qui produit bien en base la table suivante :

table

et dans l'assembly finale (vu avec Reflector)

assembly

Évidemment, en généralisant ce principe il est possible de facilement faire évoluer votre modèle sans avoir à travailler parfois de manière répétitive.

Vous retrouverez cette fonctionnalité (il vous suffit de marquer votre entité avec l'attribut) dans la librairie EntityFramework.Patterns que je maintiens et disponible via nuget.

Que pensez-vous de cette technique?
Utilisez-vous un autre framework (Postsharp) pour réaliser ce genre de tâche?