To build a secure application, it's not enough to only authenticate our users: we must still authorize their access to data and services.
Your application may support users with different roles - for example 'Admin' users may have full privileges, while 'HR' users may only work with information in their department. But you must also authorize against rogue or malicious users, since in most environments you must assume that either the client or the transport can be compromised.
DevForce provides role-based security authorization features to help.
Before we dive into a discussion of when and how authorization can be used, let's briefly review the IPrincipal/IIdentity means of providing role-based authorization. Once logged in, by default a UserBase (or some custom IPrincipal implementation) is always available to all client and server code. On the client, the Thread.CurrentPrincipal is set to the logged in user (except in Silverlight), and the EntityManager.Principal property will return the logged in user. On the server, the Thread.CurrentPrincipal is also set for the calling user, and all methods and base classes provide either a Principal argument or property.
Having the principal always available means that both declarative and programmatic authorization can be performed. We discuss the attributes available for declarative authorization below, but it's also very easy to programmatically check authorization. For example, a query or save could include a simple check such as the following:
C# | bool isAuthorized = Principal.IsInRole("admin"); |
VB | Boolean isAuthorized = Principal.IsInRole("admin") |
When you created your entity model, you may have noticed in the EDM Designer that each entity type has DevForce properties that govern the ability of the client to query and save:
The CanQuery and CanSave properties translate to ClientCanQuery and ClientCanSave attributes on the generated Entity class. The "Allow includes" and "Allow projects" properties combine to determine a ClientQueryPermissions attribute.
Suppose we kept the default values for the two "Allow ..." properties and disabled (made false) the client's ability to query or save this type. The generated class would look like this:
C# | [IbEm.ClientCanQuery(false)] [IbEm.ClientCanSave(false)] public partial class OrderDetail : IbEm.Entity {} |
VB | <IbEm.ClientCanQuery(False), IbEm.ClientCanSave(False)> Partial Public Class OrderDetail Inherits IbEm.Entity End Class |
Turning off all direct query access may seem a bit draconian but you may have types that should really only be accessible on the server. Turning off all direct save access can be particularly useful, since many models have quite a bit of read-only data.
Why would you set CanQuery or CanSave to true? To override the default, which will be determined by the implementation of the "interceptor", EntityServerQueryInterceptor or EntityServerSaveInterceptor. (We'll discuss the interceptors in more detail below.) The default implementations of the interceptors allow all queries and saves, but if you've implemented custom interceptors you might find it useful to disable all access by default, and enable it only for entity types as needed.
Remember that each entity is defined as a partial class, making it easy to specify these attributes with code rather than in the designer. In the partial class you can be more particular and assign the permissions by role as seen in this example.
C# | [ClientCanQuery(AuthorizeRolesMode.Any, "Admin", "Sales")] public partial class OrderDetail : IbEm.Entity {} |
VB | <ClientCanQuery(AuthorizeRolesMode.Any, "Admin", "Sales")> Partial Public Class OrderDetail Inherits IbEm.Entity End Class |
You may also decorate your entities with the RequiresAuthentication and RequiresRoles attributes. The default interceptors, when authorizing a query or save, will check these attributes before checking the ClientCanQuery and ClientCanSave attributes. RequiresAuthentication can be used to ensure that a guest user does not have any access to the entity, while RequiresRoles functions similarly to ClientCanQuery and ClientCanSave when used to require the user be a member of all roles specified.
C# | [RequiresAuthentication] public partial class OrderDetail : IbEm.Entity {} |
VB | <RequiresAuthentication> Partial Public Class OrderDetail Inherits IbEm.Entity End Class |
A separate topic devoted to securing the query with attributes covers this approach in greater detail.
The EntityServerQueryInterceptor, discussed in depth in the query life cycle topic, is responsible for authorizing data retrieval. The interceptor allows you to authorize query access, add additional filters to a query, and to authorize the query results.
As we saw above, the default interceptor will authorize every query using the authorization attributes decorating the entity. To customize this behavior, and add additional authorization, you'll need to create a custom interceptor sub-classing the EntityServerQueryInterceptor.
In your custom interceptor you'll have a chance to:
Through a custom interceptor you can also perform additional logging to audit user access and capture error details.
The EntityServerSaveInterceptor is responsible for authorizing modifications to data before they are committed to the data source. The interceptor is discussed in depth in the save life cycle topic.
As we saw above in the discussion of entity-level authorization, the default interceptor will authorize every save using the authorization attributes decorating the entity. To customize this behavior, and add additional authorization, you'll need to create a custom interceptor sub-classing the EntityServerSaveInterceptor.
In your custom interceptor you'll have a chance to:
Through a custom interceptor you can also perform additional logging to audit user access and capture error details.
You might need to authorize user access to specific properties on an entity. Maybe all your users have read access to the Employee.Name property, but only users with the "Admin" role can modify it. There are several techniques within DevForce to control field-level (property-level) access to data.
C# | protected override bool ExecuteQuery() { bool ok = base.ExecuteQuery(); if (ok) { if (!this.Principal.IsInRole("Admin")) { if (Query.ElementType == typeof(Customer)) { foreach (var customer in this.QueriedEntities.Cast<Customer>()) { customer.CreditLimit = "***"; } } } } return ok; } |
VB | Protected Overrides Function ExecuteQuery() As Boolean Dim ok As Boolean = MyBase.ExecuteQuery() If ok Then If Not Me.Principal.IsInRole("Admin") Then If Query.ElementType = GetType(Customer) Then For Each customer As var In Me.QueriedEntities.Cast(Of Customer)() customer.CreditLimit = "***" Next End If End If End If Return ok End Function |
You can and should add authorization checks to your remote server methods too. The RequiresRoles and RequiresAuthentication attributes can be used on remote methods, and you can add programmatic authorization. Let's look at a sample method:
C# | [AllowRpc, RequiresRoles("admin")] public static Object GetNumberOfOrders(IPrincipal principal, EntityManager entityManager, params Object[] args) { ... } |
VB | <AllowRpc(), RequiresRoles("admin")> _ Public Shared Function GetNumberOfOrders(ByVal principal As IPrincipal, ByVal entityManager As EntityManager, ByVal ParamArray args As Object()) As Object ... End Function |
C# | [AllowRpc] public static Object GetNumberOfOrders(IPrincipal principal, EntityManager entityManager, params Object[] args) { if (!principal.IsInRole("admin")) { throw new PersistenceSecurityException("Access denied"); } .. } |
VB | <AllowRpc()> _ Public Shared Function GetNumberOfOrders(principal As IPrincipal, entityManager As EntityManager, ParamArray args As Object()) As Object If Not principal.IsInRole("admin") Then Throw New PersistenceSecurityException("Access denied") End If ... End Function |
Both declarative and programmatic authorization can be performed with POCO types.
All POCO CRUD methods in a service provider class - for query, insert, update and delete - can be decorated with the RequiresRoles or RequiresAuthentication attributes to control access to the method. You can also check the Thread.CurrentPrincipal directly to add programmatic authorization checks to these methods.
If using a custom EntityServerPocoSaveAdapter, you can add programmatic authorization to the BeforeSave, InsertEntity, UpdateEntity and DeleteEntity methods.
See the POCO topic for more information.