Chapter 12. Authentication and Authorization

Most applications have some form of authorization, where they allow or disallow a user access to certain forms, pages, or application functions. Before any authorization can occur, the application must know the identity of the user, so most applications also use some type of authentication.

There are several types of authentication available to a .NET application, including the following:

  • Integrated Windows domain or Active Directory

  • ASP.NET membership provider

  • Custom database tables or LDAP server

In every case, the user's identity and roles are maintained in a .NET principal object, which is available to all code in your application. The support for authorization provided by .NET is role-based. The current user has a set of roles and your code can determine whether the user is in a specified role by calling the IsInRole() method on the current principal object. This capability can be used by the UI developer to decide whether to allow a user access to each form or page in the application and is the underlying technology used by ASP.NET for this purpose.

CSLA .NET uses the standard .NET model as well, allowing a business developer to specify roles that are allowed to create, retrieve, update, and delete a business object. But CSLA .NET goes a step further and also allows the developer to specify which roles are allowed to read or write to a property and to call specific methods.

By default, CSLA .NET calls the standard IsInRole() method to check the user's roles, and so it automatically works with any .NET authentication model.

Authentication

CSLA .NET supports either integrated Windows authentication (Active Directory or AD) or custom authentication. Using the ASP.NET MembershipProvider is considered a form of custom authentication because CSLA .NET can be used totally outside of ASP.NET but you can still choose to authenticate using that model. In all cases, the current thread should have a valid principal object and associated identity object, allowing the authorization code discussed later in this chapter to verify the user's roles as appropriate.

You should also be aware that when using custom authentication, the data portal requires that the custom principal object inherit from the Csla.Security.BusinessPrincipalBase class. A business application implements its own principal and identity classes so it can authenticate the user and load the user's roles as appropriate for the application.

In the .NET security model used by CSLA .NET, the user is always identified by a principal object, which must implement the IPrincipal interface from the System.Security.Principal namespace. Every principal object contains exactly one identity object, which must implement the IIdentity interface from that same namespace.

If you are using integrated Windows authentication, the .NET environment automatically has WindowsPrincipal and WindowsIdentity objects initialized for you to use. They contain the user's identity and groups, based on the username entered when logging into the Windows workstation or website. In other words, your application has to do almost nothing in this case because Windows and .NET have done most of the work.

If you are using custom authentication (including using the ASP.NET MembershipProvider model), you must create your own custom principal and identity classes and you must get the username/password (or other credentials) from the user to create those objects. This is obviously more work but allows you the flexibility of authenticating the user in any way you choose.

The CSLA .NET framework includes the Csla.Security namespace, where you can find several classes to help in the implementation of custom authentication. Before I get into that namespace, however, you should understand how CSLA .NET manages and exposes the current .NET principal object in different environments.

I'll first discuss the Csla.ApplicationContext.User property, which manages and exposes the principal object in Windows, web, and WPF applications. Then I'll show you how to configure CSLA .NET to use either integrated Windows authentication or custom authentication. Once you understand those issues, I dig into the Csla.Security namespace.

Csla.ApplicationContext.User Property

When code is running outside ASP.NET, it relies on System.Threading.Thread.CurrentPrincipal to maintain the user's principal object. On the other hand, when code is running inside ASP.NET, the only reliable way to find the user's principal object is through HttpContext.Current.User. Normally, this would mean that you would have to write code to detect whether HttpContext.Current is null and only use System.Threading if HttpContext isn't available.

The User property of the Csla.ApplicationContext class automates this process on your behalf:

public static IPrincipal User
  {
    get
    {
      if (HttpContext.Current == null)
        return Thread.CurrentPrincipal;
      else
        return HttpContext.Current.User;
    }
    set
    {
      if (HttpContext.Current != null)
        HttpContext.Current.User = value;
      Thread.CurrentPrincipal = value;
    }
  }

