developer tip

Active Directory를 사용하는 .NET의 사용자 그룹 및 역할 관리

copycodes 2020. 12. 30. 08:18
반응형

Active Directory를 사용하는 .NET의 사용자 그룹 및 역할 관리


현재 .NET 기반 프로젝트에 대한 사용자 역할 및 권한을 저장하는 방법을 연구하고 있습니다. 이러한 프로젝트 중 일부는 웹 기반이고 일부는 그렇지 않습니다. 저는 현재 프로젝트 유형간에 일관되고 이식 가능한 방식으로 원하는 것을 달성하기위한 최상의 방법을 찾기 위해 고군분투하고 있습니다.

제가있는 곳에서는 기본 사용자 정보에 대한 단일 연락처로 Active Directory를 활용하려고합니다. 이로 인해 각 응용 프로그램의 사용자가 이미 Active Directory에 저장되어 있고 적극적으로 유지 관리되기 때문에 각 응용 프로그램의 사용자에 대해 사용자 지정 데이터베이스를 유지할 필요가 없습니다. 또한 가능하면 자체 보안 모델 / 코드를 작성하지 않고 Microsoft에서 제공하는 보안 응용 프로그램 블록과 같은 기존의 것을 사용하고 싶습니다.

일부 프로젝트에는 읽기, 쓰기 또는 액세스 권한 없음과 같은 기본 권한 만 필요합니다. 다른 프로젝트에는 더 복잡한 권한이 필요합니다. 이러한 응용 프로그램의 사용자는 일부 영역에 대한 액세스 권한을 부여받을 수 있지만 다른 영역에는 액세스 할 수 없으며 권한은 각 영역에서 변경 될 수 있습니다. 앱의 관리 섹션은 AD 도구가 아닌 이 액세스를 제어하고 정의 합니다.

현재는 통합 Windows 인증을 사용하여 인트라넷에서 인증을 수행하고 있습니다. 이것은 기본 사용자 정보를 찾는 데 효과적이며 ASP.NET을 확장하여 Active Directory 역할 공급자를 제공 할 수 있다는 것을 확인했습니다. 따라서 사용자가 속한 모든 보안 그룹을 찾을 수 있습니다. 그러나이 방법의 몰락은 모든 것이 Active Directory에 저장되어 너무 커지면 유지 관리가 엉망이 될 수 있다는 것입니다.

이와 같은 맥락에서 저는 Active Directory Lightweight Directory Services에 대해서도 들어 봤습니다.이 서비스는 스키마를 확장하고 응용 프로그램 별 특성과 그룹 만 추가 할 수있는 것처럼 보입니다. 문제는 이것이 어떻게 이루어 지거나 어떻게 작동하는지에 대해 아무것도 찾을 수 없다는 것입니다. 이 인스턴스와 대화하는 방법과 새 인스턴스를 만드는 방법을 설명하는 MSDN 문서가 있지만 내 질문에 대한 답이없는 것 같습니다.

내 질문은 : 당신의 경험에 비추어 볼 때, 내가 올바른 길을 가고 있는가? 내가 원하는 것은 Active Directory만으로 가능합니까, 아니면 다른 도구를 사용해야합니까?


내가 살펴본 다른 방법 :

  • 여러 web.config 파일 사용 [ stackoverflow ]
  • 애플리케이션 전체에서 사용자를 관리하기위한 사용자 정의 보안 모델 및 데이터베이스 생성

인증을 위해 AD를 사용하는 것은 좋은 생각입니다. 어쨌든 모든 사람을 거기에 추가해야하고 인트라넷 사용자의 경우 추가 로그인이 필요하지 않기 때문입니다.

ASP.NET을 사용하면 AD에 대해 인증 할 수있는 Provider를 사용할 수 있다는 것이 맞습니다. 그룹 멤버십 지원을 제공하기 위해 포함 된 것은 없지만 원하는 경우 구현하는 것은 매우 간단하지만 샘플을 제공 할 수 있습니다. ).

여기서 진짜 문제는 AD 그룹을 사용하여 각 앱 내에서 권한을 정의하려는 경우입니다.

그렇다면 ApplicationServices를 통해 WinForms 및 WPF 앱에서도 사용할 수있는 ASP.NET 용 RoleProvider를 직접 만들 수있는 옵션이 있습니다. 이 RoleProvider는 AD의 사용자 ID를 앱별 그룹 / 역할에 연결하여 자신의 사용자 지정 데이터베이스에 저장할 수 있으며,이를 통해 각 앱은 이러한 관리자가 AD에서 추가 권한을 갖지 않고도 이러한 역할을 관리 할 수 ​​있습니다.

원하는 경우 앱 역할을 재정의하고 AD 그룹과 결합 할 수도 있습니다. 따라서 AD의 일부 글로벌 "관리자"그룹에있는 경우 앱 역할 멤버십에 관계없이 앱에서 전체 권한을 얻습니다. 반대로 AD에 그룹이나 속성이있어 해고되었다고 말할 수있는 경우 모든 앱 역할 멤버십을 무시하고 모든 액세스를 제한 할 수 있습니다 (HR이 해당 앱에 대해 알고 있다고 가정하고 각 앱에서 해당 항목을 제거하지 않을 수 있기 때문). 모두!).

요청에 따라 추가 된 샘플 코드 :

참고 :이 원본 작품을 기반으로합니다. http://www.codeproject.com/Articles/28546/Active-Directory-Roles-Provider

