Реализация собственного провайдера ролей при использовании FBA в Sharepoint

В первой части статьи я сделал обзор методов, с помощью которых можно добавить группу AllUsers, аналогичную OTB группе “NT Authority/Authenticated Users” в Windows аутентификации, при использовании FBA. Также мы наметили направления дальнейшей работы, которая необходима для реализации собственного провайдера ролей (role provider). Мы остановились на том, что определили набор методов из класса SqlRoleProvider, которые нужно переопределить.

Во второй части статьи я расскажу о том, как реализовать собственный провайдер ролей, используя информацию из первой части. Итак, нам нужно переопределить несколько методов класса SqlRoleProvider (для удобства я продублирую их здесь еще раз):

public class SqlRoleProvider : RoleProvider
{
    public override string[] FindUsersInRole(string roleName, string usernameToMatch);
    public override string[] GetAllRoles();
    public override string[] GetRolesForUser(string username);
    public override string[] GetUsersInRole(string roleName);
    public override bool IsUserInRole(string username, string roleName);
    public override bool RoleExists(string roleName);
    ...
}

Важно понимать, что эти методы должны быть переопределены консистентно, т.е. они не должны противоречить друг другу по смыслу. Например, если метод GetRolesForUser() возвращает группу “AllUsers” для пользователя “john”, то это значит, что метод GetUsersInRole() для роли “AllUsers” тоже должен вернуть пользователя “john” вместе с другими пользователями (если таковые имеются). Также метод IsUserInRole() для группы “AllUsers” и пользователя “john” должен вернуть true, и т.д.

Также мы хотим сохранить функциональность стандартного провайдера SqlRoleProvider без изменений, чтобы избежать потенциальных side эффектов, вызванных кастомным провайдером. Кроме этого, наш провайдер должен быть реализован настолько безопасно и надежно, насколько возможно.

Формализовав требования, мы можем реализовать провайдер ролей, который будет добавлять “виртуальную” группу AllUsers ко всем FBA пользователям:

public class CustomFBARoleProvider : SqlRoleProvider
{
    private const string ALL_USERS_GROUP_NAME = "AllUsers";
 
    public override string[] FindUsersInRole(string roleName, string usernameToMatch)
    {
        try
        {
            if (!this.isSpecialGroup(roleName))
            {
                return base.FindUsersInRole(roleName, usernameToMatch);
            }
 
            var userNames = getAllUsersSafely(usernameToMatch);
            if (userNames.IsNullOrEmpty())
            {
                userNames = new string[]{};
            }
            return userNames;
        }
        catch (Exception x)
        {
            return base.FindUsersInRole(roleName, usernameToMatch);
        }
    }
 
    public override string[] GetAllRoles()
    {
        try
        {
            string[] roles = base.GetAllRoles();
            return this.ensureContainsAllUsers(roles);
        }
        catch (Exception x)
        {
            return base.GetAllRoles();
        }
    }
 
    public override string[] GetRolesForUser(string username)
    {
        try
        {
            if (string.IsNullOrEmpty(username))
            {
                return base.GetRolesForUser(username);
            }
 
            bool? userExists = this.isUserExists(username);
            if (!userExists.HasValue || !userExists.Value)
            {
                return base.GetRolesForUser(username);
            }
 
            string[] roles = base.GetRolesForUser(username);
            return this.ensureContainsAllUsers(roles);
        }
        catch (Exception x)
        {
            return base.GetRolesForUser(username);
        }
    }
 
    public override string[] GetUsersInRole(string roleName)
    {
        try
        {
            if (!this.isSpecialGroup(roleName))
            {
                return base.GetUsersInRole(roleName);
            }
 
            var userNames = getAllUsersSafely();
            if (userNames.IsNullOrEmpty())
            {
                userNames = new string[] { };
            }
            return userNames;
        }
        catch (Exception x)
        {
            return base.GetUsersInRole(roleName);
        }
    }
 