In general, Csla.ApplicationContext.User should be used in favor of System.Threading or HttpContext because it automatically adjusts to the environment in which your code is running. With CSLA .NET-based applications, this is particularly important, because your client code could be a Windows Forms application but your server code could be running within ASP.NET. Remember that your business objects run in both locations, so must behave properly both inside and outside ASP.NET.

Windows Authentication

When using integrated Windows authentication, your application and .NET relies on the underlying Windows operating system to take care of the authentication and to provide information about the user. While most of the work is done for you by Windows and .NET, you do need to configure the environment, including both CSLA .NET and .NET itself.

Configuring CSLA .NET

CSLA .NET has one configuration option to control authentication and it is set in the application's app.config or web.config file in the appSettings block:

<add key="CslaAuthentication" value="Windows" />

This instructs CSLA .NET to rely on the .NET Framework and Windows to manage the current principal and identity objects. In other words, this tells CSLA .NET it doesn't have to deal with authentication and to just assume it has all been taken care of behind the scenes.

Configuring WPF and Windows Forms

If the application is running on a client workstation, the user who logged into the workstation defines the Windows identity of the user for all interactive applications. Within .NET, the default is to not use this value but rather to provide an unauthenticated GenericPrincipal from the System.Security.Principal namespace. You can change that by running this line of code as your application starts up:

AppDomain.CurrentDomain.SetPrincipalPolicy(
          System.Security.Principal.PrincipalPolicy.WindowsPrincipal);

This tells the .NET Framework to use the underlying WindowsPrincipal and WindowsIdentity objects as the current .NET principal and identity.

Configuring ASP.NET

In an ASP.NET environment, the process is controlled by web.config and your IIS virtual root settings. Your web server's virtual root must be configured to disallow anonymous access. The result is that the user must log into the website using his Windows credentials to access any pages. Obviously, this means the web server is authenticating the user's credentials.

You also need to configure ASP.NET to use Windows authentication and impersonation:

<authentication mode="Windows" />
  <identity impersonate="true" />

If you only tell ASP.NET to use Windows authentication, it relies on the web server to do the work but doesn't change the current principal and identity to match that user on every page request. By setting both values, ASP.NET relies on the web server to handle authentication and then impersonates the identity of that user.

In many ways, integrated Windows authentication is the simplest approach. But it isn't always practical because many applications allow users that aren't in the Windows domain or Active Directory to log in, and so some form of custom authentication is required.

Custom Authentication

Custom authentication means that you are responsible for getting the user's credentials (often a username and password) and ensuring they are valid. The most common way to check the credentials is by comparing them to values in a database table. People also use the ASP.NET MembershipProvider model (which is also typically just a database table lookup), LDAP servers, Active Directory Application Mode (ADAM), and many other security data stores.

Configuring CSLA .NET

You must set the CslaAuthentication configuration option to Csla to instruct CSLA .NET to use custom authentication, relying on the developer to create a custom principal and identity and to make that custom principal the current principal on the client:

<add key="CslaAuthentication" value="Csla" />

The data portal, discussed in Chapter 15, is affected by this setting as well because when using custom authentication the data portal automatically serializes the current principal from the client to the application server on every data portal call. This means that the application server impersonates the client's custom identity.

Authentication Classes in Csla.Security

CSLA .NET is designed to support easier implementation of custom authentication with a set of classes. Table 12-1 lists these base classes.

Table 12.1. Principal and Identity Classes in Csla.Security

Class

Description

BusinessPrincipalBase

Base class for all custom principal objects

CslaIdentity

Base class for custom identity objects

ICheckRoles

Interface that a custom identity class can implement so BusinessPrincipalBase knows to delegate IsInRole() to that identity object

IdentityFactory

Data portal factory class containing methods to authenticate credentials against the ASP.NET MembershipProvider; used by MembershipIdentity

MembershipIdentity

Base class to assist in using the ASP.NET MembershipProvider to create a custom identity object

UsernameCriteria

