VITA Authorization Framework

Contents

  1. Introduction
  2. Reference application - a simple online book store
  3. What we try to achive
  4. Clarification: security vs authentication vs authorization
  5. Authorization model - basic concepts
  6. AccessType enumeration
  7. Resources - EntityResource and EntityGroupResource classes
  8. Permissions: Resources + AccessType
  9. Activities
  10. User Roles - simple case
  11. Authorization Filters
  12. OperationContext
  13. Activity grants, filters and dynamic grants
  14. Runtime: associating users with authorization roles
  15. Using the secure session for data access
  16. Automatic filters in queries
  17. Web API controllers authorization
  18. Granting access by reference - GrantAccess attribute
  19. Explicitly inquiring about the permissions
  20. Creating AuthorizationFilters
  21. Authorization objects setup - general recommendations
  22. Final notes

Introduction

Any non-trivial application, especially if it is a business application, requires some level of authorization control - the access to data and functionality varies for different users, and this variability must be enforced by the application, so no privileged data is exposed to non-privileged users. Implementing these authorization requirements might be a big challenge.

VITA implements a complete Role-Based Access Control (RBAC) solution for defining and enforcing the data access rules throughout the application with instance- and property-level granularity.
It implements a robust, non-intrusive authorization layer that transparently works behind the scene and provides full data access control verification, with a minimal performance impact.

Reference application - a simple online book store

We start by introducing a reference application that we will use throughout this guide to illustrate the concepts and the implementation of the authorization-enabled solution. Using this sample application makes it easier to understand what kind of problem we are trying to solve.
Let's imagine we are tasked with creating an online book store. We have books, authors, publishers, users, book reviews, purchase orders and coupons. Users come to our site, browse books catalog, purchase books, and leave reviews on books they bought. Power users (employees of the book store) can manage the catalogs, create coupons, adjust puchase orders and moderate reviews. Book authors can also signup as users, and then edit some information about themselves and about the books they wrote.
We have the following user roles:
  1. WebVisitor - anonymous user. Can browse books, publishers, authors, reviews
  2. Customer - logged-in user. Can browse book catalog, create/edit reviews, buy books (create book orders), optionally using coupon codes (received in email), view/edit his/her orders
  3. Author - same as a Customer, plus: can update his own Bio in Author entity; can edit Abstract and Description properties of the books he wrote. These two activities are allowed only within the context of specially designed screens for authors.
  4. Book Editor - can create books, authors, but not publishers; cannot see any book orders or coupons.
  5. Customer Support - can view user information for customers and authors, can see customer orders.
  6. Store Manager - can create publishers, coupons, can adjust orders; can moderate reviews (delete any inappropriate entries).
What we have here is a simplified business application, in which the data access is strictly controlled by the current user roles and by the user's associations with the data. For authorization requirements like these, VITA provides an out-of-the-box solution integrated into the core framework.

The complete books store application model is available in the Vita.Samples.BookStore project. It includes the BooksAuthorizationHelper.cs file that contains the complete authorization setup. You can see the authorization in action by running the comprehensive code sample formatted as a unit test in AuthorizationTests.cs in Vita.UnitTests.Extended project.

What we try to achieve

Our goal is to have a completely transparent authorization subsystem, that works quietly behind the scene and verifies that every data access operation we do on behalf of the current user complies with authorization rules. We setup these rules at application startup - we explain the process in sections below. Then, in business logic code we simply open the data session providing an identity of the current user (UserID) and perform the operations according to application logic, without ever worrying if the user is in fact allowed to perform . If not - authorization will intercept and throw AccessDenied exception.
As an example, let's say we are implementing code for submitting/editing book reviews in our online book store:

  // User can create, update, delete reviews
  var secureSession = OpenSecureSession(dora); //Dora is current user
  var doraReview = secureSession.GetEntity<IBookReview>(doraReviewId);
  doraReview.Review += " (update: some more info)";
  secureSession.SaveChanges(); //Everything works fine

  ... 
  // Now Diego tries to update Dora's review (by forging PUT request)
  var secureSession = OpenSecureSession(diego); //Diego is user now
  // Reading Dora's review goes OK, users are allowed to READ other user's reviews
  var doraReview = secureSession.GetEntity<IBookReview>(doraReviewId);
  
  //BANG! AccessDenied is thrown, users cannot update other user's review
  doraReview.Review += " (update from Diego: disagree!!!)"; 

So the whole point is that authorization checks are completely hidden - they are automatic, all-inclusive and reliable, strictly enforcing the access rules configured at application startup. The programmer never has to worry about forgetting to do an authorization check in code. And the main application logic is not cluttered with numerous authorization checks.

Clarification: security vs authentication vs authorization

