Enterprise Library Roles Base Security with CSLA

Enterprise Library Roles Base Security with CSLA

Old forum URL: forums.lhotka.net/forums/t/3977.aspx


bigface3 posted on Monday, December 03, 2007

This is going to be a long post, and I really hope someone can help me out.

I'm trying to use Enterprise Library roles based security with CSLA and Windows Authentication.  I want to use a WindowsPrincipal identity object and check IsInRole() (which works perfectly on it's own), but I also want to read roles out of a database table and also check them using Enterprise Library.  Each role in the database table are saved in such a way that they can be converted into a EntLib IAuthorizationRule and can then be evaluted as a BooleanExpression.  In EnterpriseLibrary calling BooleanExpression.Evalute in turn calls IsInRole() on the IPrincipal object to check that users' groups.

For Example say my WindowsIdentity is in the following Active Directory groups: ProjectAdmins, ProjectTransfer, and ProjectView groups.  Simply calling Csla.ApplicationContext.User.IsInRole("ProjectAdmins") works perfectly fine for the CanGetObject, CanAdd, etc, and it also works fine for the authorization rules like AuthorizationRules.AllowRead("Name", "ProjectAdmins")...perfect.  Now I want to introduce EnterpriseLibrary into the picture.  Here I may have a role in my database table with a name of AllowTransfer that has the following expression: R:ProjectAdmins OR R:ProjectTransfer.  The expression field is a BooleanExpression that can be evaluated against the WindowsIdentity which cheks to see if I am in the ProjectAdmins group or the ProjectTransfer Active Directory groups.

I have implemented the following, and it works using a local dataportal but does not work using a remote WCF dataportal -- and I understand why, it's because my principal object is not attached to the WCF context since I tell CSLA that I am using Windows Authentication.  If I tell CSLA to use for example anything else as the Authentication method I always get a "System.ServiceModel.FaultException`1[System.ServiceModel.ExceptionDetail]: Invalid token for impersonation - it cannot be duplicated.." error because I am keeping a instance of the WindowsIdentity in my principal object - I think that's why I get the error.  The reason why it goes out to the dataportal is to get the roles from the Database table so it can also evaluate them.

Here is my code (I have both my client and WCF service configured to use Windows security at this point so I have not posted my app.config or web.config):

I start with the following in my Main:

AppDomain.CurrentDomain.SetPrincipalPolicy(System.Security.Principal.PrincipalPolicy.WindowsPrincipal);
MyLibrary.Security.CSLAPrincipal.Login(System.Threading.Thread.CurrentPrincipal);

then for instance I call:

Project.GetProject(5);  // 5 is the id number of the project I'm trying to retrieve

Here is my CSLAPrincipal object (CSLAPrincipal.cs):

using System;
using System.Collections.Generic;
using System.Text;
using System.Security;
using System.Security.Principal;
using Microsoft.Practices.EnterpriseLibrary.Security;
using Microsoft.Practices.EnterpriseLibrary.Security.Configuration;

namespace MyLibrary.Security
{
    [Serializable]
    public class CSLAPrincipal : Csla.Security.BusinessPrincipalBase
    {
        private static IPrincipal _windowsIdentity;

        private CSLAPrincipal(IIdentity identity)
            : base(identity) { }

        public static bool Login(IPrincipal principal)
        {
            _windowsIdentity = principal;
            return SetPrincipal(principal);
        }

        private static bool SetPrincipal(IPrincipal principal)
        {
            if (principal.Identity.IsAuthenticated)
            {
                CSLAPrincipal cslaPrincipal = new CSLAPrincipal(principal.Identity);
                Csla.ApplicationContext.User = cslaPrincipal;
            }
            return principal.Identity.IsAuthenticated;
        }

        private static bool Authorize(IPrincipal principal, string ruleName)
        {
            if (principal == null) throw new ArgumentNullException("principal");
            if (ruleName == null || ruleName.Length == 0) throw new ArgumentNullException("ruleName");

            IDictionary<string, IAuthorizationRule> rules = RuleCollection.ToDictionary();
            BooleanExpression booleanExpression = GetParsedExpression(ruleName, rules);
            if (booleanExpression == null)
            {
                return false;
                //throw new InvalidOperationException(String.Format("Authorization Rule {0} not found.", ruleName));
            }

            bool result = booleanExpression.Evaluate(principal);

            return result;
        }