Criteria class for passing a username and password to a custom identity object

UnauthenticatedIdentity

Simple subclass of CslaIdentity to provide a generic, unauthenticated identity object

UnauthenticatedPrincipal

Simple subclass of BusinessPrincipalBase to provide a generic, unauthenticated principal object

A developer can use these classes to implement custom authentication against almost any security store and to create custom principal and identity objects that can be used by .NET and CSLA .NET for authorization.

You can look at the code in these classes in the code download for this book (www.apress.com/book/view/1430210192 or www.lhotka.net/cslanet/download.aspx). Here, I focus on the issues they are designed to address.

Creating a Custom Principal Class

The data portal, which I discuss in Chapter 15, can only deal with Serializable objects. And when CSLA .NET is configured to use custom authentication, it automatically serializes the client principal and identity objects and copies them to the application server on each data portal call. The result is that the application server is able to impersonate the client's identity.

Note

While CSLA .NET for Silverlight is not the topic of this book, it also has a data portal that serializes objects, and BusinessPrincipalBase is designed to work with CSLA .NET for Silverlight as well.

The BusinessPrincipalBase class is a very basic implementation of the IPrincipal interface from System.Security.Principal that is compatible with the data portal. The goal of this class is to make it easier for a business developer to create her own custom principal by subclassing BusinessPrincipalBase.

The simplest approach is a subclass like this:

[Serializable]
  public class CustomPrincipal : BusinessPrincipalBase
  {
    private CustomPrincipal(IIdentity identity)
      : base(identity)
    {    }

    public static void Login(string username, string password)
    {
      var identity = CustomIdentity.GetIdentity(username, password);
      Csla.ApplicationContext.User =
        new CustomPrincipal(identity);
    }

      public static void Logout()
      {
        var identity = new UnauthenticatedIdentity();
        Csla.ApplicationContext.User = new CustomPrincipal(identity);
      }
  }

The Login() method calls the factory method on a CustomIdentity class (which I show in the next section) to get back an identity object. That identity object may or may not be authenticated, but it is a valid object either way. The identity object is passed as a parameter to the constructor of CustomPrincipal, which passes it to the BusinessPrincipalBase base class.

The important thing is that the resulting principal object, containing its identity object (either authenticated or not), is set as the current principal by setting the User property of Csla.ApplicationContext. This ensures that the principal is available to the current thread and/or the current HttpContext as appropriate.

As BusinessPrincipalBase implements IPrincipal, it has an Identity property that returns the identity object created in the Login() method. Its IsInRole() method calls the identity object contained by this principal, assuming that identity object is a subclass of CslaIdentity. Here's the IsInRole() implementation from BusinessPrincipalBase:

public virtual bool IsInRole(string role)
  {
    var cslaIdentity = _identity as CslaIdentity;
    if (cslaIdentity != null)
      return cslaIdentity.IsInRole(role);
    else
      return false;
  }

The method is virtual, so a subclass can replace the implementation, but if the identity object is a subclass of CslaIdentity, this implementation does the work automatically.

As you'll see in the next section of this chapter, I recommend having the identity object authenticate the user's credentials and (if successful) load the user's roles, all in one trip to the security store. Also, take a look at the Logout() method. Notice how it creates a CustomPrincipal object but with an UnauthenticatedIdentity as its identity. The UnauthenticatedIdentity object has no username and no roles and its IsAuthenticated property returns false.

You'll see a more complete example in Chapter 17 when I walk through the ProjectTracker reference application's business object implementation.

Creating a Custom Identity Class

Every principal object contains an identity object. In fact, the identity object is the object that contains information about the user, such as the username, how the user was authenticated, and so forth. Identity objects implement IIdentity from the System.Security.Principal namespace, and CslaIdentity is a base class that makes it easy to create custom identity classes that work with the data portal.