There are three closely related terms that are sometimes used as synonyms in certain context - security, authentication, authorization. You sometimes start reading a piece mentioning security in its title, but the main content is about authenticating users. This might be confusing, so without any claims to have the 'right' definitions of the terms, in the context of this guide we assume the following definitions:
  1. Authentication - a part of application responsible for managing user logins and passwords and providing the rest of the application with the information on the currently authenticated user.
  2. Security - an application aspect concerned with preventing any unauthorized access - by faking user identity, by elevating user privileges or any other evil techniques.
  3. Authorization - a sub-system in charge of verification that the current user has sufficient rights to access the particular data or functionality.
The main point here is to emphasize the responsibilities of each subsystem and to clearly outline what Authorization in particular is responsible for. Authorization is not concerned with the 'validity' of the current user (how he was verified) - it is a given and trusted entity provided by surrounding code. And authorization is not concerned with the validity of the command from the user - if it could have been fabricated by an intruder - this is the responsibility of the Security subsystem. VITA Authorization framework is dealing with the Authorization proper as just defined.
Note: we will still use the term 'secure' for some authorization-related concepts. The authorization-enabled entity session is ISecureSession - more appropriately it should be called 'IAuthorizationEnabledSession' - which is a bit too many long words, so I decided to use 'Secure', even if it is a bit misleading.

Authorization model - basic concepts

In this section we describe the authorization model - basic types and classes - which are used to express and codify the data access rules for an application. We will define Access types (operations like Read, Update), resources (sets of entity types), permissions, activities and user roles. Using these basic classes we will encode the authorization rules for our book store application. The resulting authorization objects (authorities) will be used to control the data access at runtime by the framework.
The following table lists the concepts implemented as classes that are involved in VITA authorization model:

Table 1. VITA Authorization classes
Concept/class Description
AccessType A flag enumeration defining operations on entities that are subject to authorization.
EntityResource A reference to an entity type with an optional set of properties that are included into the resource.
EntityGroupResource A container for a list of EntityResource objects.
EntityGroupPermission A combination of AccessType value and a reference to a list of group resources.
Activity A set of permissions combining the necessary permissions for a real-life activity at the functional module level.
ActivityGrant A link between user role and Activity with optional data filter. Associates activity permissions with the role for the data restricted by the data filter.
Role A named collection of activity grants - a complex set of activities grants representing some end-user job responsibility.
Authority A runtime container for all permissions granted to a user with a set of roles, re-organized and optimized for performance.
OperationContext A container for user-related information used by the authorization framework. Operation context is attached to an entity session.
AuthorizationFilter A container for a set of associations between a given user and entity instances. Contains a set of lambda expressions for resolving associations for entity instances (organized as a dictionary by entity type). During the resolution the result of the lambda is compared to some key value for the user in the OperationContext.
DynamicActivityGrant An extension of the ActivityGrant, with the ability to enable associated permissions temporarily, in the context of a certain user activity.
IEntityAccess An interface providing detailed information about user permissions to a given entity instance. Retrieved using the EntityHelper.GetEntityAccess method.
AuthorizationService A framework service managing basic authorization tasks. Authority cache is maintained by this service.


We will now review these concepts in more details while gradually building the authorization model for our book store.

AccessType enumeration

The AccessType enumeration (flagset) defines a set of operations that might be performed on an entity:

  [Flags]
  public enum AccessType {
    None,
    // Restricted read access. Can read in code, but user may not see the value in UI. 
    Peek = 1, 
    // Can read values and show it to the user in UI.
    ReadStrict = 1 << 1,
    // Can create new entities. 
    CreateStrict = 1 << 2,
    // Can update an entity or entities. 
    UpdateStrict = 1 << 3,
    //Can delete an entity or entities. 
    DeleteStrict = 1 << 4,
	
    // Can read values and show it to the user in UI.
    Read = ReadStrict | Peek,
    // Can create new and read existing entities.
    Create = CreateStrict | Read,
    // Can view and update an entity or entities.
    Update = UpdateStrict | Read,
    // Can view and delete an entity or entities.
    Delete = DeleteStrict | Read,
    // Full CRUD access.
    CRUD = Read | Create | Update | Delete, 
	
    // API access types
    ApiGet = 1 << 8,
    ApiPost = 1 << 9,
    ApiPut = 1 << 10,
    ApiDelete = 1 << 11,

  }
CRUD operations are represented by two members each - with and without 'Strict' suffix. Values with the suffix like CreateStrict is a basic, create-only action, while Create is a combination with Read flag. Usually if a user can create a data, he can see it, so Create is provided for convenience and it is expected to be used most of the time.
More interesting is a Peek value. VITA authorization model supports two distinct levels of read data access: Peek and ReadStrict. This split into two operation types is made to support the different ways the data may be used by the application code. There are cases when the code needs to read the data to use it internally for some calculations on behalf of the current user. But the data itself is not supposed to be available to the user in the UI. This is what a Peek access type is used for - code-only use.
As an example, the Coupons table in our book store will be available for Peek only to ordinary web visitors - so the system can lookup the coupon code entered by the user when he buys a book. But any attempt to show the coupons in the UI to a web visitor will be rejected - the Read permission is needed for this. We will see how it works in code later in this document.
The ReadStrict flag represents a pure read access, without 'peek' - which does not make much sense - so it is introduced just to define the Read action, distinguished from the Peek.
The four Api* values are used for setting up permissions to reach Web API controllers; these will be discussed in a separate section below.

