Использование атрибутов безопасности для контроля доступа к коду

Довелось мне недавно перерабатывать модель безопасности на немаленьком проекте с трудной судьбой. Модель получилась удобная, изящная, практичная, в ней по уму реализованы и красиво вписаны в бизнес-логику всякие участники, удостоверения и роли, и единственное, чего не хватало до полного счастья — атрибутов для лаконичного контроля использования кода вида «этот метод запускается только если у текущего пользователя есть такая-то роль». У меня внезапно нашлось несколько часов времени на исследования в этой области, и при ближайшем рассмотрении не менее внезапно оказалось, что для моей ситуации в .NET уже всё есть, осталось только научиться этим пользоваться. И тут внезапно оказалось (снова!), что готовый и полный howto на эту тему нагуглить неожиданно сложно — по крайней мере, мне не удалось этого сделать. Целостную картину пришлось собирать по кусочкам, и
Сначала кратко напомню, что интерфейс IPrincipal является абстракцией, предназначенной для реализации авторизационного слоя безопасности, а интерфейс IIdentity — аутентификационного (то есть IIdentity описывает пользователя в смысле «кто он таков и чем знаменит», а IPrincipal — что ему можно). В рамках предлагаемой схемы нужно будет реализовать оба интерфейса. Реализуем своеобразные стабы (в реальном приложении, конечно, здесь нужно будет реализовать свою бизнес-логику):

class CustomIdentity : IIdentity
{
    public string Name { get { return "Йа криведко!"; } }
    public string AuthenticationType { get { return "Example"; } }
    public bool IsAuthenticated { get { return true; } }
}

class CustomPrincipal : IPrincipal
{
    private readonly string[] _roles;

    public CustomIdentity Identity { get; private set; }
    IIdentity IPrincipal.Identity { get { return Identity; } }

    public CustomPrincipal(string[] roles)
    {
        _roles = roles;
        Identity = new CustomIdentity();
    }

    public bool IsInRole(string role)
    {
        return _roles.Contains(role);
    }
}

Вот.
Далее, с точки зрения модели безопасности .NET роль является строкой. Не будем спорить с умными дядями в очках из Microsoft, строка так строка:

static class SecurityRoleNames
{
    public const string ApplicationUser = "ApplicationUser";
    public const string ApplicationAdmin = "ApplicationAdmin";
}

Обратите внимание, строки объявлены в качестве констант; чуть позже я поясню, зачем это нужно.
Кстати, больше никакие вспомогательные классы не понадобятся, можно переходить к рассмотрению собственно программы. Я набросал консольное приложение, чтобы не мудрствовать лукаво: для демонстрации прототипа, в общем-то, сойдёт. Это приложение показывает, как именно выглядит использование атрибута PrincipalPermission в коде, и дальнейшие словеса здесь, в общем-то, излишни:

class Program
{
    /// 
    /// Хелпер, поочерёдно вызывающий методы TestUser и TestAdmin
    /// 
    static void PerformTests()
    {
        try
        {
            Console.Write("Exec TestUser(): ");
            TestUser();
            Console.Write("Exec TestAdmin(): ");
            TestAdmin();
        }
        catch (Exception e)
        {
            Console.WriteLine("Exception: {0}", e.Message);
        }
    }
        
    /// 
    /// Сей метод доступен только аутентифицированному пользователю (роль ApplicationUser)
    /// 
    [PrincipalPermission(SecurityAction.Demand, Role = SecurityRoleNames.ApplicationUser)]
    static void TestUser()
    {
        Console.WriteLine("TestUser passed");
    }

    /// 
    /// Сей метод доступен только администратору (роль, соответственно, ApplicationAdmin)
    /// 
    [PrincipalPermission(SecurityAction.Demand, Role = SecurityRoleNames.ApplicationAdmin)]
    static void TestAdmin()
    {
        Console.WriteLine("TestAdmin passed");
    }

    static void Main(string[] args)
    {
        /* Вот кстати, политику обязательно следует задать при старте приложения */
        /* Не буду подробно останавливаться на этом моменте, это есть в документации */
        AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);

        /* Сначала вызываем методы от имени анонимного пользователя
         * При попытке вызова первого же защищённого атрибутом метода вылетит 
         * SecurityException, а метод вызван не будет
         */
        Console.WriteLine("nTesting user without roles");
        PerformTests();

        /* Теперь вызываем методы от имени пользователя
         * Первый метод должен успешно выполниться, а при попытке вызова второго должно
         * вылететь исключение 
         */
        Console.WriteLine("nTesting user with role ApplicationUser");
        Thread.CurrentPrincipal = 
            new CustomPrincipal(new[] { SecurityRoleNames.ApplicationUser });
        PerformTests();

        /* Вызов от имени администратора, оба вызова должны пройти без проблем */
        Console.WriteLine("nTesting user with roles ApplicationUser & ApplicationAdmin");
        Thread.CurrentPrincipal =
            new CustomPrincipal(new[]
                                    {
                                        SecurityRoleNames.ApplicationUser,
                                        SecurityRoleNames.ApplicationAdmin
                                    });
        PerformTests();

        Console.ReadKey();
    }
}

Что ж, посмотрим, что получилось:

Testing user without roles
Exec TestUser(): Exception: Сбой при запросе разрешений для владельца учетной записи.

Testing user with role ApplicationUser
Exec  TestUser(): TestUser passed
Exec TestAdmin(): Exception: Сбой при запросе разрешений для владельца учетной записи.

Testing user with roles ApplicationUser & ApplicationAdmin
Exec TestUser(): TestUser passed
Exec TestAdmin(): TestAdmin passed

Реальное поведение соответствует ожидаемому. Похоже, всё работает. Надёжно, красиво, легко использовать. Лепота.

PS Ах да, я забыл пояснить, зачем делать строки константными. Если этого не сделать — компилятор не сможет собрать программу, ругаясь крайне невнятным образом:

An attribute argument must be a constant expression, typeof expression
or array creation expression of an attribute parameter type

2 thoughts on “Использование атрибутов безопасности для контроля доступа к коду

  1. Относительно констант в параметрах конструктора атрибута: метаданные атрибутов собираются во время компиляции, поэтому его параметры должны быть константами времени компиляции. Ну ещё могут быть typeof() выражениями или массивом констант времени компиляции.

    А ещё интересный момент относительно атрибутов: конструктор атрибута вызывается только при попытке получить его. Так что если в теле конструктора есть проверка аргумента с выбрасыванием ArgumentException, то вывалится эта радость исключительно в рантайме, но не при компиляции (а жаль, иногда было бы полезно)

    Нравится

    • Ну да, может, я и погорячился насчёт невнятности ругани, но как по мне — всё равно оглашение требований без намёка на объяснение причин выглядит как-то… хотя да, что с компилятора взять-то, всё нормально.

      Что касается конструктора атрибута — мне кажется, такое устройство даёт больше возможностей: в частности, конструктор имеет возможность учитывать рантайм-контекст, что, разумеется, невозможно на стадии компиляции. Конструировать атрибуты на стадии компиляции было бы в частных случаях, наверное, удобнее, но мне кажется, что цена оказалась бы слишком высока.

      Нравится

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

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

Логотип WordPress.com

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

Google photo

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

Фотография Twitter

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

Фотография Facebook

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

Connecting to %s