When a user logs in using custom authentication, the typical model is to authenticate his credentials using a read-only root object (see the stereotype discussion in Chapters 4 and 5). The CslaIdentity class inherits from ReadOnlyBase so it is not only an identity object but can handle the authentication process in its DataPortal_Fetch() method:

[Serializable]
public abstract partial class CslaIdentity :
    ReadOnlyBase<CslaIdentity>, IIdentity

By subclassing CslaIdentity, the developer can focus more directly on authenticating the user's credentials and (if successful) loading the user's list of roles. Here's a very simple subclass:

[Serializable]
  public class CustomIdentity : CslaIdentity
  {
    private CustomIdentity()
    { /* require use of factory method */ }

    public static void GetIdentity(string username, string password)
    {
      return DataPortal.Fetch<CustomIdentity>(
        new UsernameCriteria(username, password));
    }
private void DataPortal_Fetch(UsernameCriteria criteria)
    {
      // authenticate credentials here
      if (authenticated)
      {
        base.Name = username;
        base.IsAuthenticated = true;
        base.Roles = roles; // list of roles from security store
      }
      else
      {
        base.Name = string.Empty;
        base.IsAuthenticated = false;
        base.Roles = null;
      }
    }
  }

This is just an example, and to make this work, the DataPortal_Fetch() method needs to be finished, so it talks to the security store to validate the user's credentials and loads the user's list of roles.

The UsernameCriteria class is used to easily pass the username and password credentials from the factory method through the data portal and to the DataPortal_Fetch() method. If an application uses credentials other than a username/password pair, the developer needs to create her own custom criteria class, as described in Chapter 5.

MembershipProvider Authentication

Creating a custom identity object that validates the user's credentials against the ASP.NET MembershipProvider component follows the same basic process I've discussed thus far. The developer needs to create a custom principal and custom identity class as shown previously.

However, CSLA .NET includes the MembershipIdentity class to simplify the process of validating the username and password against the ASP.NET security store. So instead of subclassing CslaIdentity, a developer can subclass MembershipIdentity because it already includes the code necessary to do the credential validation.

This means creating a subclass that looks like this:

[Serializable]
  public class CustomIdentity : MembershipIdentity
  {
    protected override void LoadCustomData()
    {
      // load roles and any custom properties here
    }
  }

The MembershipIdentity base class takes care of validating the username and password but doesn't attempt to load any roles for the user. It does call the LoadCustomData() method shown here so the developer can override that method to load the user's roles and any other user-related data. By the time LoadCustomData() is invoked, the identity object is already loaded with the username, IsAuthenticated is true, and the identity object is essentially ready for use.

If the user's credentials are invalid, no exception is thrown. Instead, an unauthenticated instance of MembershipIdentity is returned, with an IsAuthenticated value of false. In this case, the LoadCustomData() method is not invoked because the identity doesn't represent a valid user.

You should now have an understanding of the difference between Windows and custom authentication. And you should understand how the BusinessPrincipalBase and various identity base classes can be used to create custom principal and identity objects that provide user and role information to .NET and CSLA .NET for authorization.

In the rest of the chapter I discuss how CSLA .NET supports authorization at the type and property levels.

Authorization

Authorization supports the idea that each business object property and method can have a list of roles that are allowed and denied access. I already touched on some of these concepts when I discussed how properties are declared in Chapter 7. Behind the scenes, those methods make use of an AuthorizationRules object from the Csla.Security namespace.

Every business object that uses authorization rules has an associated AuthorizationRules object that manages the list of roles associated with each property and method. The AuthorizationRules class also maintains a list of roles allowed to create, get, update, and delete each business object type.

To do this work, AuthorizationRules relies on a number of other classes. Table 12-2 lists the types required for authorization.

Table 12.2. Types Used to Implement the Authorization Subsystem

Type

Description

AccessType

Lists access types for properties and methods

AuthorizationRules

Coordinates the functionality of the authorization subsystem

AuthorizationRulesManager