ActiveDirectoryMembershipProvider의 경우 ValidateUser 메서드 만 구현하면되지만 원하는 경우 더 많은 것을 구현할 수 있지만 새 AccountManagement 네임 스페이스는 다음과 같이 간단합니다.

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

역할 제공자의 경우 약간 더 많은 작업이 필요하며 제외하려는 그룹, 제외하려는 사용자 등과 같이 Google을 검색하는 동안 발견 한 몇 가지 주요 문제가 있습니다.

아마도 전체 블로그 게시물의 가치가 있지만 이것은 시작하는 데 도움이 될 것입니다. 이것은 성능을 향상시킬 수있는 방법의 샘플처럼 세션 변수에서 조회를 캐싱하는 것입니다 (전체 캐시 샘플이 너무 길기 때문).

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<string> _groupsToUse;
        private List<string> _groupsToIgnore;
        private List<string> _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

        /// <summary>
        /// Initializes a new instance of the ADRoleProvider class.
        /// </summary>
        public ActiveDirectoryRoleProvider()
        {
            _groupsToUse = new List<string>();
            _groupsToIgnore = new List<string>();
            _usersToIgnore = new List<string>();
        }

        public override String ApplicationName { get; set; }

        /// <summary>
        /// Initialize ADRoleProvider with config values
        /// </summary>
        /// <param name="name"></param>
        /// <param name="config"></param>
        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<string> 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<string> groups )
        {
            var principal = GroupPrincipal.FindByIdentity( context, IdentityType.SamAccountName, group );

            if ( principal == null )
                return;

            List<string> res =
                principal
                    .GetGroups()
                    .ToList()
                    .Select( grp => grp.Name )
                    .ToList();

            groups.AddRange( res.Except( groups ) );
            foreach ( var item in res )
                RecurseGroup( context, item, groups );
        }

        /// <summary>
        /// Retrieve listing of all roles to which a specified user belongs.
        /// </summary>
        /// <param name="username"></param>
        /// <returns>String array of roles</returns>
        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<string>) ( 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<string>( 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[] { "" };
                }
            }
        }

        /// <summary>
        /// Retrieve listing of all users in a specified role.
        /// </summary>
        /// <param name="rolename">String array of users</param>
        /// <returns></returns>
        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[] { "" };
                }
            }
        }

        /// <summary>
        /// Determine if a specified user is in a specified role.
        /// </summary>
        /// <param name="username"></param>
        /// <param name="rolename"></param>
        /// <returns>Boolean indicating membership</returns>
        public override bool IsUserInRole( string username, string rolename )
        {
            return GetUsersInRole( rolename ).Any( user => user == username );
        }

        /// <summary>
        /// Retrieve listing of all roles.
        /// </summary>
        /// <returns>String array of roles</returns>
        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();
        }

        /// <summary>
        /// Determine if given role exists
        /// </summary>
        /// <param name="rolename">Role to check</param>
        /// <returns>Boolean indicating existence of role</returns>
        public override bool RoleExists( string rolename )
        {
            return GetAllRoles().Any( role => role == rolename );
        }

        /// <summary>
        /// Return sorted list of usernames like usernameToMatch in rolename
        /// </summary>
        /// <param name="rolename">Role to check</param>
        /// <param name="usernameToMatch">Partial username to check</param>
        /// <returns></returns>
        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

        /// <summary>
        /// AddUsersToRoles not supported.  For security and management purposes, ADRoleProvider only supports read operations against Active Direcory. 
        /// </summary>
        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." );
        }

        /// <summary>
        /// CreateRole not supported.  For security and management purposes, ADRoleProvider only supports read operations against Active Direcory. 
        /// </summary>
        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." );
        }

        /// <summary>
        /// DeleteRole not supported.  For security and management purposes, ADRoleProvider only supports read operations against Active Direcory. 
        /// </summary>
        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." );
        }

        /// <summary>
        /// RemoveUsersFromRoles not supported.  For security and management purposes, ADRoleProvider only supports read operations against Active Direcory. 
        /// </summary>
        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

        /// <summary>
        /// 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.
        /// </summary>
        /// <param name="ConnectionString">Active Directory Connection String</param>
        /// <param name="filter">LDAP format search filter</param>
        /// <param name="field">AD field to return</param>
        /// <returns>String array containing values specified by 'field' parameter</returns>
        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<string> r = new List<string>();
                    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 );
            }
        }
    }
}

이에 대한 샘플 구성 하위 섹션 항목은 다음과 같습니다.

<roleManager enabled="true" defaultProvider="ActiveDirectory">
  <providers>
    <clear/>
    <add
        applicationName="MyApp" name="ActiveDirectory"
        type="MyApp.Security.ActiveDirectoryRoleProvider"
        domain="mydomain" groupMode="" connectionString="LDAP://myDirectoryServer.local/dc=mydomain,dc=local"
    />
  </providers>
</roleManager>

휴, 그것은 많은 코드입니다!

추신 : 위의 역할 제공자의 핵심 부분은 다른 사람의 작업을 기반으로합니다. 링크는 없지만 Google을 통해 찾았으므로 원본에 대한 부분적인 크레딧을 제공합니다. LINQ를 사용하고 캐싱을위한 데이터베이스의 필요성을 없애기 위해 크게 수정했습니다.

ReferenceURL : https://stackoverflow.com/questions/726837/user-group-and-role-management-in-net-with-active-directory

반응형