Resources - EntityResource and EntityGroupResource classes

Resources are generalized references for entity types, optionally with a explicit subset of properties. The EntityResource class is a simple container for two pieces of information - an entity type, and an optional list of properties in this entity that are subject for authorization. EntityResource instances are grouped into sets under EntityGroupResource instances. It represents a group of entities that will be managed together, having identical permissions for AccessType. It is a matter of convenience to group related entities and then use the group as a single reference in setting up permissions.
The application code can use EntityGroupResource directly for combining entities, without creating EntityResource instances explicitly - the group container class has all methods necessary to setup a group. The following code defines some entity resources for the books store:

  var books = new EntityGroupResource("Books", typeof(IBook), 
                         typeof(IBookAuthor), typeof(IAuthor));
  var authorEditData = new EntityGroupResource("AuthorEditData");
  authorEditData.Add(typeof(IAuthor), "Bio");
  authorEditData.Add(typeof(IBook), "Description,Abstract");

All resource groups have descriptive names. Some resources, like 'books', refer to entity types as a whole. Others, like 'authorEditData', refer to subset of properties: IAuthor.Bio, IBook.Description, IBook.Abstract - these are the properties that an Author will be able to edit.

Permissions: Resources + AccessType

Permissions are simply tuples of resources (entity group resources) and access type (operations). They are specifications stating which actions (access types) are allowed for which entity types and individual properties.
The following code defines some permissions for our book store. 'books', 'publishers', 'orders' etc. are all entity resource objects defined previously.
  var browseBooks = new EntityGroupPermission("BrowseBooks", AccessType.Read, books, publishers);
  var createOrders = new EntityGroupPermission("CreateOrders", AccessType.CRUD, orders);
  var lookupCoupon = new EntityGroupPermission("LookupCoupon", AccessType.Peek, coupons);
  var useCoupon = new EntityGroupPermission("UseCoupon", AccessType.UpdateStrict, couponAppliedOn);
  var manageCoupons = new EntityGroupPermission("ManageCoupons", AccessType.CRUD, coupons);
Notice the lookupCoupon permission definition - it sets the Peek access on ICoupon entities. This permission will be granted to web visitors and it will allow the business logic code (running as the user) to lookup coupon in the database by the promotion code supplied by user buying the book. The Peek permission prevents the system from showing them to the user directly in UI.
Another permission for web visitor is 'useCoupon' - this allows the code to update the ICoupon.AppliedOn property which marks the coupon as used for purchase.

Activities

Having the permissions defined, we are ready to start building user roles with appropriate permissions. VITA Authorization framework in fact allows you to do this - add permissions directly to roles. However, the recommended arrangement is different. There is one more concept - an Activity that sits in-between permissions and user roles, and is the actual container for permissions. In this section we explain why this extra entity is needed, and how it is used.
Activity, in plain words, is a mini-role, a collection of permissions defined in a limited scope of application module or area. It is introduced to make it easier to manage permissions for large, enterprise-wide business applications, consisting of multiple modules. For such applications it becomes really difficult to assemble user roles from low-level permissions to particular entities (tables) from different modules. One problem is that the administrator who builds the role, need to know well the internals of all models involved - which is unrealistic for even medium-sized applications.
So application designers in the past few years came up with the concept of an Activity - a mini role, defined at module level, often directly by the developer of the module who knows all the details. The activity definitions for a module become a part of public module interface. Later, when the integrated software package is being configured at customer site, the administrator uses the activities to assemble the end user roles, matching users' responsibilities in the company. Note that the Activity concept is not 'invented' by the VITA framework - it had been known and discussed for years already in the development community.
VITA Authorization framework defines a class Activity that implements the concept. The following code defines some activities for the book store:
  var browsing = new Activity("Browsing", browseBooks, browseReviews);
  var shopping = new Activity("Shopping", createOrders, lookupCoupon, useCoupon);
  var editingByAuthor = new Activity("EditingByAuthor", editByAuthor);
At the basic level, the Activity is a named container for a set of permissions. Activities form a parent/child hierarchy, so an Activity can contain simpler activities, as well as permissions. In VITA Authorization framework an Activity is a bit more than just intermediary container for permissions - activities are involved in instance-level authorization and in dynamic permission granting. We will explain how it works in the following sections.

User Roles - simple case

The next level in the authorization model hierarchy is a Role - a container for activities that usually closely matches real-life user's job responsibilities in regards to the business application functionality. Roles support hierarchies, so we can compose roles from other simpler roles.
The following code creates a BookEditor role that is granted 'browsing' and 'bookEditing' activities for all books in the catalog:
  BookEditor = new Role("BookEditor", browsing, bookEditing);
