Главная » Хабрахабр » Доступ к данным в многопользовательских приложениях

Доступ к данным в многопользовательских приложениях

Вопрос ограничения доступа к данным встает при разработке многопользовательских систем почти всегда. Основные сценарии следующие:

  1. ограничение доступа к данным для пользователей не прошедших аутентификацию
  2. ограничение доступа к данным для аутентифицированных, но не обладающих необходимыми привелегиями пользователей
  3. предотвращение несанкционированного доступа с помощью прямых обращений к API
  4. фильтрация данных в поисковых запросах и списковых элементах UI (таблицы, списки)
  5. предотвращение изменения данных, принадлежащих одному пользователю другими пользователями

Сценарии 1-3 хорошо описаны и обычно решаются с помощью встроенных средств фреймворков, например role-based или claim-based авторизации. А вот ситуации, когда авторизованный пользователь может по прямому url получить доступ к данным «соседа» или совершить действие в его аккаунте случаются сплошь и рядом. Происходит это чаще всего из-за того что программист забывает добавить необходимую проверку. Можно понадеяться на код-ревью, а можно предотвратить такие ситуации применив глобальные правила фильтрации данных. О них и пойдет речь в статье.

Списки и таблицы

Типовой контроллер для получения данных в ASP.NET MVC может выглядеть как-то так:

[HttpGet] public virtual IActionResult Get([FromQuery]T parameter) ); }

В данном случае вся ответственность за фильтрацию данных ложится только на программиста. Вспомнит ли он о том, что необходимо добавить условие в Where или нет?

Однако, для ограничения доступа нам потребуется информация о текущем пользователе, а значит конструирование DbContext придется усложнить, чтобы проинициализировать конкретные поля. Можно решить проблему с помощю глобальных фильтров.

Если правил много, то реализации DbContext неизбежно придется узнать «слишком много», что приведет к нарушению принципа единственной ответственности.

Слоеная архитектура

Проблемы с доступом к данным и копипастой возникли, потому что в примере мы проигнорировали разделение на слои и из контроллеров сразу потянулись к слою доступа к данным, минуя слой бизнес-логики. Такой подход даже окрестили «толстыми тупыми уродливыми контроллерами». В этой статье я не хочу касаться вопросов, связанных с репозиториями, сервисами и структурированием бизнес-логики. Глобальные фильтры хорошо справляются с этой задачей, нужно только применить их к абстракции из другого слоя.

Добавляем абстракцию

В .NET для доступа к данным уже есть IQueryable. Заменим прямой доступ к DbContext на доступ вот к такому провайдеру:

public interface IQueryableProvider { IQueryable<T> Query<T>() where T: class; IQueryable Query(Type type); }

А для доступа к данным сделаем вот такой фильтр:

public interface IPermissionFilter<T> { IQueryable<T> GetPermitted(IQueryable<T> queryable); }

Реализуем провайдер таким образом, чтобы он искал все объявленные фильтры и автоматически применял их:

public class QueryableProvider: IQueryableProvider { // ищем фильтры и запоминаем их типы private static Type[] Filters = typeof(PermissionFilter<>) .Assembly .GetTypes() .Where(x => x.GetInterfaces().Any(y => y.IsGenericType && y.GetGenericTypeDefinition() == typeof(IPermissionFilter<>))) .ToArray(); private readonly DbContext _dbContext; private readonly IIdentity _identity; public QueryableProvider(DbContext dbContext, IIdentity identity) { _dbContext = dbContext; _identity = identity; } private static MethodInfo QueryMethod = typeof(QueryableProvider) .GetMethods() .First(x => x.Name == "Query" && x.IsGenericMethod); private IQueryable<T> Filter<T>(IQueryable<T> queryable) => Filters // ищем фильтры необходимого типа .Where(x => x.GetGenericArguments().First() == typeof(T)) // создаем все фильтры подходящего типа и применяем к Queryable<T> .Aggregate(queryable, (c, n) => ((dynamic)Activator.CreateInstance(n, _dbContext, _identity)).GetPermitted(queryable)); public IQueryable<T> Query<T>() where T : class => Filter(_dbContext.Set<T>()); // из EF Core убрали Set(Type type), приходится писать самому 🙁 public IQueryable Query(Type type) => (IQueryable)QueryMethod .MakeGenericMethod(type) .Invoke(_dbContext, new object[]{}); }

Код получения и создания фильтров в примере не оптимален. Вместо Activator.CreateInstance а лучше использовать скомпилированные Expression Trees. В некоторых IOC-контейнерах реализованна поддержка регистрации открытых generic'ов. Я оставлю вопросы оптимизации за рамками этой статьи.

Реализуем фильтры

Реализация фильтра может выглядеть, например, так:

public class EntityPermissionFilter: PermissionFilter<Entity> { public EntityPermissionFilter(DbContext dbContext, IIdentity identity) : base(dbContext, identity) { } public override IQueryable<Practice> GetPermitted( IQueryable<Practice> queryable) { return DbContext .Set<Practice>() .WhereIf(User.OrganizationType == OrganizationType.Client, x => x.Manager.OrganizationId == User.OrganizationId) .WhereIf(User.OrganizationType == OrganizationType.StaffingAgency, x => x.Partners .Select(y => y.OrganizationId) .Contains(User.OrganizationId)); } }

Исправляем код контроллера

[HttpGet] public virtual IActionResult Get([FromQuery]T parameter) { var total = QueryableProvider .Query<TEntity>() .Where(/* some business rules */) .Count(); var items = QueryableProvider .Query<TEntity>() .Where(/* some business rules */) .ProjectTo<TDto>() .Skip(parameter.Skip) .Take(parameter.Take) .ToList(); return Ok(new {items, total}); }

Изменений совсем не много. Осталось запретить прямой доступ к DbContext из контроллеров и если фильтры правильно написаны, то вопрос доступа к данным можно считать закрытым. Фильтры достаточно маленькие, поэтому покрыть их тестами не составит труда. Кроме того эти-же самые фильтры можно использовать, чтобы написать код авторизации, предотвращающий несанкционированный доступ к «чужим» данным. Этот вопрос я оставлю для следующей статьи.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Создание пакетов для Kubernetes с Helm: структура чарта и шаблонизация

Теперь подойдём к практике с другой стороны — с точки зрения создателя чартов (т.е. Про Helm и работу с ним «в общем» мы рассказали в прошлой статье. И хотя эта статья пришла из мира эксплуатации, она получилась больше похожей на ...

[Из песочницы] Экономим на RAID-контроллере, или как накормить Варю иопсами

В наш век облачных сервисов, AWS Lambda и прочих шаред хостингов абсолютно неосязаемых вычислительных ресурсов иногда хочется немножко своего. Кроме желания, иногда бывают и потребности вдумчиво покрутить тот или иной программный продукт с минимальными затратами на платформу. Найти какие-то излишки ...