        private static BooleanExpression GetParsedExpression(string ruleName, IDictionary<string, IAuthorizationRule> rules)
        {
            IAuthorizationRule rule = null;
            rules.TryGetValue(ruleName, out rule);

            if (rule == null) return null;

            Parser parser = new Parser();
            return parser.Parse(rule.Expression);

        }

        public override bool IsInRole(string role)
        {
            return _windowsIdentity.IsInRole(role) ||
            Authorize(_windowsIdentity, role);
        }
    }

    public partial class RuleCollection
    {
        public static IDictionary<string, IAuthorizationRule> ToDictionary()
        {
            IDictionary<string, IAuthorizationRule> rules = new Dictionary<string, IAuthorizationRule>();
            foreach (NameValuePair item in RuleCollection.GetRuleCollection())
            {
                AuthorizationRuleData rule = new AuthorizationRuleData(item.Key, item.Value);
                rules.Add(rule.Name, rule);
            }
            return rules;
        }
    }
}

RuleCollection is a Csla.NameValueListBase<string, string> defined in another class that basically gets all of the Roles from the database table.  I add extra functionality to RuleCollection by means of the ToDictionary() method shown above within CSLAPrincipal.cs that converts the Name/Value pair list into a Dictionary of AuthorizationRules.

So in my Project object when I call something like if (Csla.ApplicationContext.User.IsInRole("AllowTransfer")) CSLA calls my overridden IsInRole method which checks the groups of the windows principal and the roles from the name/value pair list which in turn really just convert to groups.  Note when I call the Login method of CSLAPrincipal I keep a static _windowsIdentity of the current Windows Identity.

 

Am I going about this all wrong?

RockfordLhotka replied on Tuesday, December 04, 2007

I'd suggest you not hold a reference to the windows principal. It is always available as an ambient value anyway (you can always directly get the current WindowsPrincipal object directly).

If you code just uses the current WindowsPrincipal, it should always be right - at least if you set up your virtual root security correctly to require impersonation, and configure WCF to pass the Windows identity over the wire.

Then you can tell CSLA to use custom authentication so it passes your custom principal too.

The current principal will always be the custom principal, but the WindowsPrincipal will be the user's principal, and the two will work together on both sides of the wire.

bigface3 replied on Tuesday, December 04, 2007

Thanx Rocky!

So I can always get the WindowPrincipal by doing something like this in my IsInRole() method? instead of holding a reference to it...

IPrincipal principal = new WindowsPrincipal(WindowsIdentity.GetCurrent());

RockfordLhotka replied on Tuesday, December 04, 2007

That is my understanding, yes. In fact I thought there was an easier way to get the WindowsPrincipal, but perhaps I’m mis-remembering.

 

Rocky

 

From: bigface3 [mailto:cslanet@lhotka.net]
Sent: Tuesday, December 04, 2007 10:21 AM
To: rocky@lhotka.net
Subject: Re: [CSLA .NET] Enterprise Library Roles Base Security with CSLA

 

Thanx Rocky!

So I can always get the WindowPrincipal by doing something like this in my IsInRole() method? instead of holding a reference to it...

IPrincipal principal = new WindowsPrincipal(WindowsIdentity.GetCurrent());



bigface3 replied on Wednesday, December 05, 2007

Thanx Rocky, this really helped!

To summarize what I did to get this to work: I created a Identity object that basically just implements the items it needs to from IIdentity (AuthenticationType, IsAuthenticated, and Name) and also contains the methods that check the EntLibe authorization rules (moved these from the custom Principal object).  In my Principal object I no longer keep a reference to the windows principal, instead in my IsInRole method I get the current windows user as I describes above, and I also get the current custom identity object from the principal, and check both the IsInRole of the windows principal and the EntLib authorization on the identity object.  I also set the CslaAuthentication in the *.config files to be something other than Windows.  My errors go away! and I can use my custom Principal on both a local dataportal and a remote dataportal.

Oh while I'm thinking about it, I do have a question about the CslaAuthentication setting in the config file.  Does it have to be set in the config file?  Can't it be set in code?  I tried briefly through code, and when I tried to build VS told me that the CslaAuthentication property was read-only.

RockfordLhotka replied on Wednesday, December 05, 2007

Config settings really should be set in the config file.

 

However, you can do a hack and set the value in System.Configuration.ConfigurationManager, because AppSettings is really just a dictionary and so is read-write.

 

I’m not sure that’s a great idea, but it does work. And really, the only reason I’m not sure it is a great idea is that you never know if Microsoft will someday decide to change that to a read-only dictionary or something.

 

Rocky

Copyright (c) Marimer LLC