Sunday, September 13, 2009

The trust relationship between the primary domain and the trusted domain failed.

Over the past month, I've spent a couple of hours puzzling over a bug in the implementation of Microsoft's System.Security.Principal.WindowsPrincipal.IsInRole. I want to say something about the general problem and then I'll talk specifically about this problem instance and how I worked around it.

The general form of the problem is something I often cited in code reviews at Anheuser-Busch. Now I believe that Microsoft owes its success much more to salesmanship than engineering ability, but this is a high-profile product with years of history in the field, so I'm surprised the bug has survived so long. I guess it's worth publishing my advice (which I'm sure did not originate with me):


  1. Throw or propagate exceptions at the right level of abstraction. The Java language designers were on to something with checked exceptions: exceptions really can be regarded as part of your API (see also criticism linked at c2). If you have a method String Profile.GetSetting(String) with two implementations, one of which uses ADO.NET for relational database storage while the other uses System.IO.File for filesystem storage, then callers should never see System.Data.Common.DbExceptions. If, as the maintainer of Profile.GetSetting, you anticipate ADO.NET or System.IO exceptions, then you should catch them and throw exceptions of some more appropriate type(s). It's OK and usually a good idea to use what you catch to initialize the InnerException property of what you throw: that's useful diagnostic information. But, you should make the effort to wrap the exceptions you propagate when doing so mitigates a leaky abstraction.

  2. Use distinct exception types for distinct equivalence classes of conditions you think callers might reasonably treat in different ways. I think it's helpful to think about Lisp conditions, which generalize exceptions, when you make design decisions involving errors.

  3. Document the exceptions you throw.



And without further ado, the specifics:

Bug 4541

Summary: The trust relationship between the primary domain and the trusted domain failed.
Product: REDACTED Reporter: Foy, Sean
Component: triageAssignee: Foy, Sean
Status: ASSIGNED

Severity: blocker CC: michael.jorgensen@REDACTED
Priority: P3

Version: unspecified

Hardware: All

OS: All

URL: http://tahaze-ww07.REDACTED/REDACTED/requests/19791117T120000?requestor=sean.foy@REDACTED
Whiteboard:
Time tracking:
Orig. Est. Actual Hours Hours Worked Hours Left %Complete Gain
8.0 1.1 1.0 0.1 90 6.9
Deadline:



Description Foy, Sean 2009-09-13 19:08:54 CDT
Steps to reproduce:
0. logout if you're not using Windows Authentication.
1. create a request
2. follow the link to the request from the assignment page
3. authenticate (implicitly or otherwise) using Windows Authentication
Expected results: request edit page
Actual results: authorization (IsInRole predicate evaluation on a
WindowsPrincipal) fails.


System.SystemException: The trust relationship between the primary domain and the trusted domain failed.

at
System.Security.Principal.NTAccount.TranslateToSids(IdentityReferenceCollection
sourceAccounts, Boolean& someFailed)
at System.Security.Principal.NTAccount.Translate(IdentityReferenceCollection
sourceAccounts, Type targetType, Boolean& someFailed)
at System.Security.Principal.NTAccount.Translate(IdentityReferenceCollection
sourceAccounts, Type targetType, Boolean forceSuccess)
at System.Security.Principal.WindowsPrincipal.IsInRole(String role)
at MVCUtils.IsInRole(String role) in
c:\documents-and-settings\sean.foy\my-documents\REDACTED\MvcUtils.cs:line 218
at REDACTED.REDACTED.IsInRole(String rolename) in
c:\documents-and-settings\sean.foy\my-documents\REDACTED\REDACTED.cs:line 284
at REDACTED.Viewee03e5938d284b4d944019fc1e9c8130.RenderViewLevel0() in
c:\documents-and-settings\sean.foy\my-documents\REDACTED\Views\Request\requestform.spark:line
20
at REDACTED.Viewee03e5938d284b4d944019fc1e9c8130.RenderView(TextWriter writer) in
c:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET
Files\REDACTED\930b9661\fd62837e49004099a1fddf1e5288ae22-1.cs:line 1240
at Spark.Web.Mvc.SparkView.Render(ViewContext viewContext, TextWriter
writer)
at System.Web.Mvc.ViewResultBase.ExecuteResult(ControllerContext context)
at
System.Web.Mvc.ControllerActionInvoker.InvokeActionResult(ControllerContext
controllerContext, ActionResult actionResult)
at
System.Web.Mvc.ControllerActionInvoker.<>c__DisplayClass11.<invokeactionresultwithfilters>b__e()
at
System.Web.Mvc.ControllerActionInvoker.InvokeActionResultFilter(IResultFilter
filter, ResultExecutingContext preContext, Func`1 continuation)
at
System.Web.Mvc.ControllerActionInvoker.<>c__DisplayClass11.<>c__DisplayClass13.<invokeactionresultwithfilters>b__10()
at
System.Web.Mvc.ControllerActionInvoker.InvokeActionResultWithFilters(ControllerContext
controllerContext, IList`1 filters, ActionResult actionResult)
at System.Web.Mvc.ControllerActionInvoker.InvokeAction(ControllerContext
controllerContext, String actionName)
at System.Web.Mvc.Controller.ExecuteCore()
at System.Web.Mvc.ControllerBase.Execute(RequestContext requestContext)
at
System.Web.Mvc.ControllerBase.System.Web.Mvc.IController.Execute(RequestContext
requestContext)
at System.Web.Mvc.MvcHandler.ProcessRequest(HttpContextBase httpContext)
at System.Web.Mvc.MvcHandler.ProcessRequest(HttpContext httpContext)
at
System.Web.Mvc.MvcHandler.System.Web.IHttpHandler.ProcessRequest(HttpContext
httpContext)
at
System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean&
completedSynchronously)

