Нестандартная система разграничения доступа в приложении ASP. Net MVC

2 мая 2011 г.

В одном из проектов, написанном на ASP. Net MVC 3 потребовалась авторизация пользователей.
Проект предусматривал несколько фиксированных ролей пользователей и авторизацию на уровне контроллеров и действий.

Казалось бы, что проще, используем существующее решение в виде атрибута [Authorize(Roles = «Role1»)], проставленного у контроллера или действия.
Так и было сделано, но оказалось что это оооочень неудобно в применении:

  • Атрибуты разбросаны по всему проекту, приходилось держать в голове кто что и где может сделать.
  • В случае добавления новой роли приходилось открывать все (!) файлы с контроллерами и добавлять соответствующие записи к атрибутам.
  • Тоже самое в случае переименования роли (хотя тут позже был найден выход путем вынесения названия ролей в static const string).

Было решено применить иной подход:

  • оставить авторизацию на уровне контроллер/действие
  • вынести назначение прав в одно место (файл), что бы все было перед глазами
  • сохранить или улучшить управление правами путем внедрения понятий «все действия в контроллере», «любая роль» и т.д.


Перейдем сразу к решению

Основа решения — добавление фильтра «Безопасность» (класс MySecurityFilter) к ряду уже зарегистрированных (пример использования кода в конце статьи).

Вот исходный код этого фильтра:

public class MySecurityFilter : FilterAttribute, IAuthorizationFilter
 {
   public void OnAuthorization(AuthorizationContext filterContext)
   {
     var role = filterContext.HttpContext.User.GetRole();

     var action = (string)filterContext.RouteData.Values["action"];
     var controllerType = filterContext.Controller.GetType();

     if (!MySecurity.Instance.IsGrantAction(role, controllerType, action))
     {
       filterContext.Result = new HttpUnauthorizedResult();
     }
   }
 }

Думаю особых вопросов здесь не должно возникнуть. Мы наследуемся от интерфейса IAuthorizationFilter и обрабатываем событие OnAuthorization:

  • Получаем контроллер и экшн
  • Запрашиваем у инстанс MySecurity разрешено ли нам действие
  • Если запрещено задаем filterContext.Result = new HttpUnauthorizedResult();

Посмотрим, что внутри у MySecurity:

 {
   private static readonly MySecurity InstanceField = new MySecurity();
   public static MySecurity Instance{get { return InstanceField;}}

   private readonly Dictionary<string, List<string>> dict;
   public MySecurity()
   {
     dict = new Dictionary<string, List<string>>();
   }
   public PermissionTableBuilder Table()
   {
     return new PermissionTableBuilder(this);
   }
   // методы класса, описание ниже
   // ...
 }
 

Несколько замечаний по данному классу:
1. Словарь содержит таблицу прав доступа. Ключ — название роли. Значение — список пермишшнов, в формате «ControllerName+Action»
Стоит отметить, что для ролей допустимо указание "*" и "?" (любой авторизованный и просто любой пользователь).
Для Action допустимо указание "*" — любой экшн в контроллере.
2. PermissionTableBuilder — специальный класс для упрощения построения этой таблицы

Методы класса MySecurity

Добавляем права:

public class MySecurity
public void AddPerm(string role, string perm)
 {
   if (!dict.ContainsKey(role))
   {
     dict[role] = new List<string>();
   }
   dict[role].Add(perm);
 }
 

Проверяем права доступа:

public bool IsGrant(string role, string perm)
 {
   switch (role)
   {
     case "?":
       return Any(perm);
     case "*":
       return Any(perm) || All(perm);
     default:
       return Any(perm) || All(perm) || Role(role, perm);
   }
 }
// проверяем права доступа для любого пользователя
private bool Any(string perm)
 {
   return Role("?", perm);
 }
// проверяем права доступа для любого авторозованного, т.е. с ролью пользователя
private bool All(string perm)
 {
   return Role("*", perm);
 }
// проверяем права доступа для конкретной роли
private bool Role(string role, string perm)
 {
   if (dict.ContainsKey(role))
   {
     return dict[role].Contains(perm);
   }
   return false;
 }
 

Дополнительные методы для упрощения проверки прав доступа:

public bool IsGrantAction(string role, Type controllerType, string action)
 {
   return IsGrantAction(role, controllerType.Name, action);
 }

