📜 ⬆️ ⬇️

Breeze Server - we delimit access to objects using attributes


In the last article, Breeze.js + Entity Framework + Angular.js = convenient work with database entities directly from the browser, we looked at creating a simplest application, where we took samples and stored data in the database directly from javascript in the browser. Of course, the first readers to have questions about security. Therefore, today we will look at how to organize access control. To do this, we will slightly modify our application from the previous article so that it would be possible with the help of attributes to distribute certain access rights to add, delete, modify and view data to certain users or roles.

Unfortunately, there are no built-in tools for this in the library. And here, the developers offer us two ways.

The first way


Absolutely all changes in our application were saved using the SaveChanges method of a single DbController controller. And, if we do not need flexible access control, but we just need to allow someone to save data, or deny it, then the easiest way out is simply to hang the Authorize attribute on SaveChanges, and then WebApi will take care of giving / denying access to data change. This option is very straightforward and absolutely not flexible, all or nothing, and, as a rule, in real projects this is always not enough.

Path Two


The SaveChanges method accepts one JObject parameter, it contains in one package all the data that needs to be saved. Then we pass it EFContextProvider to the SaveChanges method, and he already parses the object with the data and saves the changes to the database. It has the virtual method BeforeSaveEntity, which is called each time before saving the entity, and we will use it.
')
Since the article on security, I consider it necessary, just in case, to mention that the code from the article serves solely the purpose of showing some ways by which security mechanisms can be implemented and only for this purpose, in order not to complicate the project, a rather bad programming style is used, therefore using pieces This code in real projects is absolutely unacceptable.

Perhaps, let's go straight to the practice, and, as we go, let's analyze what and how. Let's start from the place where we stopped in the last article , for this you can download the project at this link . After downloading the project you need to build, so that NuGet will restore all the packages that I removed from the project to reduce the size, and you can proceed.

First you need to implement authentication. Let's make the simplest cookie-based authentication, for this we install the NuGet Microsoft ASP.NET Identity Owin package , Microsoft ASP.NET Web API 2.2 OWIN and Microsoft.Owin.Host.SystemWeb , since last time we didn’t use OWIN in the application, then we will create OWIN Startup class Startup.cs, and in it we register the standard route for WebApi controllers, set the authentication type to DefaultAuthenticationTypes.ApplicationCookie and use CamelCasePropertyNamesContractResolver to force WebApi to give us data to camelCase.
using System; using System.Threading.Tasks; using Microsoft.Owin; using Owin; using System.Web.Http; using Newtonsoft.Json.Serialization; using Microsoft.Owin.Security.Cookies; using Microsoft.AspNet.Identity; [assembly: OwinStartup(typeof(BreezeJsDemo.App_Start.Startup))] namespace BreezeJsDemo.App_Start { public class Startup { public void Configuration(IAppBuilder app) { HttpConfiguration config = new HttpConfiguration(); config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie }); config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); app.UseWebApi(config); } } } 

Now we will create the LoginController.cs controller.
 using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Http; using Microsoft.Owin; using Microsoft.Owin.Security; using System.Security.Claims; using Microsoft.AspNet.Identity; namespace BreezeJsDemo.Controllers { public class LoginController : ApiController { public class LoginViewModel { public string user { get; set; } public string role { get; set; } } public IHttpActionResult Post(LoginViewModel login) { var authenticationManager = HttpContext.Current.GetOwinContext().Authentication; if (authenticationManager.User.Identity.IsAuthenticated) { authenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie); } var claims = new Claim[] { new Claim( ClaimTypes.Name, login.user), new Claim( ClaimTypes.Role, login.role) }; var identity = new ClaimsIdentity(claims, DefaultAuthenticationTypes.ApplicationCookie); authenticationManager.SignIn(identity); return Ok(); } } } 

Here we use the Post method to take the username and role name, create a ClaimsIdentity based on them, and log in. In order not to complicate the example, we will not do neither checks, nor passwords, nor user bases, so to say:
-We are all gentlemen here, all believe in each other's words.

