VITA Authorization Framework
Contents
- Introduction
- Reference application - a simple online book store
- What we try to achive
- Clarification: security vs authentication vs authorization
- Authorization model - basic concepts
- AccessType enumeration
- Resources - EntityResource and EntityGroupResource classes
- Permissions: Resources + AccessType
- Activities
- User Roles - simple case
- Authorization Filters
- OperationContext
- Activity grants, filters and dynamic grants
- Runtime: associating users with authorization roles
- Using the secure session for data access
- Automatic filters in queries
- Web API controllers authorization
- Granting access by reference - GrantAccess attribute
- Explicitly inquiring about the permissions
- Creating AuthorizationFilters
- Authorization objects setup - general recommendations
- 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:
- WebVisitor - anonymous user. Can browse books, publishers, authors, reviews
- 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
- 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.
- Book Editor - can create books, authors, but not publishers; cannot see any book orders or coupons.
- Customer Support - can view user information for customers and authors, can see customer orders.
- 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:
- 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.
- Security - an application aspect concerned with preventing any unauthorized access - by faking user identity, by elevating user privileges or any other evil techniques.
- 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:
- For parameter type IEntitySession, ISecureSession the injected value is an entity session itself.
- For parameter type OperationContext, the context itself is provided as a parameter
- 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.
- 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 authorizationOne 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.