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 retrieveHere 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?
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.
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());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());
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.
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