Notice that BookEditor is a user role that is listed in our specification at the beginning of this document. We are at the point when we can define the Roles for the end users. Well, almost. The BookEditor role has a permission to browse/edit books in the entire catalog.
But for other roles, we must setup permissions involving specific instances - for example, a customer should be able to see only his book orders, and not the orders from other users. So permissions and activities should be granted conditionally - only for some entities that are 'connected' to the current user - like purchase orders are linked to the user who placed them. This concept is implemented in VITA using an AuthorizationFilter.
An Activity is added (granted) to a Role using an ActivityGrant - essentially a link connecting the Role and the Activity. This grant can optionally have an AuthorizationFilter object attached that can be used for restricting the activity grant only to those entities that pass the filter.*

Authorization Filters

AuthorizationFilter is an object that is capable of answering a simple question: given a user and a piece of data (as a data entity), is there an association between the two? For example, for the user 'JohnD' and purchase order #123 - does this order belongs to this user?
AuthorizationFilter is a dictionary of lambda expressions that evaluate the relationship between an entity and user. For our example with purchase orders the evaluating lambda is:
  (IPurchaseOrder po, Guid userId) => po.User.Id == userId; 
- where userId is the ID of the current user. The first parameter of lambda is always an entity that is under authorization check (that user tries to access to). The lambda can have up to four extra parameters which are values 'describing' the current user (ex: departmentId, userType, etc). The authorization engine injects these parameters automatically when evaluating lambdas against specific entities selected from the database. Where does it get these values? From an OperationContext instance linked to current secure session - more on this in the next section.
The following code create a data filter for filtering user-owned information (user entity and book orders).
    var userDataFilter = new AuthorizationFilter("UserData");
    userDataFilter.Add<IUser, Guid>((u, userId) => u.Id == userId);
    userDataFilter.Add<IBookOrder, Guid>((bo, userId) => bo.User.Id == userId);
    userDataFilter.Add<IBookOrderLine, Guid>((ol, userId) => ol.Order.User.Id == userId);
    userDataFilter.Add<IBookReview, Guid>((r, userId) => r.User.Id == userId);
The expressions added to the filter specify how to match entities IBookOrder, IBookOrderLine, IBookReview with current user identified by a user Id.
We need another filter for authors. Users who are also authors have special privileges - they are able to edit their own Bio, and update the Abstract and Description properties of the books they authored. So we need a filter that evaluates relation between the author/user and target entities IAuthor, IBook:
  var authorDataFilter = new AuthorizationFilter("AuthorData");
  authorDataFilter.Add<IAuthor, Guid>((a, userId) => a.User.Id == userId);
  authorDataFilter.Add<IBook, Guid>((b, userId) => (b.Authors.Any(a => a.User.Id == userId)));
Notice the use of expression 'a.User.Id' - we might have a problem here. Not all authors are users, so IAuthor.User property/column is nullable. Which means that if we evaluate this expression for a non-user author, we may get Null-reference exception. This actually will not happen. AuthorizationFilter is smart enough to recognize this particular situation when it prepares to compile the lambda expression into an executable delegate, so it rewrites the expression and injects the safe checks. The rewritten expression works correctly and returns false when 'author.User' property is null.
Now look at the lambda expression for a book entity - it simply checks that book's author list has an author with user Id matching the current user Id. This lambda is not very efficient, it causes reload of all authors for any book we check. We show it here for simplicity - the code in the sample BookStore app uses more efficient version that loads just one (or zero) entity for a given book.
We turn now to OperationContext.

OperationContext

The OperationContext is a container for a 'context' of the entity session, including all current user information. It is available through session.Context property. Internally it has a property User (as a UserInfo object), plus context.Values dictionary - set of name/value pairs holding any values related to the current set of operations and current user. The authorization system uses these values to evaluate the permissions to access objects at runtime.
OperationContext is created before any entity session is created. You might either create it explicitly, or it will be created automatically if you use a shortcut extension method EntityApp.OpenSession().
In Web applications scenario, the VITA-provided HTTP infrastructure classes create an instance of OperationContext and inject it into the API controller (either classic Web API ApiController, or SlimApiController based class defined in Vita.Web assembly). This Context instance is pre-filled with information about current user (user is authenticated). You then use this context instance in the controller methods to open secure sessions.
For desktop and console applications, you normally explicitly create an instance of OperationContext and keep it through in some global singleton field.
Note that in both cases you need to explicitly fill out the values that are used by authorization filter lambdas, except UserId - it is taken from the context.User.UserId property. For example, if lambdas use 'departmentId' parameter, you should set it in the operation context before you open any secure sessions:
  Context.Values["departmentId"] = GetDepartmentId(currentUser);

One of the several special parameter in lambdas is "userId" - system recognizes it as a special value, and reads it from the Context.User.UserId - so you don't have to explicitly place user ID into Values dictionary. There are some other special parameters - more on these in a section below.

Activity grants, filters and dynamic grants

