Grupo de usuarios y gestión de roles en .NET con Active Directory

Actualmente estoy investigando métodos para almacenar roles de usuario y permisos para proyectos basados ​​en .NET. Algunos de estos proyectos están basados ​​en la web, otros no. Actualmente estoy luchando por encontrar el mejor método para lograr lo que estoy buscando de una manera consistente y portátil en todos los tipos de proyectos.

En donde estoy, buscamos aprovechar Active Directory como nuestro único punto de contacto para información básica del usuario. Debido a esto, no tenemos que mantener una base de datos personalizada para los usuarios de cada aplicación, ya que ya están almacenados en Active Directory y se mantienen activamente allí. Además, no queremos escribir nuestro propio modelo / código de seguridad si es posible y nos gustaría usar algo preexistente, como los bloques de aplicaciones de seguridad provistos por Microsoft.

Algunos proyectos requieren solo privilegios básicos, como leer, escribir o no tener acceso. Otros proyectos requieren permisos más complejos. Los usuarios de esas aplicaciones pueden tener acceso a algunas áreas, pero no a otras, y sus permisos pueden cambiar en cada área. Una sección de administración de la aplicación controlaría y definiría este acceso, no las herramientas de AD.

Actualmente, utilizamos la Autenticación de Windows integrada para realizar la autenticación en nuestra intranet. Esto funciona bien para encontrar información básica del usuario, y he visto que ASP.NET se puede ampliar para proporcionar un proveedor de roles de Active Directory, de modo que pueda encontrar cualquier grupo de seguridad al que pertenezca un usuario. Pero, lo que parece ser la caída de este método para mí es que todo está almacenado en Active Directory, lo que podría generar un desastre si las cosas crecen demasiado.

En esta misma línea, también he oído hablar de los servicios de directorio ligero de Active Directory, que parece que podría ampliar nuestro esquema y agregar solo atributos y grupos específicos de la aplicación. El problema es que no puedo encontrar nada sobre cómo se haría esto o cómo funciona esto. Hay artículos de MSDN que describen cómo hablar con esta instancia y cómo crear una nueva instancia, pero nada parece responder mi pregunta.

Mi pregunta es: de acuerdo con su experiencia, ¿estoy yendo por el camino correcto? ¿Es posible hacer lo que estoy buscando con solo Active Directory, o tienen que usarse otras herramientas?


Otros métodos que he estudiado:

  • Uso de múltiples archivos web.config [ stackoverflow ]
  • Crear un modelo de seguridad personalizado y una base de datos para administrar usuarios en todas las aplicaciones

Usar AD para su autenticación es una gran idea, ya que debe agregar a todos de todos modos, y para los usuarios de la intranet no es necesario un inicio de sesión adicional.

Tiene razón en que ASP.NET le permite usar un Proveedor que le permitirá autenticarse contra AD, aunque no hay nada incluido para brindarle soporte de membresía grupal (aunque es bastante trivial implementarlo si lo desea, puedo brindarle una muestra )

El verdadero problema aquí es si desea usar grupos de AD para definir permisos dentro de cada aplicación, ¿sí?

Si es así, entonces tiene la opción de crear su propio RoleProvider para ASP.NET que también puede ser utilizado por WinForms y aplicaciones WPF a través de ApplicationServices. Este RoleProvider podría vincular el ID del usuario en AD a grupos / roles por aplicación que puede almacenar en su propia base de datos personalizada, lo que también permite que cada aplicación permita la administración de estos roles sin requerir que estos administradores tengan privilegios adicionales en AD.

Si lo desea, también puede tener una anulación y combinar funciones de aplicaciones con grupos de AD, por lo que si están en algún grupo “Administrador” global en AD obtienen un permiso completo en la aplicación, independientemente de la membresía de la función de la aplicación. Por el contrario, si tienen un grupo o propiedad en AD para decir que han sido despedidos, puedes ignorar toda la membresía de la función App y restringir todo el acceso (ya que probablemente HR no los elimine de todas y cada una de las aplicaciones, suponiendo que las conozcan ¡todas!).

Código de muestra agregado según lo solicitado:

NOTA: basado en este trabajo original http://www.codeproject.com/Articles/28546/Active-Directory-Roles-Provider

Para su ActiveDirectoryMembershipProvider solo necesita implementar el método ValidateUser, aunque podría implementar más si lo desea, el nuevo espacio de nombres de AccountManagement lo hace trivial:

