It's tempting to design a Business Layer that accepts User identity information and attempts to authenticate and authorize the user. However, security is a cross-cutting concern, and as such, should not be intertwined with your business logic.
The strategies presented here will extract the authorization component from the business logic and embed it as a separate concern within the Business Layer. I'm going to ignore the authentication function, and focus on determining what authority an authenticated user has to call functions in the Business Layer.
Two-step Business Layer Factory
try
{
using(IFactory factory = AuthenticatedFactory.New(user, pw))
{
IOrderProcessing processing = factory.CreateOrderProcessing();
return processing;
// or
// container.RegisterInstance<IOrderProcessing>(processing);
}
}
catch (AuthenticationError e) { }
catch (AuthorizationError e) { }
finally
{
// clean up
}
The authentication is done in the AuthenticationFactory. If the AuthenticationFactory has dependencies on a data layer or LDAP provider, you can trade in the static New method approach for a dependency injection approach:
public class AuthenticatedFactory
{
public AuthenticatedFactory(IDataLayer dataLayer)
{
DataLayer = dataLayer;
}
// more code
}
Get your factory this way:
using (IFactory factory = container.Resolve<AuthenticatedFactory>())
{
factory.User = user;
factory.Password = pw;
// get the business layer
}
Fine-grained Authorization using Sub-classes
One strategy for the Business Layer is to have coarse-grained authorization to the BusinessLayer. The user is either allowed to access that particular Business Layer, or an AuthorizationError is thrown. To make the access more fine-grained, the IOrderProcessing class that is returned can be tailored to the privileges that the authenticated user has. In either case, the factory method CreateOrderProcessing with determine the access level for the user.
For example suppose the IOrderProcessing interface offers three methods: LookupOrder(), EnterOrder() and ApproveOrder(). There are three levels of access: managers can access all three, clerks can access the first two, and salespeople can only access LookupOrder(). This can be accommodated by four classes and the factory determines which to return to the application.
public class NoAccessOrderProcessing : IOrderProcessing
{
public virtual Order LookupOrder() { throw new AuthenticationError(); }
public virtual void EnterOrder(Order o) { throw new AuthenticationError(); }
public virtual void ApproveOrder(Order o) { throw new AuthenticationError(); }
}
public class SalespersonOrderProcessing : NoAccessOrderProcess
{
public override Order LookupOrder() { /* code here */ }
}
public class ClerkOrderProcessing : SalespersonOrderProcessing
{
public override void EnterOrder(Order o) { /* code here */ }
}
public class ManagerOrderProcessing : ClerkOrderProcessing
{
public override void ApproveOrder(Order o) { /* code here */ }
}
This approach has the advantage of removing the authorization logic from the business logic, but it embeds the authorization logic in your class hierarchy. The designer of the business logic will need to be aware of the authorization model at design-time. However, if you know that the functions permitted to each role are going to be static, this approach has merit.
Single business class with Static-bound delegates
An alternate approach uses indirection through delegates. This strategy will move the logic entirely out of the class hierarchy and into the Factory class. The public methods on the business layer will call delegates that are, by default, set to the methods that perform the actual business logic. The Factory can disable certain methods by setting the delegates to null prior to handing the business layer object to the application.
public delegate Order LookupHandlerType();
public delegate void ProcessOrderType(Order o);
public class OrderProcessing : IOrderProcessing
{
public LookupHandlerType LookupHandler;
public ProcessOrderType EnterHandler;
public ProcessOrderType ApproveHandler;
public OrderProcessing(IDataLayer dataLayer) {
DataLayer = dataLayer;
LookupHandler = _LookupOrder;
EnterHandler = _EnterOrder;
ApproveHandler = _ApproveOrder;
}
private IDataLayer DataLayer { get; set; }
public Order LookupOrder() {
if (LookupHandler == null)
throw new AuthorizationError();
return LookupHandler();
}
public void EnterOrder(Order o) {
if (EnterHandler == null)
throw new AuthorizationError();
EnterHandler(o);
}
public void ApproveOrder(Order o) {
if (ApproveHandler == null)
throw new AuthorizationError();
ApproveOrder(o);
}
// business logic in these methods
private Order _LookupOrder() { return new Order(); }
private void _EnterOrder(Order o) { }
private void _ApproveOrder(Order o) { }
}
public class Factory
{
public IOrderProcessing CreateOrderProcessing()
{
// set these based on role user is in.
bool manager = false;
bool clerk = true;
bool salesperson = true;
//var processing = container.Resolve<IOrderProcessing>() as OrderProcessing;
OrderProcessing processing = new OrderProcessing(new MyDataLayer());
if (!manager) processing.ApproveHandler = null;
if (!clerk) processing.EnterHandler = null;
if (!salesperson) processing.LookupHandler = null;
return processing;
}
}
Dynamic-binding Authorization
As a further refinement, rather than statically binding to the Handler delegate properties, we could have the names of the Handlers stored in a database or file and use Reflection to set them to null if the role disallows access.
public class DynamicFactory
{
public IOrderProcessing CreateOrderProcessing()
{
//var processing = container.Resolve<IOrderProcessing>() as OrderProcessing;
OrderProcessing processing = new OrderProcessing(new MyDataLayer());
//get user role, list of prohibited methods from authorization database
//var prohibitedHandlers = new List<string>()
// { "ApproveHandler", "EnterHandler" };
//disable access
foreach (string handler in prohibitedHandlers)
{
var field = processing.GetType().GetField(handler);
field.SetValue(processing, null);
}
return processing;
}
}
I've presented some strategies for authorizing access to your business layer objects. It's important to keep authorization code separated from your business logic, since it is a cross-cutting concern and since security models will likely change over time, it makes sense to encapsulate the access in a Factory type object that can evolve over time. The key is that the business logic object that is handed to the application is already configured for the privileges that the user has and the authorization checking does not need to be embedded in the business logic.
No comments:
Post a Comment