REDACTED, Version=1.0.4835.11, Culture=neutral, PublicKeyToken=null version
1.0.4835.11

Comment 1 Foy, Sean 2009-09-13 20:08:54 CDT

Additional hours worked: 1.0 There are no documented exceptions for WindowsPrincipal.IsInRole and although Google finds many reports of this exception, the most promising resolutions are vaguely described patches from Microsoft for SharePoint. But empirically I've found that I get reasonable answers for queries of the form
p.IsInRole(@"domain\group") and
p.IsInRole(@"group@domain")
where the domain name can be any non-empty string. But I get this exception for queries of the form
p.IsInRole(@"group").
Evidently IsInRole expects a domain name and throws this exception when none is present.


And here's the patch:

Index: MvcUtils.cs
===================================================================
--- MvcUtils.cs (revision 4851)
+++ MvcUtils.cs (working copy)
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Security.Principal;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
@@ -214,8 +215,22 @@
return result.ToString();
}

+ public static Boolean IsInRole(IPrincipal p, String role) {
+ try {
+ return p.Identity.IsAuthenticated &amp;&amp; p.IsInRole(role);
+ }
+ catch (SystemException e) {
+ // see bug 4541
+ if (!e.Message.StartsWith("The trust relationship between the primary domain and the trusted domain failed.")) {
+ throw;
+ }
+
+ return false;
+ }
+ }
+
public static Boolean IsInRole(String role) {
- return HttpContext.Current.User.Identity.IsAuthenticated &amp;&amp; HttpContext.Current.User.IsInRole(role);
+ return IsInRole(HttpContext.Current.User, role);
}
}

Index: TestMvcUtils.cs
===================================================================
--- TestMvcUtils.cs (revision 4851)
+++ TestMvcUtils.cs (working copy)
@@ -175,6 +175,32 @@
r => authorized.IsInRole(r)));
}

+ private System.Security.Principal.WindowsPrincipal getWindowsPrincipal() {
+ return
+ new System.Security.Principal.WindowsPrincipal(
+ System.Security.Principal.WindowsIdentity.GetCurrent());
+
+ }
+
+ [Test]
+ public void WindowsPrincipalIsInRoleExpectsDomain() {
+ var p = getWindowsPrincipal();
+ try {
+ p.IsInRole("whatever");
+ Assert.Fail("maybe we can simplify MvcUtils.IsInRole after all");
+ }
+ catch (SystemException) {
+ //expected
+ }
+ }
+
+ [Test]
+ public void IsInRoleToleratesAbsenceOfDomains() {
+ var p = getWindowsPrincipal();
+ MVCUtils.IsInRole(p, @"domain\whatever");
+ MVCUtils.IsInRole(p, "whatever@domain");
+ }
+
public Mock<httpcontextbase> mockContext(String applicationRoot, String requestUrl) {</httpcontextbase>
var appRoot = new Uri(applicationRoot);
var ctx = new Mock<httpcontextbase>();</httpcontextbase>
Index: Web.config
===================================================================
--- Web.config (revision 4851)
+++ Web.config (working copy)
@@ -12,8 +12,8 @@
<add key="ActiveDirectoryForestName" value="REDACTED"></add>
<add key="ActiveDirectoryUserName" value="REDACTED\svc.tahaze.websql"></add>
<add key="ActiveDirectoryPassword" value="REDACTED"></add>
- <add key="map-to-role-msl" value="REDACTED_Form_Editors"></add>
- <add key="map-to-role-admin" value="REDACTED_Admins"></add>
+ <add key="map-to-role-msl" value="REDACTED\REDACTED_Form_Editors"></add>
+ <add key="map-to-role-admin" value="REDACTED\REDACTED_Admins"></add>
<add key="MVCAuthnz.CookieAuthenticationHttpModule.signingKey" value="REDACTED"></add>
<add key="seanfoy.oopsnet.assemblyQualifiedName" value="seanfoy.oopsnet.ASPNetErrorHandler,seanfoy.oopsnet"></add>
<add key="seanfoy.oopsnet.handlerName" value="handleError"></add>
@@ -84,7 +84,6 @@


-->
-
<system.web></system.web>
<httpmodules></httpmodules>
<add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule, System.Web.Routing"></add>