
The issue of restricting access to data arises when developing multi-user systems almost always. The main scenarios are as follows:
- data access restriction for non-authenticated users
- data access restriction for authenticated but not having the necessary user privileges
- prevent unauthorized access with direct API calls
- filtering data in search queries and list elements UI (tables, lists)
- Prevent other users from changing data owned by one user.
Scenarios 1-3 are well described and are usually solved using built-in framework tools, such as
role-based or
claim-based authorizations. But situations where an authorized user can access the data of the “neighbor” via a direct url or take an action in his account all the time. This happens most often due to the fact that the programmer forgets to add the necessary checks. You can rely on code review, or you can prevent such situations by applying global data filtering rules. About them will be discussed in the article.
Lists and tables
A typical controller for receiving data in ASP.NET MVC may look something
like this :
[HttpGet] public virtual IActionResult Get([FromQuery]T parameter) { var total = _dbContext .Set<TEntity>() .Where() .Count(); var items= _dbContext .Set<TEntity>() .Where() .ProjectTo<TDto>() .Skip(parameter.Skip) .Take(parameter.Take) .ToList(); return Ok(new {items, total}); }
In this case, all responsibility for filtering data lies only with the programmer. Will he remember that it is necessary to add a condition in
Where
or not?
You can solve the problem with the help of
global filters . However, to restrict access, we will need information about the current user, which means that the construction of
DbContext
will have to be complicated in order to initialize specific fields.
If there are many rules, then the implementation of
DbContext
will inevitably have to learn "too much", which will lead to a violation of the
principle of sole responsibility .
Puff architecture
Problems with data access and copy-paste arose, because in the example we ignored the separation into layers and from the controllers immediately reached for the data access layer, bypassing the business logic layer. Such an approach was even dubbed "
thick, stupid, ugly controllers ." In this article, I don’t want to touch on issues related to repositories, services, and structuring business logic. Global filters do a good job with this task; you just need to apply them to an abstraction from another layer.
Add an abstraction
In .NET, data access already has
IQueryable
. Let's replace direct access to
DbContext
to access such provider here:
public interface IQueryableProvider { IQueryable<T> Query<T>() where T: class; IQueryable Query(Type type); }
And for data access, we will do this filter:
public interface IPermissionFilter<T> { IQueryable<T> GetPermitted(IQueryable<T> queryable); }
We implement the provider in such a way that it searches for all the declared filters and automatically applies them:
public class QueryableProvider: IQueryableProvider {
The code for obtaining and creating filters in the example is not optimal. Instead of
Activator.CreateInstance
it is better to use
compiled Expression Trees . Some IOC containers have implemented support for
registering open generics . I will leave optimization issues beyond the scope of this article.
Implement filters
A filter implementation might look like this:
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)); } }
We correct the controller code
[HttpGet] public virtual IActionResult Get([FromQuery]T parameter) { var total = QueryableProvider .Query<TEntity>() .Where() .Count(); var items = QueryableProvider .Query<TEntity>() .Where() .ProjectTo<TDto>() .Skip(parameter.Skip) .Take(parameter.Take) .ToList(); return Ok(new {items, total}); }
There are not many changes at all. It remains to prohibit direct access to
DbContext
from the controllers and if the filters are correctly written, then the issue of data access can be considered closed. The filters are quite small, so it’s easy to cover them with tests. In addition, these same filters can be used to write an authorization code that prevents unauthorized access to "alien" data. I will leave this question for the next article.