Having defined the authorization data filters, we can now continue with definition of user roles for our books store:
  WebVisitor = new Role("WebVisitor");
  WebVisitor.Grant(browsing);
  WebVisitor.Grant(userDataFilter, shopping, reviewing, editingPersInfo);
  Author = new Role("Author"); 
  Author.ChildRoles.Add(WebVisitor);
  AuthorEditGrant = Author.GrantDynamic(authorDataFilter, editingByAuthor);
Here we start with creating the WebVisitor role, granting it the browsing activity. It is granted unconditionally, without any filtering restrictions.
Next, we grant three more activities (shopping, reviewing, editing personal info), but with an extra data restriction represented by the userDataFilter that we built previously. The link between an activity (shopping) and a role (WebVisitor) is an ActivityGrant - it contains an optional data filter that restricts the activity to the data that is in a certain relation to the current user. We used the Grant(...) method to create a static grant - the associated permissions are given indefinitely, without timing or scope restrictions.
For the Author role, we first add a child role WebVisitor - so an author can do anything the web visitor can do. Then we use a GrantDynamic method to grant the editingByAuthor activity. We save the returned instance of the DynamicActivityGrant in a static field.
The dynamic grant object implements a general concept known as a Purpose-aware RBAC. The idea of the purpose-aware authorization is to enable permissions to access a resource in a limited scope, for a certain purpose. For example, an employee can access customer's email for payment reminder purpose from a special UI form, but not for sending a promotion email in marketing campaign (some other special UI form). VITA authorization framework implements such restrictions through dynamic grants. The permissions associated with the dynamic grant are enabled for a limited scope (timespan) of a certain operation, and they are disabled when the operation is over.
The usage pattern involves the Execute method, which activates the granted permissions and returns a disposable token. This token disables the permissions when it is disposed. The following code illustrates the flow:
  var secureSession = OpenSecureSession(johnTheAuthor); // helper method
  using (BooksAuthorization.AuthorEditGrant.Execute(secureSession.Context)) {
    var csBook = secureSession.GetEntity<IBook>(csBookId);
    csBook.Description = "New description";
    secureSession.SaveChanges();
  }
Updating the Description property is allowed in this context, as the AuthorEditGrant is enabled. When we exit the 'using' block, the token returned by the execute method is disposed, and the disposing method disables the permission. Now if in some other place the application code tries to update the book's Description property with Author as a user and without calling the grant.Execute method, the authorization framework would throw the AccessDenied exception.

Runtime: associating users with authorization roles

Authorization service does not have a predefined entity for a User. All it assumes is that Users in the application are identified by IDs. This is done in this way because the way User object/entity is defined would differ between applications, so Authorization service does not impose any restrictions on they way you define it. The only thing it expects is that at a certain moment your code will provide a set of Roles (Role class instances) for a given user, and authorization service will do the rest - compile role list into an Authority (role set optimized for runtime use), stick it into secure session and start watching the data access operations.
The sequence of events is the following. Your code creates an instance of OperationContext using the current user information (as a UserInfo object). Then it calls the extension method OperationContext.OpenSecureSession(). The method calls authorization service and asks it to prepare an Authority - a compiled and optimized table of permissions for the current user. The authorization runtime now has to know what Roles are assigned to the user. It calls the virtual method EntityApp.GetUserRoles(userInfo) - which you should override and return the list of authorization roles for the current user. Note that one of the possibilities is that the user is anonymous - he's not registered in the system at all. For the anonymous user we still need to provide a set of roles to properly allow/restrict the access to data.
Let's go back to our book store sample. We define an IUser entity with a UserType property which identifies user as Customer/Author/Editor or Admin. For each of the user types (plus anonymous user) we setup a list of authorization roles and save these lists in static fields in AuthorizationHelper static class. We also define a GetRoles(UserType) static method that returns the appropriate list for user type. Then we put the following code into the EntityApp.GetUserRoles override:
   public override IList<Role> GetUserRoles(UserInfo user) {
      switch(user.Kind) {
        case UserKind.Anonymous:
          var roles = new List<Role>(); 
          roles.Add(BooksAuthorizationHelper.AnonymousUser);
          return; 
        case UserKind.AuthenticatedUser:
          var session = e.EntityStore.OpenSystemSession();
          var iUser = session.GetEntity<IUser>(e.User.UserId); 
          return BooksAuthorizationHelper.GetRoles(iUser.Type);
      }
      return new List<Role>(); //should never happen
    }
We have to query the database to retrieve the IUser entity by user id. Note that this will not happen every time we open a secure session. Compiled role sets (Authority objects) are cached by authorization service (by User ID as a key), so this happens only once, right after user login.
Now we are ready to open a secure session:
  var secureSession = operationContext.OpenSecureSession(); 
The returned SecureSession object has inside all information about the current user and his permissions. It will evaluate all data operations against the data that the code performs on behalf of the user, and it will throw an exception as soon as its tries to step outside the boundaries.

Using the secure session for data access

