
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:
- Role users have the right to add ListItem objects to the database.
- users with the role of "Role2" can delete them
- a user named “User” can change the values of any of his fields
- users with the role of "Role2" can change the value of the IsBought field
- users named "User2" can change the value of the IsBought field
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:
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.
- ContextProvider ContextProvider - link to ContextProvider
- Object Entity - directly saved entity itself as a .NET object, with property values that came from the client in the package
- EntityState EntityState - object status (Added, Modified, Deleted)
- Dictionary <String, Object> OriginalValuesMap - the original property values, before saving. The breeze will change in the database the values of only those fields whose names are the keys of this dictionary, while not touching the rest of the entry. And this is the data that came from the client in the package of changes, that is, this data cannot be trusted . The breeze itself does not pay attention to the values of the properties that are stored there (with the exception of the fields that are used for concurrency check), it is only important to have a key with the name of the variable field in the dictionary. That is, if you, for example, want to change the EditDate property on the server side, which is not changed on the client, you will first need to change its value in Entity, and then add the “EditDate” key in OriginalValuesMap with any value, for example, null . And vice versa, if you do not want to change the value of any field that the client wanted to change - you need to delete the corresponding key from the OriginalValuesMap.
- bool ForceUpdate - if set to true - the breeze will update the values of all fields of the entity, in spite of the contents of the OriginalValuesMap, defaults to false
- Dictionary <String, Object> UnmappedValuesMap - values of other properties of the entity that came with the save package in json, but not included in your model.
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();
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.