Now add the appropriate fields to the interface. First of all, you need to change a little /app/shoppingList/shoppingList.controller.js. We need the $ http service, so add it depending
 ... ShoppingListController.$inject = ['$scope', '$http', 'breeze']; function ShoppingListController($scope, $http, breeze) { ... 

And the login () function
 ... vm.login = login; ... function login() { $http.post('api/login', { user: vm.user, role: vm.role }); } ... 

Add username and role input fields to the /app/shoppingList/shoppingList.html markup, for example, up in the navbar
  <nav class="navbar navbar-default"> <ul class="navbar-nav nav"> <li ng-if="vm.hasChanges()"><a ng-click="vm.saveChanges()"><span class="glyphicon glyphicon-thumbs-up"></span>  </a></li> <li ng-if="vm.hasChanges()"><a ng-click="vm.rejectChanges()"><span class="glyphicon glyphicon-thumbs-down"></span>  </a></li> <li><a ng-click="vm.refreshData()"><span class="glyphicon glyphicon-refresh"></span> </a></li> </ul> <form class="navbar-form navbar-right"> <div class="form-group"> <input ng-model="vm.user" class="form-control" placeholder=" " /> <input ng-model="vm.role" class="form-control" placeholder="" /> </div> <button ng-click="vm.login()" class="btn btn-link"><span class="glyphicon glyphicon-user"></span> </button> </form> </nav> 

Now, when we can introduce ourselves to our application whoever we want, let's move on to the access attributes. Suppose we will distribute access rights using the following attributes: CanAddAttribute (gives the right to add a new record to the database), CanDeleteAttribute (delete right) and CanEditAttribute (right to change), and if you hang CanEditAttribute on the class properties, the user can change their values, and if you hang it on a class - the user will be able to change all its properties without exceptions. Of course, in a real project such a scheme would be extremely inconvenient and unviable, but to explain the idea of ​​this set would be quite enough.
  public class HasRightsAttribute: Attribute { public String User { get; set; } public String Role { get; set; } } [AttributeUsage(AttributeTargets.Class, AllowMultiple=true)] public class CanAddAttribute: HasRightsAttribute { } [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public class CanDeleteAttribute : HasRightsAttribute { } [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = true)] public class CanEditAttribute: HasRightsAttribute { } 

We distribute these attributes to our models, for example
  [CanEdit(User = "User")] [CanAdd(Role = "Role")] [CanDelete(Role = "Role2")] public class ListItem { public int Id { get; set; } public String Name { get; set; } [CanEdit(User = "User2", Role = "Role2")] public Boolean IsBought { get; set; } public int CategoryId { get; set; } public Category Category { get; set; } } 

And this is what it will mean:

And add something like this to the Category class.
  [CanEdit( User = "User")] [CanAdd( Role = "Role")] public class Category { public int Id { get; set; } public String Name { get; set; } public List<ListItem> ListItems { get; set; } } 

Now let's work on the main one, create a class SecureEFContextProvider - the successor of the EFContextProvider, which will implement access control
 using Breeze.ContextProvider; using Breeze.ContextProvider.EF6; using BreezeJsDemo.Classes.Attributes; using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Reflection; using System.Web.Http; using System.Net; using System.Security.Claims; using System.Data.Entity.Infrastructure; using System.Data.Entity.Core.Objects; using BreezeJsDemo.Model; namespace BreezeJsDemo.Classes { public class SecureEFContextProvider<T> : EFContextProvider<T> where T : class, new() { protected override bool BeforeSaveEntity(EntityInfo entityInfo) { var user = HttpContext.Current.GetOwinContext().Authentication.User; if (user.Identity.IsAuthenticated) { var userName = user.FindFirst(ClaimTypes.Name).Value; var role = user.FindFirst(ClaimTypes.Role).Value; var entityType = entityInfo.Entity.GetType(); switch (entityInfo.EntityState) { case EntityState.Added: //    CanAddAttribute      if (entityType.GetCustomAttributes<CanAddAttribute>().Any(x => x.Role == role || x.User == userName)) { return true; } break; case EntityState.Deleted: //    CanDeleteAttribute      if (entityType.GetCustomAttributes<CanDeleteAttribute>().Any(x => x.Role == role || x.User == userName)) { return true; } break; case EntityState.Modified: //    CanEditAttribute      if (entityType.GetCustomAttributes<CanEditAttribute>().Any(x => x.Role == role || x.User == userName)) { return true; } //    if (entityInfo.OriginalValuesMap.All(x => entityType .GetProperty(x.Key) //  CanEditAttribute      .GetCustomAttributes<CanEditAttribute>().Any(y => y.Role == role || y.User == userName))) { return true; } break; } } //   throw new HttpResponseException(HttpStatusCode.Forbidden); } } } 

In this class, we overloaded the BeforeSaveEntity method, EFContextProvider calls it before saving each entity. This place is provided by developers specifically to check what we are going to change, to validate changes or change some data before storing, for example, the date of the last change of the object. If the method returns false - the entity will not be saved, if an exception is thrown in the method, the entire batch of changes will be saved and the exception will be returned to the client. The base version of the method simply always returns true, so instead of calling it, you can write return true.

You can also overload the protected Dictionary <Type, List> BeforeSaveEntities (Dictionary <Type, List> saveMap) method, the entire save package immediately gets into it, you should also return the Dictionary <Type, List>, this will be the new save package.

These methods take as input objects of type EntityInfo, which contains data about the entity you want to save and the type of operation you want to perform, consider some of its properties.


First of all, in the method, we check if the user has passed authentication (user.Identity.IsAuthenticated), and if not, we prohibit saving. Next, we check the status of the entity and look for the corresponding attribute for the user name or its role, if there is one, we allow saving. If the status is EntityState.Modified and the user does not have rights to change the entire object, we look at the changed properties in OriginalValuesMap and look for the necessary attribute of the property, if there is no such property, we prohibit saving.

Considering that each entity falls into this method before being saved, it is also possible to implement the distinction not at the field level, but at the level of individual records. For example, to prevent User from deleting an entry with identifier 1. It is also possible, for example, instead of attributes, to store all access rights somewhere in the database so that you can change them in runtime.

Next, in DbController you need to replace EFContextProvider with our SecureEFContextProvider.
 ... private SecureEFContextProvider<ShoppingListDbContext> _contextProvider = new SecureEFContextProvider<ShoppingListDbContext>(); ... 

Now the preservation of each entity under our control. But if now try to make any changes - the user will think that everything went well, because we did not handle the error while saving. Let's make some small changes to the saveChanges method.
  function saveChanges() { manager.saveChanges().then(null, function (error) { if (error.status === 403) { manager.rejectChanges(); alert("   "); } }); } 

Now, if the server returns 403, the changes that did not succeed will be rolled back, and the user will see the message.

With preservation sorted out. Now let's talk about access to read certain data. The most logical and reliable method, of course, is to create a DTO for each entity you want to work with, make connections between them, then create a special DbContext for them, just to generate client metadata using EFContextProvider. In DbController make appropriate methods for each DTO. In general, everything is exactly the same as in our application, but in the BeforeSaveEntity method, it accepts DTO and already works with the real context and real entities, and then returns false from the method so that the DTO context does not try to save. In this case, the BeforeSaveEntities method is better, because the entire save package immediately gets into it, and, accordingly, you can save all changes at once in one sitting, then you need to return the empty Dictionary <Type, List> from the method so that the DTO context does not began to save anything.

With this approach, the client will not get any extra data, plus we do not disclose the scheme of our database. But, naturally, the amount of work on creating DTO and additional processing of their preservation also increases. Of course, it is difficult to scare someone with this, but why don't we even dream up a little more ...

First, we write the attribute, which will distribute the rights to read entities and properties
  [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = true)] public class CanReadAttribute: HasRightsAttribute { } 

Distribute attributes to classes
  [CanEdit(User = "User")] [CanAdd(Role = "Role")] [CanDelete(Role = "Role2")] [CanRead(User = "User", Role = "Role")] public class ListItem { [CanRead(Role = "Role2", User = "User2")] public int Id { get; set; } public String Name { get; set; } [CanEdit(User = "User2", Role = "Role2")] [CanRead(Role="Role2", User="User2")] public Boolean IsBought { get; set; } [CanRead(Role = "Role2", User = "User2")] public int CategoryId { get; set; } public Category Category { get; set; } } 

  [CanEdit(User = "User")] [CanAdd(Role = "Role")] [CanRead(User="User", Role="Role")] [CanRead(Role = "Role2", User = "User2")] public class Category { public int Id { get; set; } public String Name { get; set; } public List<ListItem> ListItems { get; set; } } 

The principle is the same if there is an attribute on the class - the user has the right to view all the properties, if not - then only those on which there is an attribute. We will hide data in the ObjectContext ObjectMaterialized event, for this we will slightly supplement the class SecureEFContextProvider
  public class SecureEFContextProvider<T> : EFContextProvider<T> where T : class, new() { public SecureEFContextProvider() { ObjectContext.ObjectMaterialized += ObjectContext_ObjectMaterialized; } private void ObjectContext_ObjectMaterialized(object sender, ObjectMaterializedEventArgs e) { var user = HttpContext.Current.GetOwinContext().Authentication.User; String userName = null; String role = null; if (user.Identity.IsAuthenticated) { userName = user.FindFirst(ClaimTypes.Name).Value; role = user.FindFirst(ClaimTypes.Role).Value; } var entityType = e.Entity.GetType(); //    CanReadAttribute      -      if (entityType.GetCustomAttributes<CanReadAttribute>().Any(x => x.Role == role || x.User == userName)) { return; } //  ,       var _forbiddenProperties = e.Entity.GetType().GetProperties() .Where(x => !x.GetCustomAttributes<CanReadAttribute>() .Any(y => y.Role == role || y.User == userName)); foreach (var property in _forbiddenProperties) { //       property.SetValue(e.Entity, null); } } 

Here we simply set the value of null to all properties that the user does not have access to. Now, if you run the project, you can see that an unauthorized user does not even have the right to read object keys, User User or the Role role has rights to view all properties, but User2 and Role2 cannot see the list item names. This is what we wanted. But I want to note, despite the fact that the user does not have access to the data directly in some properties of objects, the complete data model schema is completely known on the client.

Here, perhaps, and all that I wanted to talk about today. Just note that the user will be very discouraged, not knowing which fields he can edit and which ones will not, and User2 will be shocked that the list items have empty names. Therefore, next time we will consider working with metadata in the breeze and give our user visual clues about what rights he has in the application. The finished project can be downloaded from the link .

PS While writing an article I decided to write a library that will implement this access control using the attributes / fluent interface. If you have any ideas, tips and suggestions for functionality or implementation - you are welcome in the comments. How something more decent will be ready - I will post it on GitHub, in NuGet, and write here a tutorial.

Source: https://habr.com/ru/post/249417/


All Articles