ISecureSession is an interface representing a session with enabled authorization checks. For most cases, its use is not different from a regular, non-secure session. The only difference is that the underlying framework code watches the data access operations and throws AccessDenied exception whenever the code breaks the permissions. Look at the following code executing as user 'dora': it tries to update Dora's own information (users are allowed to update their own info); then it tries to update other user's info - and the system will interrupt with AccessDenied exception:
  var secureSession = OpenSecureSession(dora);
  var doraRec = secureSession.GetEntity<IUser>(dora.Id);
  doraRec.DisplayName = "Dora the Explorer"; 
  secureSession.SaveChanges(); // Success!
  var otherUserRec = secureSession.GetEntity<IUser>(otherUserId); // throws AccessDenied
When it comes to querying entities, authorization allows GetEntity call on entity if either the entity or at least one its property is available for Read/Peek access. If only some properties are available for read, the framework would return the entity, but will 'deny access' (see below) as soon as we try to read the property that is not allowed.
There are two options on ISecureSession interface that provide an extra control over authorization checks:
  • DenyReadAction - specifies how to handle the situation when read access is denied. There are two options: DenyReadActionType.Throw - system throws AccessDenied, like in the code above; DenyReadActionType.Filter - system returns null instead of requested entity (or default value for the property if we are reading property).
  • RequireReadAccess - specifies what read access level to require on reads. Available options are ReadAccessLevel.Peek and ReadAccessLevel.Read. The peek option should be used when the application code needs the value for internal use only, and Read should be set when the value is about to be shown in the UI. Remember that when we specify 'read' permissions on entities, we have two levels in AccessType enumeration - Peek for internal use and Read for showing in the UI.
This actually sums it up - open a secure session, set its extra options if necessary, and start performing data access operations, just like you would do with regular session. Authorization code works in the background and verifies every single operation. See the extensive working demo of authorization for a sample book store in AuthorizationTests.cs file in the BooksSample unit tests project.

Automatic filters in queries

