New information has been added to this article since publication.
Refer to the Editor's Update below.

Authorize It

Use Role-Based Security in Your Middle Tier .NET Apps with Authorization Manager

Keith Brown

Code download available at:AuthorizationManager.exe(128 KB)

This article assumes you're familiar with Windows Server 2003

Level of Difficulty123

SUMMARY

Authorization Manager in Windows Server 2003 represents a significant improvement in the administration of role-based security, making it more scalable, flexible, and easier to implement. Using Authorization Manager, you can define roles and the tasks those roles can perform. You can nest roles to inherit characteristics from other roles, and you can define application groups. In addition, Authorization Manager lets you use scripts to modify permissions dynamically, and it allows you to wrap your security logic in a security policy that can be stored in Active Directory. Authorization Manager also includes an easy-to-use API for running access checks. The author discusses all of these topics and demonstrates them with a working sample.

Contents

Introducing Authorization Manager
A Sample App: the Corporate Library
Authorization Store
The AzMan Runtime Interface
Stores, Applications, and Scopes
Application Groups
Scripts
Supporting Authorization Scripts
Auditing
Conclusion

Role-based security has been evolving on the Windows® platform since the first release of Windows NT®. Using roles, the operating system can make determinations such as whether a process is privileged by checking the security context for a group called BUILTIN\Administrators. The operating system makes decisions based on this logical role (such as whether to let you install services or device drivers). When you install the operating system, you get to choose who will assume this role by adding them to the Administrators group.

Microsoft® Transaction Services (MTS) and COM+ tried to make role-based security palatable for application developers, providing a simple role-based authorization infrastructure for COM servers. The goal was to enable a trusted subsystem model for multitier server applications, where an application server is trusted by back-end resources to authorize requests. By performing authorization early, you avoid having to delegate client credentials to back-end servers. Delegation is rife with a whole host of problems that range from potential security vulnerabilities to scalability issues.

If you've been looking for a general-purpose authorization solution in the middle tier, your search may very well be over.

Introducing Authorization Manager

Authorization Manager (commonly known as AzMan) is a new general-purpose role-based security architecture for Windows. AzMan is not tied to COM+, so it can be used in any application that needs role-based authorization, including ASP.NET Web apps or Web Services, client-server systems based on .NET Remoting, and so on. As of this writing, Authorization Manager is available only on Windows Server™ 2003, service pack 4 for Windows 2000, and is slated to ship as a future service pack for Windows XP.