Maintains list of roles for a business object or business object type

IAuthorizeReadWrite

Defines methods for use by UI components such as Csla.Wpf.Authorizer

IsInRoleProvider

Defines a delegate signature for the method that resolves whether the current user is in a specific role

ObjectAuthorizationRules

Maintains the cache of all object type level authorization roles for all business object types

NoAccessBehavior

Lists options describing what step should be taken when a user attempts an action but does not have access

RolesForProperty

Maintains a list of roles allowed and denied access to a property

RolesForType

Maintains a list of roles allowed and denied access to a business object type

SharedAuthorizationRules

Maintains the cache of all per-type property authorization roles defined for all business object types

I'll discuss how these types are used to implement authorization at the business object type level and at the property level.

Type Level Authorization

A business developer can specify what roles are allowed to create, get, update, and delete each business object type. This behavior is not at the object instance level but is at the type level. In other words, these roles are defined and can be accessed without ever creating an instance of a business object.

The intent of this functionality is to allow a UI developer to determine whether the user could create, retrieve, update, or delete an object. Ideally, the UI developer would do these checks before ever creating an instance of a business object, so the various buttons, menu items, and links the user would use to perform each action can be disabled if they don't work anyway.

The AuthorizationRules class uses the ObjectAuthorizationRules type to manage this behavior.

AddObjectAuthorizationRules Method

Inside a business class, a developer can write code like this to define these roles:

private static void AddObjectAuthorizationRules()
  {
    Csla.Security.AuthorizationRules.AllowGet("Supervisor");
  }

This indicates that the users in the Supervisor role should be allowed to retrieve instances of this business object type. Notice that the AddObjectAuthorizationRules() method is static, so it can be invoked without needing to first create an instance of the business object. Table 12-3 lists the static methods from the AuthorizationRules class available to the business object developer.

Table 12.3. Per-Type Authentication Methods

Method

Description

AllowCreate()

Specifies the roles allowed to create a new object

AllowGet()

Specifies the roles allowed to get an existing object

AllowEdit()

Specifies the roles allowed to edit and save (insert or update) an object

AllowDelete()

Specifies the roles allowed to delete an object

The AddObjectAuthorizationRules() method is invoked by ObjectAuthorizationRules the first time an attempt is made to get the list of roles for a business object type. Because all these values are maintained in a static cache, multithreading issues must be managed, just as I discuss in Chapter 11 in regard to validation.

Here's the GetRoles() method and the declaration of the static cache it uses:

private static Dictionary<Type, RolesForType> _managers =
    new Dictionary<Type, RolesForType>();

  internal static RolesForType GetRoles(Type objectType)
  {
    RolesForType result = null;
    if (!_managers.TryGetValue(objectType, out result))
    {
      lock (_managers)
      {
if (!_managers.TryGetValue(objectType, out result))
        {
          result = new RolesForType();
          _managers.Add(objectType, result);
          // invoke method to add auth roles
          var flags = BindingFlags.Static |
                             BindingFlags.Public |
                             BindingFlags.NonPublic |
                             BindingFlags.FlattenHierarchy;
          MethodInfo method = objectType.GetMethod(
            "AddObjectAuthorizationRules", flags);
          if (method != null)
            method.Invoke(null, null);
        }
      }
    }
    return result;
  }

The same kind of lock scheme I discuss in Chapter 11 is used here. The result is that the first thread to attempt to access this property and get through the lock statement will use reflection to invoke the AddObjectAuthorizationRules() method on the business class. This only happens once per AppDomain, and the roles are cached for use throughout the remainder of the application's lifetime.

The methods called by the business developer are defined in the AuthorizationRules class. For example, here's the AllowGet() method:

public static void AllowGet(Type objectType, params string[] roles)
  {
    var typeRules = ObjectAuthorizationRules.GetRoles(objectType);
    typeRules.AllowGet(roles);
  }