// assumes: using System.DirectoryServices.AccountManagement; public override bool ValidateUser( string username, string password ) { bool result = false; try { using( var context = new PrincipalContext( ContextType.Domain, "yourDomainName" ) ) { result = context.ValidateCredentials( username, password ); } } catch( Exception ex ) { // TODO: log exception } return result; } 

Para su proveedor de roles es un poco más de trabajo, hay algunos problemas clave que descubrimos durante la búsqueda en google, como los grupos que desea excluir, los usuarios que desea excluir, etc.

Probablemente valga la pena una publicación completa en el blog, pero esto debería ayudarlo a comenzar, es búsquedas en caché en variables de Sesión, solo como una muestra de cómo podría mejorar el rendimiento (ya que una muestra de Caché completa sería demasiado larga).

 using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Configuration.Provider; using System.Diagnostics; using System.DirectoryServices; using System.DirectoryServices.AccountManagement; using System.Linq; using System.Web; using System.Web.Hosting; using System.Web.Security; namespace MyApp.Security { public sealed class ActiveDirectoryRoleProvider : RoleProvider { private const string AD_FILTER = "(&(objectCategory=group)(|(groupType=-2147483646)(groupType=-2147483644)(groupType=-2147483640)))"; private const string AD_FIELD = "samAccountName"; private string _activeDirectoryConnectionString; private string _domain; // Retrieve Group Mode // "Additive" indicates that only the groups specified in groupsToUse will be used // "Subtractive" indicates that all Active Directory groups will be used except those specified in groupsToIgnore // "Additive" is somewhat more secure, but requires more maintenance when groups change private bool _isAdditiveGroupMode; private List _groupsToUse; private List _groupsToIgnore; private List _usersToIgnore; #region Ignore Lists // IMPORTANT - DEFAULT LIST OF ACTIVE DIRECTORY USERS TO "IGNORE" // DO NOT REMOVE ANY OF THESE UNLESS YOU FULLY UNDERSTAND THE SECURITY IMPLICATIONS // VERYIFY THAT ALL CRITICAL USERS ARE IGNORED DURING TESTING private String[] _DefaultUsersToIgnore = new String[] { "Administrator", "TsInternetUser", "Guest", "krbtgt", "Replicate", "SERVICE", "SMSService" }; // IMPORTANT - DEFAULT LIST OF ACTIVE DIRECTORY DOMAIN GROUPS TO "IGNORE" // PREVENTS ENUMERATION OF CRITICAL DOMAIN GROUP MEMBERSHIP // DO NOT REMOVE ANY OF THESE UNLESS YOU FULLY UNDERSTAND THE SECURITY IMPLICATIONS // VERIFY THAT ALL CRITICAL GROUPS ARE IGNORED DURING TESTING BY CALLING GetAllRoles MANUALLY private String[] _defaultGroupsToIgnore = new String[] { "Domain Guests", "Domain Computers", "Group Policy Creator Owners", "Guests", "Users", "Domain Users", "Pre-Windows 2000 Compatible Access", "Exchange Domain Servers", "Schema Admins", "Enterprise Admins", "Domain Admins", "Cert Publishers", "Backup Operators", "Account Operators", "Server Operators", "Print Operators", "Replicator", "Domain Controllers", "WINS Users", "DnsAdmins", "DnsUpdateProxy", "DHCP Users", "DHCP Administrators", "Exchange Services", "Exchange Enterprise Servers", "Remote Desktop Users", "Network Configuration Operators", "Incoming Forest Trust Builders", "Performance Monitor Users", "Performance Log Users", "Windows Authorization Access Group", "Terminal Server License Servers", "Distributed COM Users", "Administrators", "Everybody", "RAS and IAS Servers", "MTS Trusted Impersonators", "MTS Impersonators", "Everyone", "LOCAL", "Authenticated Users" }; #endregion ///  /// Initializes a new instance of the ADRoleProvider class. ///  public ActiveDirectoryRoleProvider() { _groupsToUse = new List(); _groupsToIgnore = new List(); _usersToIgnore = new List(); } public override String ApplicationName { get; set; } ///  /// Initialize ADRoleProvider with config values ///  ///  ///  public override void Initialize( String name, NameValueCollection config ) { if ( config == null ) throw new ArgumentNullException( "config" ); if ( String.IsNullOrEmpty( name ) ) name = "ADRoleProvider"; if ( String.IsNullOrEmpty( config[ "description" ] ) ) { config.Remove( "description" ); config.Add( "description", "Active Directory Role Provider" ); } // Initialize the abstract base class. base.Initialize( name, config ); _domain = ReadConfig( config, "domain" ); _isAdditiveGroupMode = ( ReadConfig( config, "groupMode" ) == "Additive" ); _activeDirectoryConnectionString = ReadConfig( config, "connectionString" ); DetermineApplicationName( config ); PopulateLists( config ); } private string ReadConfig( NameValueCollection config, string key ) { if ( config.AllKeys.Any( k => k == key ) ) return config[ key ]; throw new ProviderException( "Configuration value required for key: " + key ); } private void DetermineApplicationName( NameValueCollection config ) { // Retrieve Application Name ApplicationName = config[ "applicationName" ]; if ( String.IsNullOrEmpty( ApplicationName ) ) { try { string app = HostingEnvironment.ApplicationVirtualPath ?? Process.GetCurrentProcess().MainModule.ModuleName.Split( '.' ).FirstOrDefault(); ApplicationName = app != "" ? app : "/"; } catch { ApplicationName = "/"; } } if ( ApplicationName.Length > 256 ) throw new ProviderException( "The application name is too long." ); } private void PopulateLists( NameValueCollection config ) { // If Additive group mode, populate GroupsToUse with specified AD groups if ( _isAdditiveGroupMode && !String.IsNullOrEmpty( config[ "groupsToUse" ] ) ) _groupsToUse.AddRange( config[ "groupsToUse" ].Split( ',' ).Select( group => group.Trim() ) ); // Populate GroupsToIgnore List with AD groups that should be ignored for roles purposes _groupsToIgnore.AddRange( _defaultGroupsToIgnore.Select( group => group.Trim() ) ); _groupsToIgnore.AddRange( ( config[ "groupsToIgnore" ] ?? "" ).Split( ',' ).Select( group => group.Trim() ) ); // Populate UsersToIgnore ArrayList with AD users that should be ignored for roles purposes string usersToIgnore = config[ "usersToIgnore" ] ?? ""; _usersToIgnore.AddRange( _DefaultUsersToIgnore .Select( value => value.Trim() ) .Union( usersToIgnore .Split( new[] { "," }, StringSplitOptions.RemoveEmptyEntries ) .Select( value => value.Trim() ) ) ); } private void RecurseGroup( PrincipalContext context, string group, List groups ) { var principal = GroupPrincipal.FindByIdentity( context, IdentityType.SamAccountName, group ); if ( principal == null ) return; List res = principal .GetGroups() .ToList() .Select( grp => grp.Name ) .ToList(); groups.AddRange( res.Except( groups ) ); foreach ( var item in res ) RecurseGroup( context, item, groups ); } ///  /// Retrieve listing of all roles to which a specified user belongs. ///  ///  /// String array of roles public override string[] GetRolesForUser( string username ) { string sessionKey = "groupsForUser:" + username; if ( HttpContext.Current != null && HttpContext.Current.Session != null && HttpContext.Current.Session[ sessionKey ] != null ) return ( (List) ( HttpContext.Current.Session[ sessionKey ] ) ).ToArray(); using ( PrincipalContext context = new PrincipalContext( ContextType.Domain, _domain ) ) { try { // add the users groups to the result var groupList = UserPrincipal .FindByIdentity( context, IdentityType.SamAccountName, username ) .GetGroups() .Select( group => group.Name ) .ToList(); // add each groups sub groups into the groupList foreach ( var group in new List( groupList ) ) RecurseGroup( context, group, groupList ); groupList = groupList.Except( _groupsToIgnore ).ToList(); if ( _isAdditiveGroupMode ) groupList = groupList.Join( _groupsToUse, r => r, g => g, ( r, g ) => r ).ToList(); if ( HttpContext.Current != null ) HttpContext.Current.Session[ sessionKey ] = groupList; return groupList.ToArray(); } catch ( Exception ex ) { // TODO: LogError( "Unable to query Active Directory.", ex ); return new[] { "" }; } } } ///  /// Retrieve listing of all users in a specified role. ///  /// String array of users ///  public override string[] GetUsersInRole( String rolename ) { if ( !RoleExists( rolename ) ) throw new ProviderException( String.Format( "The role '{0}' was not found.", rolename ) ); using ( PrincipalContext context = new PrincipalContext( ContextType.Domain, _domain ) ) { try { GroupPrincipal p = GroupPrincipal.FindByIdentity( context, IdentityType.SamAccountName, rolename ); return ( from user in p.GetMembers( true ) where !_usersToIgnore.Contains( user.SamAccountName ) select user.SamAccountName ).ToArray(); } catch ( Exception ex ) { // TODO: LogError( "Unable to query Active Directory.", ex ); return new[] { "" }; } } } ///  /// Determine if a specified user is in a specified role. ///  ///  ///  /// Boolean indicating membership public override bool IsUserInRole( string username, string rolename ) { return GetUsersInRole( rolename ).Any( user => user == username ); } ///  /// Retrieve listing of all roles. ///  /// String array of roles public override string[] GetAllRoles() { string[] roles = ADSearch( _activeDirectoryConnectionString, AD_FILTER, AD_FIELD ); return ( from role in roles.Except( _groupsToIgnore ) where !_isAdditiveGroupMode || _groupsToUse.Contains( role ) select role ).ToArray(); } ///  /// Determine if given role exists ///  /// Role to check /// Boolean indicating existence of role public override bool RoleExists( string rolename ) { return GetAllRoles().Any( role => role == rolename ); } ///  /// Return sorted list of usernames like usernameToMatch in rolename ///  /// Role to check /// Partial username to check ///  public override string[] FindUsersInRole( string rolename, string usernameToMatch ) { if ( !RoleExists( rolename ) ) throw new ProviderException( String.Format( "The role '{0}' was not found.", rolename ) ); return ( from user in GetUsersInRole( rolename ) where user.ToLower().Contains( usernameToMatch.ToLower() ) select user ).ToArray(); } #region Non Supported Base Class Functions ///  /// AddUsersToRoles not supported. For security and management purposes, ADRoleProvider only supports read operations against Active Direcory. ///  public override void AddUsersToRoles( string[] usernames, string[] rolenames ) { throw new NotSupportedException( "Unable to add users to roles. For security and management purposes, ADRoleProvider only supports read operations against Active Direcory." ); } ///  /// CreateRole not supported. For security and management purposes, ADRoleProvider only supports read operations against Active Direcory. ///  public override void CreateRole( string rolename ) { throw new NotSupportedException( "Unable to create new role. For security and management purposes, ADRoleProvider only supports read operations against Active Direcory." ); } ///  /// DeleteRole not supported. For security and management purposes, ADRoleProvider only supports read operations against Active Direcory. ///  public override bool DeleteRole( string rolename, bool throwOnPopulatedRole ) { throw new NotSupportedException( "Unable to delete role. For security and management purposes, ADRoleProvider only supports read operations against Active Direcory." ); } ///  /// RemoveUsersFromRoles not supported. For security and management purposes, ADRoleProvider only supports read operations against Active Direcory. ///  public override void RemoveUsersFromRoles( string[] usernames, string[] rolenames ) { throw new NotSupportedException( "Unable to remove users from roles. For security and management purposes, ADRoleProvider only supports read operations against Active Direcory." ); } #endregion ///  /// Performs an extremely constrained query against Active Directory. Requests only a single value from /// AD based upon the filtering parameter to minimize performance hit from large queries. ///  /// Active Directory Connection String /// LDAP format search filter /// AD field to return /// String array containing values specified by 'field' parameter private String[] ADSearch( String ConnectionString, String filter, String field ) { DirectorySearcher searcher = new DirectorySearcher { SearchRoot = new DirectoryEntry( ConnectionString ), Filter = filter, PageSize = 500 }; searcher.PropertiesToLoad.Clear(); searcher.PropertiesToLoad.Add( field ); try { using ( SearchResultCollection results = searcher.FindAll() ) { List r = new List(); foreach ( SearchResult searchResult in results ) { var prop = searchResult.Properties[ field ]; for ( int index = 0; index < prop.Count; index++ ) r.Add( prop[ index ].ToString() ); } return r.Count > 0 ? r.ToArray() : new string[ 0 ]; } } catch ( Exception ex ) { throw new ProviderException( "Unable to query Active Directory.", ex ); } } } } 

Una entrada de subsección de configuración de ejemplo para esto sería la siguiente:

       

¡Vaya, eso es mucho código!

PD: las partes principales del proveedor de roles anteriores se basan en el trabajo de otra persona, no tengo el enlace a mano pero lo encontramos a través de Google, por lo tanto, crédito parcial para esa persona por el original. Lo modificamos mucho para usar LINQ y para eliminar la necesidad de una base de datos para el almacenamiento en caché.