Saturday, April 24, 2010
« Custom CodeAnalysis Rules for CSLA | Main | Chicago code camp on May 1 »

I’m well into the redesign of the authorization system in CSLA .NET. The new system is integrated into the new business rules system, and so is no longer a separate concept. To me this makes sense, since authorization rules are just a specific type of business rule.

Some of the basic concepts from CSLA .NET 2.0 to 3.8 are still there. Per-property authorization:

  • CanReadProperty()
  • CanWriteProperty()
  • CanExecuteMethod()

Per-type authorization (which is a little different now) where you can ask:

  • Can the user create an instance of this type?
  • Can the user get/fetch an instance of this type?
  • Can the user edit/save an instance of this type?
  • Can the user delete an instance of this type?

And the new per-instance authorization where you can ask:

  • Can the user create the instance you already have? (silly, but possible)
  • Can the user get/fetch the instance you already have? (silly, but possible)
  • Can the user edit/save this instance?
  • Can the user delete this instance?

The actual authorization checks occur by executing an IAuthorizationRule rule object. CSLA provides the AuthorizationRule base class to make it easy to implement most rules, and includes IsInRole and IsNotInRole rules. For example, here’s the IsInRole rule:

/// <summary>
/// IsInRole authorization rule.
/// </summary>
public class IsInRole : AuthorizationRule
{
  private List<string> _roles;

  /// <summary>
  /// Creates an instance of the rule.
  /// </summary>
  /// <param name="action">Action this rule will enforce.</param>
  /// <param name="roles">List of allowed roles.</param>
  public IsInRole(AuthorizationActions action, List<string> roles)
    : base(action)
  {
    _roles = roles;
  }

  /// <summary>
  /// Creates an instance of the rule.
  /// </summary>
  /// <param name="action">Action this rule will enforce.</param>
  /// <param name="element">Member to be authorized.</param>
  /// <param name="roles">List of allowed roles.</param>
  public IsInRole(AuthorizationActions action, Csla.Core.IMemberInfo element, List<string> roles)
    : base(action, element)
  {
    _roles = roles;
  }

  /// <summary>
  /// Rule implementation.
  /// </summary>
  /// <param name="context">Rule context.</param>
  protected override void Execute(AuthorizationContext context)
  {
    if (_roles.Count > 0)
    {
      foreach (var item in _roles)
        if (Csla.ApplicationContext.User.IsInRole(item))
        {
          context.HasPermission = true;
          break;
        }
    }
    else
    {
      context.HasPermission = true;
    }
  }
}

At its most basic a rule is composed of some simple items:

  • An action (read/write property, execute method, create/get/edit/delete type/instance)
  • An optional target element (property/method)
  • An Execute() method that actually implements the rule

If your rule needs more information that’s fine – it is your class, so you can just add more properties. Just remember that a rule object is used across all instances of a business object type, so (just like with other business rules) the Execute() method absolutely must not change instance-level state in the rule object.

The Execute() method is passed a context object. The context object contains some simple properties:

  1. Target (optional reference to business object)
  2. HasPermission (true/false result returned by rule)

The Target property may be null. For per-property rules it will not be null, and for per-instance rules it will not be null. But for per-type rules it will be null. The way CSLA itself (especially the data portal) invokes rules is always per-property or per-type. The thing is, when possible even the per-type checks include the Target reference (typically only for CanEditObject checks). So you can use it if it is there, but you need to handle the case where it is null.

But the real point here, is that you can write your own authorization rule that has nothing to do with roles. You could use permissions, claims, random numbers – whatever you want to use to decide whether the user can or can’t perform the requested action.

Authorization rules are added in the AddBusinessRules() override in your business class – just like other business rules:

protected override void AddBusinessRules()
{
  BusinessRules.AddRule(new IsInRole(
    AuthorizationActions.WriteProperty,
    NameProperty,
    new List<string> { “Administrator” });
}

The rule set concept from other business rules applies here too – so you can have different authorization rules for different users/contexts/etc.

Finally, you can invoke rules as follows:

// per-type
bool result = Csla.Rules.BusinessRules.HasPermission(
  AuthorizationActions.CreateObject, typeof(Customer));
// per-instance
bool result = Csla.Rules.BusinessRules.HasPermission(
  AuthorizationActions.EditObject, _myCustomer);

The existing IAuthorizeReadWrite interface continues to operate as in 3.x, so you can use that to invoke CanReadProperty/CanWriteProperty/CanExecuteMethod as before.

Sunday, April 25, 2010 9:20:06 AM (Central Standard Time, UTC-06:00)
Hello Rocky, thanks for the explanation. Some feedback based on the current trunk:

1. CanReadProperty etc. are currently not working with inheritance (works after changing PropertyInfoManager.GetRegisteredProperties to FieldManager.GetRegisteredProperties).

2. Rules can break quickly without any feedback when using private backing fields (which I have to use).
For example I also had to make another ReadProperty function virtual and override it:
protected virtual P ReadProperty<P>(PropertyInfo<P> propertyInfo)
Maybe IPropertyInfo could have a UsesPrivateBackingField property, and calling the Get/Read/Set/Load/ functions would result in an exception, when a value is accessed from the FieldManager (for properties with UsesPrivateBackingField).
Any feedback would be usefull, when a rule isn't working - or better the rule system would just work with properties wich private backing fields :).
AlexG
Sunday, April 25, 2010 10:29:10 AM (Central Standard Time, UTC-06:00)
LoadProperty and ReadProperty are now virtual in BusinessBase, so you can override them to support private backing fields when using rules that subclass BusinessRule and use its LoadProperty/ReadProperty methods.

Thanks for pointing out the bug in the code - I have not even updated all the tests yet though - I just prototyped all the basic changes and wanted to do the blog post and get feedback before I go a lot further.
Sunday, April 25, 2010 8:21:29 PM (Central Standard Time, UTC-06:00)
Rocky,

Looks good and glad to see it will become part of the greater business rules system rather than continuing in isolation.

Will we still need to honour the IsInRole concept if we don't use roles array - I mean for backwards compatibility anywhere?
Or can we assume that all the automatic auth rule checks (e.g. DataPortal etc) will change over to use the Execute method.
Paul
Sunday, April 25, 2010 9:39:04 PM (Central Standard Time, UTC-06:00)
"Can the user get/fetch the instance you already have? (silly, but possible)"

Not silly at all. You might want to keep a cache of items from the DB, but you dont do the authorization until you hand it out
Dennis
Sunday, April 25, 2010 10:09:15 PM (Central Standard Time, UTC-06:00)
@dennis - good point - not silly.

@paul - your authz rules can do whatever they want - use IsInRole or not. The two rules that CSLA will provide will use IsInRole on the principal. If you don't like that behavior, it is easy enough to create your own rules.
Monday, April 26, 2010 3:00:02 AM (Central Standard Time, UTC-06:00)
How to easily databind on these?

What do we do, if we need to visit the DB to do authorization checks for the specific object? I am thinking asyncronious here.
Dennis
Monday, April 26, 2010 4:34:33 AM (Central Standard Time, UTC-06:00)
Thanks for the update!

I think here should be a change to .FirstOrDefault():

var prop = FieldManager.GetRegisteredProperties().Where(c => c.Name == propertyName).First();
if prop == null...


No nitpicking, just a micro contribution :)


AlexG
Monday, April 26, 2010 7:53:22 AM (Central Standard Time, UTC-06:00)
@alexg - the use of First() is intentional. Having a PropertyInfo<T> is required, so it should fail if one can't be found.

@dennis - data binding is exactly like it is in 3.8 - in XAML you bind to PropertyStatus, your viewmodel or ObjectStatus to make the UI aware of the authz rules.

There is no async concept for authorization, mostly because it isn't clear how that would translate into the UI. I could certainly support it in the rules engine - but what would the UI do during the (potentially many seconds) the authz rule was running? Would it just default to false until the real answer shows up? The user would be blocked out of seeing/editing/doing various functions?

Additionally, if this was handled per-rule, where each rule invokes the server, that'd be extremely chatty. If you really had server rules for authz on each property, you could end up making dozens of server calls to load a form - that'd never work in practice because performance would be amazingly horrible. And that is not something I can fix by just allowing async authz rules.

So if you want async authz rules, you should do what you'd do in 3.8 - create a ReadOnlyBase object you send to the server to get all the rule results, blocking the UI until that object returns. Then the authz rules can use those results to do their job.

The best way to do this in practice is to use a unit of work object to retrieve the business object and the authz object all in one server call - that way you get everything back all at once. I show how to implement a UoW object in the Core 3.8 video series.
Monday, April 26, 2010 9:18:56 AM (Central Standard Time, UTC-06:00)
>the use of First() is intentional. Having a PropertyInfo<T> is
>required so it should fail if one can't be found.
Thats still ensured because you check for null in the next line and throw a meaningful ArgumentOutOfRangeException with the property name.
I'm pretty sure it will only work with .FirstOrDefault():

var prop = FieldManager.GetRegisteredProperties().Where(c => c.Name == propertyName).FirstOrDefault();
if (prop == null)
throw new ArgumentOutOfRangeException("propertyName");
AlexG
Monday, April 26, 2010 9:38:38 AM (Central Standard Time, UTC-06:00)
Ahh, point taken :)
Thursday, April 29, 2010 5:21:33 AM (Central Standard Time, UTC-06:00)
Hello Rocky,

Please consider a 1:N association betwenn Rule and RuleSet.

I currently have at least 4 RuleSets per type. Many rules are shared between most RulesSets and as a consequence I currently have to add _many_ duplicate rules per type.

Alex
AlexG
Thursday, April 29, 2010 5:32:41 AM (Central Standard Time, UTC-06:00)
@alexg - This should not be a problem - it just requires that you give your AddBusinessRules() code a little more thought:

protected override void AddBusinessRules()
{
base.AddBusinessRules();

var commonRule = new MyCommonRule(...);
// add to default ruleset
BusinessRules.AddRule(commonRule);
// switch to other ruleset
BusinessRules.RuleSet = "Other";
// add to other ruleset
BusinessRules.AddRule(commonRule);
// ...
}

The same rule instance is added to all the rule sets, which saves memory and should accomplish what you need.
Thursday, April 29, 2010 7:44:55 AM (Central Standard Time, UTC-06:00)
Thanks a lot for the explanation! Problem solved.
AlexG
Comments are closed.