Taming the ActionCatalog in SCSF

The Smart Client Software Factory provides additional capbility above and beyond what CAB (the Composite Application UI Block) has. A little known gem is the ActionCatalog which can ease your pain in security trimming your application.

For example suppose you have a system where you want to hide a menu item from people that don't have access to it. This is pretty typical and generally ends up in having to scatter your code with if(User.IsInRole("Administrator")) statements, which can get pretty ugly real quick. The ActionCatalog system in SCSF helps you avoid this.

Here's a typical example. I've created a new Business Module and in the ModuleController I'm extending the menu by adding items to it:

public class ModuleController : WorkItemController

{

    public override void Run()

    {

        ExtendMenu();

    }

 

    private void ExtendMenu()

    {

        ToolStripMenuItem conditionalMenu = new ToolStripMenuItem("Conditional Code");

        if (canManageUsers())

        {

            conditionalMenu.DropDownItems.Add(new ToolStripMenuItem("Manage Users"));

        }

        if (canManageAdministrators())

        {

            conditionalMenu.DropDownItems.Add(new ToolStripMenuItem("Manage Administrators"));

        }

        WorkItem.UIExtensionSites[UIExtensionSiteNames.MainMenu].Add(conditionalMenu);

    }

 

    private bool canManageAdministrators()

    {

        string userName = Thread.CurrentPrincipal.Identity.Name;

        return

            Thread.CurrentPrincipal.Identity.IsAuthenticated &&

            userName.ToLower().Equals("domain\\admin");           

    }

 

    private bool canManageUsers()

    {

        string userName = Thread.CurrentPrincipal.Identity.Name;

        return

            Thread.CurrentPrincipal.Identity.IsAuthenticated &&

            userName.ToLower().Equals("domain\\joeuser");

    }

}