There's no locking here because this method is intended for use only within the AddObjectBusinessRules() method, and that method is only invoked within the context of a lock statement, so it is already thread-safe.

Using Type Level Roles

Now that you understand how the type level roles are added and cached, it is important to understand how they are used. Any code in the business or UI layer can determine whether the current user is authorized to create, get, update, or delete a type of business object with code such as this:

bool canGet = Csla.Security.AuthorizationRules.CanGetObject(typeof(MyObject));

There are CanCreateObject(), CanEditObject(), and CanDeleteObject() methods as well, and they work the same way. For example, here's the CanGetObject() method:

public static bool CanGetObject(Type objectType)
  {
    bool result = true;
    var principal = ApplicationContext.User;
    var allow = Csla.Security.AuthorizationRules.GetAllowGetRoles(objectType);
    if (allow != null)
    {
if (!Csla.Security.AuthorizationRulesManager.PrincipalRoleInList(
                       principal, allow))
        result = false;
    }
    else
    {
      var deny = Csla.Security.AuthorizationRules.GetDenyGetRoles(objectType);
      if (deny != null)
      {
        if (Csla.Security.AuthorizationRulesManager.PrincipalRoleInList(
                         principal, deny))
          result = false;
      }
    }
    return result;
  }

The GetAllowGetRoles() and GetDenyGetRoles() methods are helper methods that retrieve the list of roles allowed and denied access to the get operation for the specified type:

internal static List<string> GetAllowGetRoles(Type objectType)
  {
    var typeRules = ObjectAuthorizationRules.GetRoles(objectType);
    return typeRules.AllowGetRoles;
  }

The PrincipalRoleInList() method loops through the list of roles to determine whether the current user is in any of the roles in the list. This method is just a simple loop, but it calls a private method named IsInRole() rather than calling the IsInRole() method on the current principal object.

Here's the IsInRole() method:

private static bool IsInRole(IPrincipal principal, string role)
  {
    if (mIsInRoleProvider == null)
    {
      string provider = ApplicationContext.IsInRoleProvider;
      if (string.IsNullOrEmpty(provider))
        mIsInRoleProvider = IsInRoleDefault;
      else
      {
        string[] items = provider.Split(','),
        Type containingType = Type.GetType(items[0] + "," + items[1]);
        mIsInRoleProvider =
          (IsInRoleProvider)(Delegate.CreateDelegate(
            typeof(IsInRoleProvider),
            containingType, items[2]));
      }
    }
    return mIsInRoleProvider(principal, role);
  }

This method abstracts the IsInRole() concept so it isn't necessarily tied to checking with the current principal object. If the application's config file contains an entry for an IsInRoleProvider() method, that method is used instead of the default. The config entry would go in the <appSettings> element and would look like this:

<add key="CslaIsInRoleProvider" value="Namespace.Class.Method,Assembly" />

The default IsInRoleProvider() exists in the AuthorizationRules class and looks like this:

private static bool IsInRoleDefault(IPrincipal principal, string role)
  {
    return principal.IsInRole(role);
  }

The reason for all this work is to allow an advanced business developer to replace how the IsInRole() operation is performed by substituting his own method for this one.

At this point you should understand how business type level authorization roles are stored in ObjectAuthorizationRules and how the AuthorizationRules class makes the behaviors available both to the business object developer and any other code that needs to check the rules.

Property and Method Level Authorization

It is quite common for a user to have access to a form or a page but not to all the data on that form. Or a user may be allowed to view some data but not change it, based on her role. CSLA .NET supports this concept by allowing a business developer to specify which roles are allowed or denied read and write access to each property on a business object. The developer can do the same thing for methods exposed by the object by specifying which roles are allowed to execute the method.

Per-property authorization is implemented by the GetProperty() and SetProperty() methods I discuss in Chapter 7. These two methods call CanReadProperty() and CanWriteProperty(), which actually perform the role checks with the help of the AuthorizationRules object.