public bool IsGrantAction(string role, string controllerName, string action)
 {
   // проверим сначала нет ли Permission, в котором разрешены любые действия
   if (IsGrant(role, MySecurityHelper.GetPermission(controllerName, "*")))
   {
     return true;
   }
   return IsGrant(role, MySecurityHelper.GetPermission(controllerName, action));
 } 

Класс PermissionTableBuilder, служащий для упрощения построения таблицы прав доступа.
Его функционал может показаться непонятным, поэтому стоит взглянуть на пример его использования в конце статьи.


public class PermissionTableBuilder
 {
   private readonly MySecurity table;
   private readonly List<string > roles;

   public PermissionTableBuilder(MySecurity table)
   {
     this.table = table;
     roles = new List<string>();
   }

   public PermissionTableBuilder WithRole(string role)
   {
     roles.Add(role);
     return this;
   }

   public PermissionTableBuilder AddPerm<TController>(params string [] actions)
   {
     ProcessPerm(actions, x=> MySecurityHelper.GetPermission(typeof (TController), x));
     return this;
   }

   public PermissionTableBuilder AddPerm(params string[] actions)
   {
     ProcessPerm(actions, x=>x);
     return this;
   }

   private void ProcessPerm(string[] actions, Func<string, string> getPermNameFunc)
   {
     if (roles.Count == 0)
     {
       throw new InvalidOperationException("Call withRole first");
     }
     foreach (var action in actions)
     {
       foreach (var role in roles)
       {
         string perm = getPermNameFunc(action);
         table.AddPerm(role, perm);
       }
     }
   }
 }

Тройка вспомогательных методов, оформленных отдельно:


public static class MySecurityHelper
 {
   public static string GetPermission(Type controllerType, string actionName)
   {
     return String.Format("{0}+{1}", controllerType.Name, actionName);
   }

   public static string GetPermission(string controllerName, string actionName)
   {
     if (controllerName.EndsWith("Controller"))
     {
       return String.Format("{0}+{1}", controllerName, actionName);
     }
     return String.Format("{0}Controller+{1}", controllerName, actionName);
   }

   public static string GetRole(this IPrincipal user)
   {
     var principal = user as RolePrincipal;
     if (principal == null)
     {
       return "?";
     }
     return principal.GetRoles().SingleOrDefault() ?? "?";
   }
 }

А теперь смотрим на пример использования всей системы целиком:

Global.asax:


public static void RegisterGlobalFilters(GlobalFilterCollection filters)
 {
   // прочие фильтры
   // ...
   filters.Add(new MySecurityFilter());
 }
protected void Application_Start()
 {
   // ...
   PermissionListCreator.Create(MySecurity.Instance);
   // ...
 }

Вспомогательный класс PermissionListCreator.cs, содержащий код, создающий таблицу прав:


public static class PermissionListCreator
 {
   public static void Create(MySecurity security)
   {
     // Открытые для всех пользователей страницы
     security.Table().WithRole("?").AddPerm<HomeController>("*");
     security.Table().WithRole("?").AddPerm<LogOnController>("*");
     security.Table().WithRole("?").AddPerm<ErrorController>("*");

     // Личный кабинет
     security.Table().WithRole("*").AddPerm<CabinetController>("*");
     
     // Управление пользователями
     security.Table().
       WithRole(MyRoles.Director).AddPerm<UsersController>("*");
     security.Table().
       WithRole(MyRoles.Admin).AddPerm<UsersController>("Add", "Edit");

     // просмотр лога
     security.Table().
       WithRole(MyRoles.Admin).
       WithRole(MyRoles.Director).
       WithRole(MyRoles.Commander).
       WithRole(MyRoles.Developer).AddPerm<LogsController>("*");
       
     // и так далее, со всеми контроллерами
 }
  1. разрешили доступ всем пользователям к домашней странице проекта, форме авторизации, и сообщениям об ошибках.
  2. разрешили всем авторизованным пользователям доступ к личному кабинету
  3. разрешили администратору добавлять и изменять пользователей(но не удалять), а директору и удалять
  4. разрешили 4-м ролям просматривать лог

Обратите внимание, что все явно не разрешенные действия — запрещены!

На мой взгляд, получилось достаточно простое в использование и надежное решение. Правда сфера его применения ограничена, но ничто не мешает использовать его с любой другой системой разграничения доступа.

Теги:
рубрика C#