Authorization checking works mostly on the client side, over entities (c# objects) delivered from the database. A reasonable assumption is that regular business logic and UI arrangements open access only to those data that the user is allowed to access.
However, there is sometimes a need for generalized data search method that serves several kinds of users. Usually search results are 'paged', so only limited number of 'top' rows are delivered from the database. If you setup the secure session to filter out 'disallowed' rows (instead of throwing exception), then your code may end up receiving less than page-size set of rows, of even an empty set - because all of the data was filtered out as inaccessible.
Wouldn't it be nice to have authorization predicates directly embedded into the search query, so that all of the delivered rows are allowed for the current user? Yes, and authorization rules provide such an option.
The authorization data filters we had discussed before were setup to run against entities delivered from the database. Every lambda appearing in filter setup is formulated in terms of expressions over entities. What we did not mention is that the method adding a filtering lambda has an extra optional parameter of type FilterUse:
  [Flags]
  public enum FilterUse {
    None = 0,
    Query = 1, // OK to use in Linq/SQL queries
    Entities = 1 << 1, //Use on entities 
    All = Query | Entities,
  }
  // Add method declaration:
  public void Add<TEntity>(Expression<Func<TEntity, bool>> lambda, FilterUse filterUse = FilterUse.Entities) { ...
We used the the Add method with default filterUse value, so all our lambdas were for running over entities only. But if you use explicit value with a Query flag, then extra magic will happen. Any LINQ query against the entity will be automatically injected with an extra WHERE condition that is translated from the lambda. As a result, even free-form search queries will be automatically restricted only to data that the current user can see. ANY LINQ query within the context of the user will have this extra restriction.
Note that not any expression is translatable into SQL, so you must be careful to not use things like custom functions (local methods) inside the expressions. To help with the situation, the authorization filter allows you to add different expressions for FilterUse.Entities and .Queries values. The authorization filter is in fact a combination of two dictionaries that keep separate versions of expressions for entity types. You can add one expression for both uses in one call, or you can add two different expressions for each use type with two call to Add.
What will happen if a user is assigned two or more roles, and each has it's own query filter for a given entity? Then both filters are automatically combined using OR operator. As an example, look at CustomerSupport role in the sample BookStore app. CustomerSupport users are allowed to access user records ONLY for user who are either customers or authors (not empoyees). The authorization setup creates two data filters, and final CustomerSupport role combines permissions added through two different filters. As a result, when customer support person queries Users table, the query automatically includes OR-ed condition from both filters:
  SELECT "Id", "UserName", "UserNameHash", "DisplayName", "Type", "IsActive"
    FROM "books"."User"
    WHERE ("Type" = 1 OR "Type" = 2) -- Customer or Author          


Granting access by reference - GrantAccess attribute

There are situations when user is granted access to information based solely on the fact that it is referenced by some other information already visible to the user.
Example: In our Book store application users can post reviews that any other user or even anonymous web visitor can read. In general, users are not allowed to access other users' information. However, when user posts a book review, it becomes viewable by any site visitor, and the name of the reviewer (IUser.DisplayName) should be shown as well. Essentially the reviewer gives up his privacy to some extent when he posts the review, and allows others to 'know' his display name.
To make this work using the techniques described in previous sections we have to grant READ permission on the IUser.DisplayName property to any user. This seems to be granting unnecessary wide permissions, to access all users, not only limited set that posted reviews.
VITA provides a simple solution for this situation. Instead of granting permissions using resources, permissions and roles objects as described in previous sections, all you need to do is add an attribute to User property of the IBookReview entity:

[Entity]
public interfaces IBookReview {
  ... 
  [GrantAccess("DisplayName")]
  IUser User { get; set; }
} 
The attribute grants a READ permission for DisplayName property of the target user entity (particular instance), as long as the current user has a permission to access the holding review entity. The attribute argument is optional comma-delimited list of property names. If you do not provide this parameter, the access to all properties is granted. The second optional parameter is access type that is granted, the default value is READ.
Currently the GrantAccess attribute can be used only on reference-type entities. In the future, similar functionality might be made available for list-type properties, like book.Authors.

Explicitly inquiring about the permissions

The authorization subsystem mostly works silently behind the scene, and application code does not use it directly after creating secure session. However, there are cases when the application code needs to know about permissions for a piece of data. For example: an application is showing a list of documents on a page, and user is allowed to edit some of them - for these documents there must be an 'Edit' link next to the document title. The page-generating code needs to know for which documents to inject the link.
In cases like these the code can obtain an authorization descriptor - an instance of the IEntityAccess interface for the object in question:
  IEntityAccess docAccess = EntityHelper.GetEntityAccess(docEntity);
  if (docAccess.CanUpdate()) {
    //inject Edit link
  }
The access descriptor object provides numerous methods for checking permissions to read/update/delete objects, including permission to read or update particular properties.
The GetEntityAccess method is used when inquiring permissions on particular instance. For cases when the code needs permissions on entity type (ex: permission to create some entity), use the method ISecureSession.IsAccessAllowed<TEntity>(accessType).

Web API controllers authorization

The authorization functionality discussed so far was about verifying access permissions at code/data boundary, so to speak. Additionally, the authorization framework allows you to restrict the access at UI/server logic boundary, in the form of permissions to call Api controllers. This is possible only for so-called Slim API controllers - plain classes not dependent on ASP.NET Web API and managed by VITA-provided routing mechanism.
The application model we have in mind here is a Single-Page Application (SPA) that uses REST-ful data interfaces to the server to access the data. On the server side the calls are handled by API controllers. Users of the application have varying level of permissions to access UI pages and underlying data, which in fact means that certain server-side controllers should be callable only if the user is in a certain role. If somehow user manages to send a data request to a controller responsible for data set not allowed for a user (due to program bug or malicious hacking) - the request should be rejected outright, without even invoking the controller. Note that even if the application passes the request to the controller, the authorization subsystem will intercept at some point, when code tries to access the data. But it is definitely better to have such request cut off as early, at request routing stage. VITA authorization framework allows you to do just this - setup controller-access permissions and add them to user roles.
The controller class (Slim API controller) should be decorated with the Secured attribute, marking the controller as available only to those users that are explicitly granted access to it in the authorization rules setup.
The access permission is defined by the ObjectAccessPermission class, which accepts type of access (set of HTTP method values), and type of controller(s):
    var callAdminController = new ObjectAccessPermission(
	      "CallLoginAdminController", AccessType.ApiAll, 
		  typeof(LoginAdministrationController));
    LoginAdministrator.Grant(callAdminController);
The AccessType flag enumeration has a number of values with the 'Api' prefix matching common HTTP methods:: ApiGet, ApiPost, ApiPut, ApiDelete. The ApiAll value is a combination of all four methods.
Note that API access permissions are not very granular - they do not provide any 'row-level' granularity, and access to the controller is granted or not for all data, regardless of its connection to the current user. The only detail level is HTTP method, so you can distinguish GET (reading data), and other methods that result in data modification.

Creating AuthorizationFilters

In this section we would like to provide some details about AuthorizationFilter class, and give some information about constructing filters in your code.
Internally the AuthorizationFilter contains two dictionaries keyed by the entity type: one for lambda expressions to be used over entities loaded from the database, and another for expressions to be injected into the LINQ queries. You add the filter expressions to one or both dictionaries using one of the overloads of Add method. The overloads differ in number of extra parameters in lambda expressions. The first parameter is always a type parameter specifying the entity type. Other parameters (zero to four) are optional, and are values automatically extracted from the current OperationContext instance.
The Add method is generic and you must explicitly specify all type arguments for the specific overload. For all overloads the lambda is the first parameter, and the second optional parameter (of type FilterUse, flag enumeration) specifies the use of the lambda. Depending on the flags set in the parameter value the lambda predicate (always returning bool) is added to one or both internal dictionaries. You can add a single lambda for both uses, or you can add different lambdas for the same entity type for different uses.
The extra parameters (other than the entity under evaluation) are retrieved from the OperationContext attached to the secure session which is used for data operation. The general rule is that the parameter value is retrieved by name from the context.Values dictionary (case insensitive), but for certain types and names the values the process is different:
  1. For parameter type IEntitySession, ISecureSession the injected value is an entity session itself.
  2. For parameter type OperationContext, the context itself is provided as a parameter
  3. For parameter named 'userId' (case does not matter), the value is retrieved from the context.User.UserId property - it always contains the ID (GUID) of the current user.
  4. For a parameter named 'altuserid' (case insensitive) the value (int) is retrieved from the context.User.AltUserId property. This property is used when application uses integer primary key for Users table.
Examples:
    // The value of adjustedOrderId is retrieved from the context.Values
	// dictionary using 'adjustedOrderId' as a key. The expression 
    // will be used for filtering loaded entities only (not SQL queries)
    adjustOrderFilter.Add<IBookOrder, Guid>(
       (bo, adjustedOrderId) => bo.Id == adjustedOrderId);
    // The value for userId will be retreived from context.User.UserId property
    userDataFilter.Add<IBookReview, Guid>((r, userId) => r.User.Id == userId);
    // The value for ctx parameter will be the operation context itself
    authorDataFilter.Add<IBook, OperationContext, Guid>(
	  (b, ctx, userId) => ctx.Exists<IBookAuthor>(
           ba => ba.Author.User.Id == userId && ba.Book.Id == b.Id));
    // The expression will be used both for queries (injected as WHERE) 
    //  and for filtering entities on the client
    custSupportUserFilter.Add<IUser>(u => u.Type == UserType.Customer,
                   FilterUse.Entities | FilterUse.Query); 

Authorization objects setup - general recommendations

Data entities in VITA application are organized in entity modules - packages of functionality around a set of related tables/entities, business logic, services and APIs. The entity modules are then combined into an EntityApp object. So the authorization configuration should follow the same two-layer structure. At the lower module level, the entity resources, permissions, filters, activities and even some roles should be defined in module-scoped authorization containers. At the higher entity app level the user application roles should be built using module-level activities and roles. The purpose of this two-level approach is to hide module-specific authorization details into module-level authorization objects/activities, like Book Browsing Activity, and allow combining these activities at higher app level into app-scope roles.
At the module level, for each entity module:
  • Create a container class for module authorization objects, for ex. BooksAuthorization.cs. Define a public property on you custom entity module that holds an instance of this class, ex: BooksModule.Authorization. Create an instance of the container in the entity module constructor.
  • In the authorization container class declare public properties for module-scoped activites, filters, roles. Build these objects in the public constructor of the class. Create and expose permissions to call API controllers if your module defines any.
At the application level:
  • Create a container for top-level user roles, define roles as properties, build roles from activities and filters defined at module level in entity modules you use. To reach a module and its authorization container, you can use EntityApp.GetModule(moduleType) method.
  • Define a method that associates a give user (who just logged in) with authorization roles. For this override the EntityApp.GetUserRoles(user) method and return a list of user roles for a given user - a user entity or some property of your custom user object identifying the user 'privilege' type in your application. See an example of this method in the BooksAuthorization.cs file in the Books sample project.
In your business logic code:
  • Use the OperationContext.OpenSecureSession() method to create a secure session. Make sure that OperationContext.User property is initialized with current user's information. For Web API controllers derived from SlimApiController class the Context property is already properly initialized when controller is created by the system.
See BookStore sample application for an example of the complete authorization setup.

Final notes

Cooperative nature of interaction.
VITA authorization is cooperative in nature. What it means is that it behaves as a trusted and friendly partner to the business code that calls it and uses its services. The premise is that the business logic (higher level) code can always bypass authorization checks if it needs to - it can just use a regular, non-secure entity session to access the data. So when it goes through authorization system, it asks for help: "While I'm doing some stuff for THIS user, please watch all my data access operations and throw AccessDenied if we break the boundaries; so I'm free to concentrate on application logic without worrying about permissions.". Compare this arrangement with a different model of Code Access Security in .NET, which is fundamentally about being suspicious of the caller code itself - and verifying that it has enough permissions to make a call.

Relying on the business flow is not enough for proper authorization
One might think that obsessive verification of every data access action is not neccessary. The reasoning might go like this: "Look, in our app the user has access only to the documents he actually owns; like in 'My Orders' page, we show him only links to his orders, so he can never reach an order from the other user. Therefore verifying access rights on the order details page is not necessary." This might be true for a desktop application, in which all links between UI and data storage are hidden inside the application code (binaries). But for Web applications, this does not hold. There is a network protocol between the UI (web page) and application logic, so it is relatively easy to fabricate a request to get a view of any potentially visible piece of data. The application logic must run authorization checks for each request from the user, even if it is supposedly coming from a page that was built with authorization checks.
And using HTTPS does not solve the problem - a legitimate, logged in user connected over HTTPS can manually fabricate a request to the data that he's not supposed to see - and only proper authorization check can prevent this from happening.
Authorization checks are therefore crucial for preventing 'hijacking' the link between UI and the core server-side business logic.


Last edited Dec 1, 2015 at 4:38 AM by rivantsov, version 60

Comments

No comments yet.