For each menu item I want to add I make a call to a method to check if the user has access or not. In the example above I'm checking two conditions. First the user has to be authenticated to the domain, then for each specific menu item I'm checking to see another condition (in this case comparing the user name, however I could do something like check to see if they're in a domain group or not).

Despite the fact that I could do a *little* bit of refactoring here, it's still ugly. I could for example extract the duplicate code on checking to see if the user is authenticated then do my specific compares. Another thing I could do is call out to a security service (say something that wraps AzMan or maybe the ASP.NET Membership Provider) to get back a conditional true/false on the users access. However with this approach I'm still stuck with these conditional statements and no matter what I do, my code smells.

Enter the ActionCatalog. A set of a few classes inside of SCSF that makes security trimming easy and makes your code more maintainable. To use the ActionCatalog there are a few steps you have to do:

  • Create a class to hold your actions
  • Register the class with a WorkItem
  • Add conditions for allowing actions to be executed 
  • Execute the actions

Setting up the Catalog 
Let's start with the changes to the ModuleController. You'll add some new methods to setup your actions, conditions, and then execute the actions. In this case the actions are directly manipulating the UI by adding menu items to it, but actions can be anything (invoked or tied to CommandHandlers) so you decide where the most appropriate split is. Here's the modified ModuleController:

public class ModuleController : WorkItemController

{

    private ToolStripMenuItem _rootMenuItem;

 

    public override void Run()

    {

        ExtendMenu();

        RegisterActionCatalog();

        RegisterActionConditions();

        ExecuteActions();

    }

 

    private void ExtendMenu()

    {

        _rootMenuItem = new ToolStripMenuItem("Action Catalog");

        WorkItem.UIExtensionSites[UIExtensionSiteNames.MainMenu].Add(_rootMenuItem);

    }

 

    private void ExecuteActions()

    {

        ActionCatalogService.Execute(ActionNames.ShowUserManagementMenu, WorkItem, this, _rootMenuItem);

        ActionCatalogService.Execute(ActionNames.ShowAdministratorManagementMenu, WorkItem, this, _rootMenuItem);

    }

 

    private void RegisterActionConditions()

    {

        ActionCatalogService.RegisterGeneralCondition(new AuthenticatedUsersCondition());

        ActionCatalogService.RegisterSpecificCondition(ActionNames.ShowUserManagementMenu, new UserCondition());

        ActionCatalogService.RegisterSpecificCondition(ActionNames.ShowAdministratorManagementMenu, new AdministratorCondition());

    }

 

    private void RegisterActionCatalog()

    {

        WorkItem.Items.AddNew<ModuleActions>();

    }

Here we've added an RegisterActionCatalog(), RegisterActionConditions(), and ExecuteActions() method (I could have put these into one method but I felt the act of registering actions, conditions and executing them voilated SRP so they're split out here).

Action Conditions
ActionNames is just a series of constants that I'll use to tag my action methods later using the Action attribute. The conditions are where the security checks are performed. Here's the general condition first which ensures any action is performed by an authenticated user:

class AuthenticatedUsersCondition : IActionCondition

{

    public bool CanExecute(string action, WorkItem context, object caller, object target)

    {

        return Thread.CurrentPrincipal.Identity.IsAuthenticated;

    }

}

Next are specific conditions for each action. As you saw from the AuthenticatedUsersCondition you do get the action passed into to the CanExecute call so you could either pass this off to a security service or check for each action in a common method. I've just created separate classes to handle specific actions but again, how you organize things is up to you.

class UserCondition : IActionCondition

{

    public bool CanExecute(string action, WorkItem context, object caller, object target)

    {

        string userName = Thread.CurrentPrincipal.Identity.Name;

        return userName.ToLower().Equals("domain\\joeuser");

    }

}

 

class AdministratorCondition : IActionCondition

{

    public bool CanExecute(string action, WorkItem context, object caller, object target)

    {

        string userName = Thread.CurrentPrincipal.Identity.Name;

        return userName.ToLower().Equals("domain\\admin");

    }

}

Both conditions contain the same code as before but are separated now and easier to maintain. Finally we call the Execute method on the actions themselves. Execute will pass in a work item (in this case the root workitem but it could be a child work item if you wanted), the caller and a target. In this case I want to add menu items to the UI so I'm passing in a ToolStripMenuItem object. The ModuleActions class contains our actions with each one tagged with the Action attribute. This keeps our code separate for each action but still lets us access the WorkItem and whatever objects we decide to pass into the actions.

The Action Catalog Itself

public class ModuleActions

{

    private WorkItem _workItem;

 

    [ServiceDependency]

    public WorkItem WorkItem

    {

        set { _workItem = value; }

        get { return _workItem; }

    }

 

    [Action(ActionNames.ShowUserManagementMenu)]

    public void ShowUserManagementMenu(object caller, object target)

    {

        ToolStripMenuItem conditionalMenu = (ToolStripMenuItem) target;

        conditionalMenu.DropDownItems.Add(new ToolStripMenuItem("Manage Users"));

    }

 

    [Action(ActionNames.ShowAdministratorManagementMenu)]

    public void ShowAdministratorManagementMenu(object caller, object target)

    {

        ToolStripMenuItem conditionalMenu = (ToolStripMenuItem)target;

        conditionalMenu.DropDownItems.Add(new ToolStripMenuItem("Manage Administrators"));

    }

}

Registering The Action Strategy
Calling ActionCatalogService.Execute isn't enough to invoke the action. In order for your Action to be registered (and called) the ActionStrategy has to be added to the builder chain. The ActionStrategy isn't added by default to an SCSF solution (even though you can resolve the IActionCatalogService since services and strategies are separate). Without the strategy in the builder chain, when it constructs the object it doesn't take into account the Action attribute.

So you need to add this to a stock SCSF project to get the action registered:

protected override void AddBuilderStrategies(Builder builder)

{

    base.AddBuilderStrategies(builder);

    builder.Strategies.AddNew<ActionStrategy>(BuilderStage.Initialization);

}

Once you've done this your action is registered and called when you invoke the catalog.Execute() method.

A few things about actions:

  • You don't have to call catalog.CanExecute for your actions. Just call catalog.Execute(). The Execute method makes a call to CanExecute to check if the action is allowed
  • You have to register an implementation of IActionCondition with the catalog in order to do checks via CanExecute. If you don't register a condition, any action is allowed

Alternative Approach
There are lots of ways to use the ActionCatalogService, this is just one of them. For example in your ModuleController you can set everything up, disable all commands, then execute your ActionCatalog which will enable menu items based on roles and security.

The ActionCatalog lets you keep your execution code separate from permissions management and who can access what. This is a simple example but with little effort you can have this call out to say a claims based WCF service, retrieve users and roles from something like an ASP.NET Membership Provider, and make applying feature level security (including UI trimming) to your Smart Client a breeze!

Hope that helps understand the ActionCatalog in SCSF! It's a pretty cool tool and can be leveraged quite easily in your apps.

1 Comment

Comments have been disabled for this content.