[Editor's Update - 1/9/2004: The Windows Server 2003 Administration Tools Pack lets you install the Windows Server 2003 management tools on a machine running Windows XP Professional.]

The first thing you'll notice when you run the AzMan admin tool shown in Figure 1 is that it's much more complex than what COM+ offered. You no longer have only roles and role assignments. You now have low-level operations that can be assigned to tasks, which can then be assigned to roles. Tasks can include other tasks, and roles can include other roles. This hierarchical approach helps cap the seemingly unbounded set of roles needed in today's complex applications.

Figure 1 Administration Manager

Figure 1** Administration Manager **

Here's how tasks and roles are created. The application designer defines the entire set of low-level operations that are considered security sensitive. The designer then defines a set of tasks that map onto those operations. Tasks were designed to be understandable by business analysts, so a given task is always composed of one or more low-level operations. If a user is granted permission to perform a task, he or she is granted permission to all operations in the task. As an example, a task named Submit Purchase Order might consist of the following operations: Acquire Next PO Number, Enqueue PO, and Send Notification. Of course, you could always simply map each task to a single operation to keep things as simple as possible, but the flexibility of separating tasks and operations is available if you need it.

Once the tasks and operations are defined, you can start coding, and include calls into the AzMan runtime anytime a sensitive operation needs to be performed. This call is IAzClientContext.AccessCheck, and I'll show an example of its usage shortly.

At deployment time, the application setup program sets up an AzMan store either as part of Active Directory® or in a simple XML file, and installs the basic low-level operations and tasks. The administrator uses the AzMan snap-in to see definitions and descriptions of the tasks for the application. The administrator then defines roles that make sense for his or her organization. Just as a task is defined as a set of low-level operations, roles are usually defined as a set of tasks. The administrator can then assign users and groups to those roles. In fact, the administrator's main job in maintaining the application from here on out will be adding and removing users from roles as people join or leave the company or change job titles.

So far, I've focused on the application developer and the administrator, but there may actually be a third person helping with the deployment: the business logic scripter. Every task can have a script associated with it. The idea here is to find dynamic security decisions typically made through calls to IsCallerInRole, and move them out of application code and into a place where an administrator can make changes to an application's security policy without having to modify and recompile code.

A Sample App: the Corporate Library

Let's look at an example. Imagine you are building a system to manage a company library. You need to be able to manage the book inventory, check books in and out, and so on. You'll use AzMan to implement role-based security.

First, you need to make a list of the sensitive operations that appear in your design:

  • Read catalog
  • Place hold (for self or another)
  • Check out book
  • Check in book
  • Add book to inventory
  • Remove book from inventory
  • Read patron history (for self or another)

Note that a couple of operations are sensitive to information that you'll only have at run time. For instance, when attempting to read a patron's history, the application must provide contextual information indicating whether the user is trying to access his own history or that of someone else. While prototyping, you can use the AzMan snap-in to add these operations to a simple XML store. Figure 1 shows what this looks like.

If you want to try following along on your own, run AZMAN.MSC, and make sure you're in developer mode via the Action | Options menu. Create a new store in an XML file, then create a new application in that store. Next, add the operations one by one, giving them a name and a unique integer that represents the operation number. This number is how the application developer identifies operations in calls to AccessCheck. Note that in naming operations, I've encoded the names with the prefix "op." This is simply to avoid naming collisions later when creating tasks and roles since these names all come from the same pool and must be unique.

The AzMan snap-in operates in two modes: developer and administrator. In administrator mode, you don't have the option of creating stores or applications, and you aren't allowed to muck with the low-level operation definitions that the application code relies upon. Frankly, nothing stops the system administrator from entering developer mode and doing these things, but the point is that in administrator mode, the number of options in the UI is reduced to simplify the administrator's life and to help avoid mistakes.

Figure 2 Task Definitions in AzMan

Figure 2** Task Definitions in AzMan **

The next step is to define a set of tasks that map to these low-level operations so the administrator will have an easy job defining roles. Because you kept your list of operations simple, you can define a single task for each operation. There's a good reason to keep things simple unless you absolutely need more complexity, and it has to do with business logic scripts—but more on that later. So for now, let's define a list of tasks that are basically the same as my operations. Figure 2 shows what this looks like in AzMan when you edit a Task Definition.

Authorization Store

It's time to switch hats and pretend that you're the administrator deploying the application. Switch to administrator mode under the Action | Options menu and note how the GUI changes: you are no longer allowed to edit the low-level operations for the application. Go ahead and add roles for the application as defined in Figure 3.

Figure 3 Roles and Tasks

Figure 3** Roles and Tasks **

One way to simplify things here is by nesting roles. For example, Clerk can be defined in terms of the Patron role, with the addition of the Check out book and Check in book tasks, as I've shown in Figure 4. Try doing that in COM+!

Figure 4 Nesting Roles

Figure 4** Nesting Roles **

The last thing the administrator needs to do is make these abstract roles concrete by adding real users to them. To do this, select the Role Assignments folder and choose the Assign Roles action. Note that a role doesn't actually become active in an application until it's been added to this folder. For example, the IAzApplication.Roles property only returns the collection of roles that have been added to the Role Assignments folder, as opposed to all roles that have been defined. Once a role has been assigned, right-click it to add either Windows users and groups, or application groups that you've previously defined in your AzMan store. I'll describe application groups later in this article.

The AzMan Runtime Interface

Once you've got some operations and tasks defined, you can start implementing access checks in code. The first thing you need to think about is authentication. If you can use some built-in Windows plumbing, like the Web server's support for Kerberos authentication, you can get a token for the client. This is by far the most natural way to use AzMan, since a token contains all the groups a user is in, making it fast to map that user onto a set of AzMan roles. If, on the other hand, you're using a form or X.509 certificate to authenticate the user, you won't have a token. Rather, you'll only have the user's name. This doesn't mean you can't use AzMan or even that you'll have to write more code. But it does mean that it'll be more expensive, since the AzMan runtime will have to look up the user's groups manually. This incurs round-trips to domain controllers.

The first thing the app needs to do is initialize the AzMan runtime, point it to the store it plans to use, and drill down to the application where the authorization settings live. For now, let's use a simple XML-based store:

AzAuthorizationStore store = new AzAuthorizationStoreClass(); store.Initialize(0, @"msxml://c:\MyStore.xml", null); IAzApplication app = store.OpenApplication( "Corporate Library Application", null);

To build this application, the project needs to reference the AzMan interop assembly, which can be found in the %WINDIR%\Microsoft.NET\Framework\AuthMan directory.

Now that the application is bootstrapped, when a new client is authenticated you need to construct a representation of the client's security context. The context is a lot like a token in that it caches role mappings for a user:

IAzClientContext ctx = app.InitializeClientContextFromToken(htoken, null);

Where do you get the client's token? Well, that depends on what type of app you're writing. For example, here's some C# code from an ASP.NET page that obtains the client's token. In this case, web.config specifies authentication mode='Windows', and IIS has been configured to require integrated Windows authentication:

WindowsIdentity id = (WindowsIdentity)User.Identity; IntPtr htoken = id.Token;

If you only know the client's name and don't have access to their token, try to figure out if there's a token present that you can get your hands on because a token is the most authoritative way to discover groups for a client. It's also the fastest way, as I mentioned earlier. If you are sure that there's no token for the client available to you, then use this alternate method to initialize a context from an account name in the form: domain\user. This call may incur round-trips to discover domain groups, so expect that it will take some time to execute:

IAzClientContext ctx = app.InitializeClientContextFromName(name, null);

Once you have a client context, you can run an access check. This call takes a number of arguments, but for now I'll keep things simple. Let's say you're implementing a function that adds a book to the inventory. I defined "Add book to inventory" as operation number five, so the code might look like what's shown in Figure 5.

Figure 5 Add Book to Inventory

// always define constants or enums for your ops! const int opAddBookToInventory = 5; const int NO_ERROR = 0; // later in the code... object[] operations = { opAddBookToInventory }; object[] scopes = { "" }; object[] results = (object[]) ctx.AccessCheck(nameOfBook, scopes, operations, null, null, null, null, null); int result = (int)result[0]; if (NO_ERROR == result) { // access is granted } else { // access was denied, result holds error code }

The first argument, nameOfBook, is a string that is used if you've got runtime auditing turned on. It identifies the object on which you're performing the operation, so you should always provide some meaningful information here. I've used the default value for the second argument, scopes, which I'll explain a bit later. The third argument is where you list one or more operations that you want to test. The result is an array that is always the same size as the operations array, with an integer status code for each operation that indicates whether access is granted or denied. Zero indicates that the access check succeeded, and the client identified by the context is allowed to perform the specified operation. Any other value indicates failure (generally what you'll see is the number 5, which is ERROR_ACCESS_DENIED).

The AzMan runtime interface isn't strongly typed. It uses VARIANTs for most of its arguments. This allows classic ASP programmers who use scripting languages to use AzMan, but means that programmers who use strongly typed languages like C# and Visual Basic® .NET may make some mistakes when calling AccessCheck that won't be caught until run time. For example, the operations array must be of type object[], not int[], but the compiler won't complain if you pass an int[] because the actual type of the argument is object. This bit me when I was first learning this API, and it took me a while to figure out exactly how to write the code to avoid getting run-time errors due to parameter type mismatches. I've heard rumors that eventually there will be a managed interface to AzMan, but until that happens, you might want to write a strongly typed wrapper around AccessCheck to avoid getting bitten. The following code shows an example that also simplifies the most common way you'll call the function:

public class AzManWrapper { public static int AccessCheck( IClientContext ctx, string objectName, int operation) { object[] results = (object[])ctx.AccessCheck( objectName, scopes, ops, null, null, null, null, null); return (int)results[0]; } }

With a wrapper, you can provide your own overloads of AccessCheck to handle more sophisticated situations where other optional arguments are necessary. Using a wrapper for this function in particular should save you a lot of grief and will reduce clutter in your application code. You could even use this wrapper to convert AccessCheck failures into exceptions instead of returning a status code, along the lines of IPermission.Demand. Don't go crazy and wrap the whole suite of interfaces though, since this function is really the only one that's tricky to call.

One thing you might be wondering at this point is whether you can use AzMan without using Windows accounts to represent users. The runtime was designed with this in mind, although you need to define custom security identifiers (SIDs) for each user, which is not terribly difficult, and you must call an alternate method to initialize a client security context, namely InitializeClientContextFromStringSid. The biggest hurdle is that you won't be able to use the AzMan snap-in to manage your stores, which are pretty tightly coupled to Windows users and groups. For more details on how to approach this, see the whitepaper by Dave McPherson referenced at the beginning of this article.

Stores, Applications, and Scopes

I want to back up for a moment and discuss the structure of the authorization store a bit. First of all, you have two choices for storing your authorization settings: you can drop the entire store into an XML file or you can host it in Active Directory. I'd strongly recommend using Active Directory for production apps, as it provides a lot more functionality and often better performance than a simple XML file.

If you have a test domain in a lab that you can play with, try bringing up the AzMan snap-in and creating a new store in Active Directory with a distinguished name like this: "CN=MyStore, CN=Program Data, DC=MyDomain, DC=com", replacing MyDomain with your own. To see what objects AzMan created in Active Directory, use a tool like ADSIEdit, an MMC snap-in that you can install from your Windows Server 2003 CD by running SUPPORT\TOOLS\SUPTOOLS.MSI. Create an application in the store and bring up the new application's property page. You'll notice that there are security and auditing tabs that aren't present when you're using a simple XML file.

In Active Directory stores, you can delegate responsibility for administering individual applications within a store, and you can audit changes to the store at a very detailed level. With an XML file, you're limited to securing the file itself with NTFS permissions and auditing. Currently, run-time auditing is only supported if the store is housed in the directory, and auditing is tremendously important to most applications. If Active Directory is available, I would strongly urge you to house your AzMan stores in it, since it's the best place for hosting security policy on Windows.

A single store can house multiple applications. Each application has its own namespace for operations, tasks, and roles. Be aware of concurrency issues if you share a store among multiple applications because stores do not yet support concurrent editing. If you think there's a chance two administrators might be editing a single store at the same time, you need to provide some external locking to serialize access to the store; otherwise, it might become corrupted. The AzMan snap-in doesn't provide this, and until it does, you will be better off limiting the contents of each store to avoid concurrent editing. The simplest solution is to limit each store to housing a single application.

Each application can also define multiple scopes, which is an advanced feature of Authorization Manager that I recommend only to people who have studied AzMan further and absolutely need this extra level of complexity. Scopes allow you to have different authorization settings for different parts of your application. For example, in a large Web application, roles might be assigned one way under a certain subdirectory; under a different subdirectory, the roles might be assigned differently or a totally different set of roles might be defined.

Scopes can be convenient in this case because they can share the low-level operation definitions and maybe even some tasks, roles, and application groups. Unfortunately, they can also be confusing and easy to misuse. For example, when you call AccessCheck, the second argument is where you specify the scope you want to use for the check. If the user is providing the scope name, perhaps via the URL in the request, you'd better be sure that scope name is canonicalized before you pass it to AccessCheck; otherwise, you may allow clever users to fool you into using a weaker scope by encoding the name in an unexpected way. If you're new to this type of attack, you should read the chapter on canonicalization in Writing Secure Code, Second Edition (Microsoft Press®, 2002). To learn more about advanced features like scopes, see the whitepaper by Dave McPherson referenced at the beginning of this article.

Application Groups

There's a nifty feature in AzMan called Application Groups. In large organizations, it can be a real pain to get new groups added to the directory for your application to use. In fact, if it's only your application that needs a particular group definition, you might be simply out of luck when an overworked domain administrator refuses to add yet another entry to his already barely manageable list of groups. In this case, Application Groups can save you. At the store, application, or scope level, you can define groups of users and assign a logical group name to them. You can then use these application groups in role assignments.

AzMan provides two types of application groups: basic and Lightweight Directory Access Protocol (LDAP) query groups. The basic groups are a lot like the groups in Active Directory, but with a twist: you can define both included members and excluded members. For example, you could define a group called EveryoneButBob, as I did in Figure 6. The benefit is increased functionality and convenience. The drawbacks are the increased number of CPU cycles required to determine membership in the application group and the memory required to store the membership list in the application group, so use this feature with care. If you like the exclusion feature shown in Figure 6, you can still get it by using domain groups as members in your application groups, thus reducing the size of the membership list AzMan needs to keep in memory.

Figure 6 Allow Everybody Except Bob

Figure 6** Allow Everybody Except Bob **

LDAP query groups are an expensive but nifty feature of AzMan. Here you can use LDAP query syntax to define a group of users who are similar in some way. For example, here's how you might define the set of engineers who are at least 21 years old:

(&(age>=21)(memberOf= CN=eng,DC=foo,DC=com))

Regardless of the type of group, Basic or LDAP query, the administrator can use these application groups as alternative ways of assigning users to roles.

Scripts

For cases where static grants aren't enough, your application can provide extra context in the form of variables and object references to AccessCheck. This allows a script writer to add business logic using JScript® or VBScript without having to change and recompile the application. For example, the Read patron history task defined earlier could be supplied with extra context, perhaps the set of roles in which the user is a member, and a Boolean indicating whether the client is accessing her own history or that of someone else. This would enable you to write a script that permits Managers to review any patron's history, but restricts normal patrons to reviewing their own histories. A script such as the one shown in Figure 7 could be written for this task.

Figure 7 Review Patron's History

' always start by being pessimistic AzBizRuleContext.BusinessRuleResult = false isManager = false roles = AzBizRuleContext.GetParameter("roles") for each role in roles if "Manager" = role then isManager = true exit for end if next if isManager then ' Managers are allowed to read any patron's history AzBizRuleContext.BusinessRuleResult = true else ' anyone else is allowed to read their own history self = AzBizRuleContext.GetParameter("self") AzBizRuleContext.BusinessRuleResult = self end if

To associate this script with the Read patron history task, bring up the definition page for the task, browse to a file containing this script, specify the language as VBScript, and then press the "Reload Rule into Store" button.

Supporting Authorization Scripts

To support the script shown in Figure 7, you'll need to pass a few more arguments to any access check that involves the Read patron history task. Since you've kept things simple and only defined one task per operation, this means you can provide this context whenever you ask about the corresponding operation. Here's a snippet of code that shows how you would provide these extra context variables for a script writer:

bool self = _userIsCheckingOwnHistory(); object[] operations = { opReviewPatronHistory }; object[] scopes = { "" }; object[] varNames = { "roles", "self" }; object[] varValues = { ctx.GetRoles(""), self }; object[] results = (object[]) ctx.AccessCheck(nameOfPatronHistory, scopes, operations, varNames, varValues, null, null, null);

AzMan is a bit picky about the order of the varNames and varValues array. You must order varNames alphabetically, as I did. The varValues array must then provide the corresponding value for each named parameter in varNames, which is pretty obvious. If you want to get even fancier, you can use the last three arguments of AccessCheck to pass in named object references. This will expand the object model that the script writer sees beyond the default AzBizRuleContext object. I'll leave you to experiment on your own with that feature and address some of the challenges you'll run into if you decide to support scripting.

The first odd thing you'll probably notice about scripts is that they are defined at the task and role level, not at the operation level. But application programmers perform access checks and provide context variables for individual operations, so how does the script writer know what variables will be available for a given task? Clearly it's up to the developer to document this carefully on a per-operation basis. One strategy is to keep things really simple and always pass the same context variables no matter what operation is being performed. This would certainly simplify things for a script writer. In my library example, I was careful to define one task per operation so I could customize the context variables for each task, but remember that administrators can define new tasks when running in admin mode. What would happen if the systems administrator were to define a new task that incorporated two operations that each provided different context variables? Just try to keep things simple and document carefully how scripts should be written in order to avoid these nasty situations.

Another thing to watch out for when writing scripts is that the results of scripts are cached in the client context object for efficiency. Throw away the client context and you throw away the cache. This is good to know because some scripts might be dependent on external data that could change over time.

Be aware that scripts are designed to qualify the tasks or roles to which they are attached. Say, for example, that Alice is a member of roles R1 and R2. Role R1 is directly granted permission to perform operation X. Role R2 is granted the same permission, but this grant is qualified by a script. When Alice tries to perform operation X, AzMan won't even bother running the script in role R2 because operation X is already statically granted through role R1. Thus, scripts cannot be used to deny permissions outright. They can only be used to decide whether a particular task or role should be considered in an access check. Just because a scripted task or role is omitted because its corresponding script evaluated to false doesn't mean there isn't an entirely different task or role that will still grant the operation being tested. Dave McPherson's whitepaper provides a very detailed description of how the runtime implements an access check. You'll find this in the Performance section. For anyone serious about using AzMan, I suggest you study this section carefully.

Auditing

Runtime auditing of access checks is an important feature that's available only if you're using an Authorization Manager store within Active Directory. If you want to enable this feature, right-click the application, choose Properties, and turn it on through the Auditing tab. At run time, the account your server process runs under is important: it must be granted the Generate Audits privilege. The built-in accounts Local Service, Network Service, and SYSTEM all have this privilege by default. Finally, note that the server machine must have auditing of object access turned on for these audits to be captured in the security event log.

What you'll see after enabling auditing is that each call to AccessCheck results in an audit entry, where the object name in the entry is whatever string you passed as the first argument to AccessCheck. The operation's friendly name will be shown in the audit, along with the client's name. If the check succeeded, a successful access will be logged; otherwise you'll see a failed access in the log. Whether you see both success and failure audits depends on what level of object access auditing you enabled via the Windows security policy.

Conclusion

Authorization Manager is an important tool for building secure systems on Windows. It expands on the idea of role-based security popularized by MTS and COM+, but can be used by any server application, not just COM-based servers. Authorization Manager strives to help you centralize security logic into a concise security policy that can be stored in Active Directory, and it provides a simple API for performing access checks. Runtime auditing satisfies a longtime need as well.

Authorization Manager has loads of features, and part of your job when writing secure code is to figure out what subset of those features your application requires. And finally, remember to keep things as simple as possible to avoid opening security holes.

For background information see:
Programming Windows Security by Keith Brown (Addison-Wesley, 2000)

Keith Brown is an independent consultant who specializes in application security on the Windows platform. He is the author of Programming Windows Security (Addison-Wesley, 2000), and teaches regularly for DevelopMentor. Keith is working on a new book which you can read online at https://www.develop.com/kbrown.