    public override bool IsUserInRole(string username, string roleName)
    {
        try
        {
            if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(roleName))
            {
                return base.IsUserInRole(username, roleName);
            }
 
            if (this.isSpecialGroup(roleName))
            {
                bool? userExists = this.isUserExists(username);
                if (userExists.HasValue && userExists.Value)
                {
                    return true;
                }
            }
            return base.IsUserInRole(username, roleName);
        }
        catch (Exception x)
        {
            return base.IsUserInRole(username, roleName);
        }
    }
 
    public override bool RoleExists(string roleName)
    {
        try
        {
            if (this.isSpecialGroup(roleName))
            {
                return true;
            }
            return base.RoleExists(roleName);
        }
        catch (Exception x)
        {
            return base.RoleExists(roleName);
        }
    }
 
    private string[] getAllUsersSafely()
    {
        try
        {
            var membershipCollection = Membership.GetAllUsers();
            if (membershipCollection == null || membershipCollection.Count == 0)
            {
                return new string[]{};
            }
            var userNames = membershipCollection.Cast<MembershipUser>()
                .Select(m => m.UserName).ToArray();
            return userNames;
        }
        catch (Exception x)
        {
            return new string[] { };
        }
    }
 
    private string[] getAllUsersSafely(string userNameToMatch)
    {
        try
        {
            var membershipCollection = Membership.FindUsersByName(userNameToMatch);
            if (membershipCollection == null || membershipCollection.Count == 0)
            {
                return new string[] { };
            }
            var userNames = membershipCollection.Cast<MembershipUser>()
                .Select(m => m.UserName).ToArray();
            return userNames;
        }
        catch (Exception x)
        {
            return new string[] { };
        }
    }
 
    private bool? isUserExists(string username)
    {
        try
        {
            var user = Membership.GetUser(username, false);
            return (user != null);
        }
        catch (Exception x)
        {
            return null;
        }
    }
 
    private string[] ensureContainsAllUsers(string[] roles)
    {
        if (roles.IsNullOrEmpty())
        {
            roles = new string[]{};
        }
        var rolesList = new List<string>(roles);
        if (!rolesList.Contains(ALL_USERS_GROUP_NAME))
        {
            rolesList.Add(ALL_USERS_GROUP_NAME);
        }
        return rolesList.ToArray();
    }
 
    private bool isSpecialGroup(string roleName)
    {
        return (string.Compare(roleName, ALL_USERS_GROUP_NAME, true) == 0);
    }
}

В данном примере я использовал удобный метод-расширение (extension method) IsNullOrEmpty() для типа IEnumerable<T>, описанный в этом посте Phil Haack-а:

public static class Extensions
{
    public static bool IsNullOrEmpty<T>(this IEnumerable<T> items)
    {
        // see http://haacked.com/archive/2010/06/10/checking-for-empty-enumerations.aspx
        //return (items == null || items.Count() == 0);
        return (items == null || !items.Any());
    }
}

Рассмотрим, к примеру, наиболее важный метод GetRolesForUser(). Сначала мы проверяем на пустое значение userName, и если оно пустое – вызываем базовую реализацию метода, т.к. не хотим сами обрабатывать случаи с некорректными входными данными. Далее мы проверяем, существует ли указанный пользователь, с использованием membership провайдера (метод isUserExists()). Если пользователь не существует, мы опять вызываем базовую реализацию, а не обрабатываем эту ошибку сами. Используя такой подход, мы по максимуму сохраняем backward compatibility со стандартным SqlRoleProvider и с его возможными потребителями в коде (напр. в коде могут быть места, заточенные под обработку ошибок и исключений, которые возвращаются из SqlRoleProvider). Если пользователь существует, мы запрашиваем все роли указанного аккаунта, и добавляем к результату нашу специальную группу AllUsers (метод ensureContainsAllUsers()).

Остальные методы реализованы по тому же принципу – если в параметрах метода передана группа AllUsers, добавляется special case. Для всех остальных случаев (в т.ч. в случае ошибок) вызывается базовая реализация. Действуя таким образом, мы сохраняем существующую функциональность SqlRoleProvider без изменений.

После этого, осталось лишь установить сборку с кастомным провайдером в GAC и заменить стандартный SqlRoleProvider в web.config-е приложения на свой провайдер:

<roleManager enabled="true" defaultProvider="RoleProviderName">
  <providers>
    <add connectionStringName="ConnectionStringName" applicationName="/" name="RoleProviderName" type="Example.CustomFBARoleProvider, Example, Version=1.0.0.0, Culture=neutral, PublicKeyToken=..." />
  </providers>
</roleManager>

Теперь если вы откроете People picker контрол и введете AllUsers, то People picker должен отрезолвить имя в RoleProviderName:AllUsers, и вы сможете назначать права этой группе также, как вы назначали их для обычных пользователей.

Как я говорил выше, эта же идея может быть использована с другими хранилищами учетных записей и, как следствие, с другими провайдерами ролей. Используя описанный подход вы сможете “виртуализировать” AllUsers FBA группу, не имея реальной группы в вашем backend хранилище аккаунтов и групп. При этом с точки зрения Sharepoint все FBA пользователи будут являться членами этой группы сразу после создания и вы сможете использовать ее для настройки параметров безопасности.

Реклама

Об авторе sadomovalex

Старший инженер, team lead, консультант. Работаю в стеке .Net. Последние несколько лет занимаюсь разработкой enterprise приложений под Sharepoint, чему и будет в основном посвящена тематика этого блога. Также активно использую и интересуюсь ASP.Net MVC, DDD, TDD, Agile. Активно участвую в жизни многих профессиональных сообществ, SPb .Net UG, SPb ALT.Net, rsdn, Finland SP UG и др.
Запись опубликована в рубрике FBA, Security, Sharepoint. Добавьте в закладки постоянную ссылку.

Один комментарий на «Реализация собственного провайдера ролей при использовании FBA в Sharepoint»

  1. Уведомление: Дайджест технических материалов #6 (обо всем) - Oleksandr Krakovetskiy blog - Microsoft User Group Винница

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s