Per-method authorization requires that the method implementation make an explicit call to CanExecuteMethod() before doing any actual work. The CanExecuteMethod() does the role check with the help of the AuthorizationRules object.

Table 12-4 lists the AuthorizationRules methods available to a business developer to specify roles that are allowed or denied access to properties and methods.

Table 12.4. Property and Method Authorization Options

Method

Description

AllowRead()

Specifies the roles allowed to read a property

DenyRead()

Specifies the roles not allowed to read a property

AllowWrite()

Specifies the roles allowed to write to a property

DenyWrite()

Specifies the roles not allowed to write to a property

AllowExecute()

Specifies the roles allowed to execute a method

DenyExecute()

Specifies the roles not allowed to execute a method

The default implementation provided by CSLA .NET is permissive. This means that by default, all users are allowed to read and write to all properties and to execute all methods. However, if one or more roles are allowed to read, write, or execute, all other roles are denied access. Alternately, you can choose to deny access to specific roles, in which case all other roles continue to have access.

Not only does each object enforce its rules but the rules are exposed publicly to the rest of the application. This is primarily so a UI developer can enable and disable UI controls to give the user visual cues about what she can and can't do. The IAuthorizeReadWrite interface in the Csla.Security namespace provides a standardized way to access this information, and it is used by the UI controls discussed in Chapter 10.

IAuthorizeReadWrite Interface

The IAuthorizeReadWrite interface defines a public interface for use by UI frameworks or other code that needs to determine what a user can do to an object's properties or methods. It looks like this:

public interface IAuthorizeReadWrite
{
  bool CanWriteProperty(string propertyName);
  bool CanReadProperty(string propertyName);
  bool CanExecuteMethod(string methodName);
}

This interface is implemented by BusinessBase and ReadOnlyBase and can be used against any business object with properties and methods.

Per-Type Authorization Rules

A business object developer must specify the roles that are allowed and denied access to each property and method. The AuthorizationRules class maintains two lists of the roles. One list is shared across all instances of each business object type and the other list is maintained for each individual object instance.

The list maintained across all instances of a type is far more efficient in terms of memory and performance and should be the preferred approach. A business object developer must override the AddAuthorizationRules() method to associate shared per-type roles with properties and methods. The code in the business object looks like this:

protected override void AddAuthorizationRules()
  {
    AuthorizationRules.AllowRead(NameProperty, "Supervisor", "Guest");
    AuthorizationRules.DenyWrite(NameProperty, "Guest");
    AuthorizationRules.AllowExecute("DoWork", "Supervisor");
  }

This specifies that the Supervisor and Guest roles are allowed to read the Name property, but the Guest role is specifically not allowed to alter the property. And the Supervisor role is allowed to execute the business object's DoWork() method.

The list of allowed and denied roles for each property and method is maintained within the AuthorizationRules object, which uses the types listed in Table 12-5 to store and retrieve the information.

Table 12.5. Types Used to Maintain the Roles for an Object Type

Type

Description

SharedAuthorizationRules

Maintains a cache with an AuthorizationRulesManager object for each business object type

AuthorizationRulesManager

Maintains a list of RolesForProperty objects, each one containing the roles for a specific property

RolesForProperty

Maintains the lists of allowed and denied roles for reading and writing a specific property

I'm not going to walk through the code for these classes. They use the same multithreaded locking scheme I discuss in Chapter 11 and earlier in this chapter. The roles are maintained in static fields and so are shared across each AppDomain and they are initialized once as the AppDomain starts up. This means they are initialized once for the lifetime of an application.

Per-Instance Authorization Rules

A business object can also have a list of roles for just that one instance. This is expensive in terms of memory and performance: memory because the lists of roles are maintained for each instance of the business type; performance because the lists of roles are initialized as each business object instance is created.

The reason for using this approach is if you have authorization rules that must vary on a per-instance basis. Such scenarios are fortunately rare because the overhead to associating the rules with each instance can be quite high.

If you want to use this model, the business developer must override AddInstanceAuthorizationRules() like this:

protected override void AddInstanceAuthorizationRules()
  {
    AuthorizationRules.InstanceAllowRead(NameProperty, "Supervisor", "Guest");
    AuthorizationRules.InstanceDenyWrite(NameProperty, "Guest");
    AuthorizationRules.InstanceAllowExecute("DoWork", "Supervisor");
  }

These roles are maintained in an AuthorizationRulesManager object that is created for each business object instance.

CanReadProperty, CanWriteProperty, and CanExecuteMethod Methods

The BusinessBase class implements three virtual methods that are used to determine whether the current user is allowed to read, write, or execute individual properties and methods. The ReadOnlyBase class implements only CanReadProperty() because it is designed to support read-only objects.

These methods are virtual so a business developer can override and extend their behavior. By default, these methods simply check the list of allowed and denied roles associated with each property or method.

To enhance performance, the result of a role check is cached and is only rechecked if the current principal object changes. This is particularly valuable in a WPF or Windows Forms application, where the principal will only change if the user logs in or out of the application (when using custom authentication). It has less value in web applications because the server is typically stateless and so the business object (and thus its cache) is destroyed at the end of each page or service request.

For example, here's the CanReadProperty() method from BusinessBase:

[EditorBrowsable(EditorBrowsableState.Advanced)]
  public virtual bool CanReadProperty(string propertyName)
  {
    bool result = true;

    VerifyAuthorizationCache();

    if (!_readResultCache.TryGetValue(propertyName, out result))
    {
      result = true;
if (AuthorizationRules.HasReadAllowedRoles(propertyName))
      {
        // some users are explicitly granted read access
        // in which case all other users are denied
        if (!AuthorizationRules.IsReadAllowed(propertyName))
          result = false;
      }
      else if (AuthorizationRules.HasReadDeniedRoles(propertyName))
      {
        // some users are explicitly denied read access
        if (AuthorizationRules.IsReadDenied(propertyName))
          result = false;
      }
      // store value in cache
      _readResultCache.Add(propertyName, result);
    }

    return result;
  }

The VerifyAuthorizationCache() method does two things. First, it ensures that the cache of authorization values is initialized, and, second, it reinitializes the cache if the current principal has changed. In other words, this method makes sure the cache is ready for use and also that cached values for one user aren't used for a different user.

The TryGetValue() method is used to efficiently retrieve any cached value for a property from the cache. If there is no cached value for this property, the authorization rules are checked for this property.

The default authorization rules used by CSLA .NET are permissive. In other words, by default all users are allowed to read and write all properties (and to execute all methods). Table 12-6 lists the results of allowing or denying specific roles access.

Table 12.6. Results of Allowing or Denying roles

Allow

Deny

Result

None

None

All users have access.

Any

None

Only allowed roles have access.

None

Any

All roles except denied roles have access.

Any

Any

Only allowed roles have access.

Ultimately, the IsReadAllowed() and IsReadDenied() methods use the same IsInRole() provider scheme I discuss earlier in the chapter. This means that by default the current principal object's IsInRole() method is called but an advanced business developer could replace that behavior.

The CanWriteProperty() and CanExecuteMethod() methods follow the same approach and the same default behavior as described in Table 12-6. Together, these methods allow per-property and per-method authorization on all business objects.

Conclusion

This chapter covered the authorization subsystem provided by CSLA .NET. The authorization behaviors leverage the standard .NET principal and identity object model, which enables a role-based authorization scheme.

Using this scheme, a business developer can specify which roles are allowed to create, get, edit, and delete each type of business object. They can also control which roles are allowed or denied read and write access to each property on a business object. The same is true for methods implemented by a business object.

In Chapters 13 through 16 I discuss the rest of the major framework features. Then, starting with Chapter 17, I walk through the ProjectTracker reference application implementation.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset