//----------------------------------------------------------------------------- // Copyright (c) Microsoft Corporation. All rights reserved. //----------------------------------------------------------------------------- namespace System.ServiceModel.Dispatcher { using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.IdentityModel.Claims; using System.IdentityModel.Policy; using System.IdentityModel.Tokens; using System.Runtime; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Security; using System.Security.Principal; using System.ServiceModel; using System.ServiceModel.Activation; using System.ServiceModel.Description; using System.ServiceModel.Diagnostics; using System.ServiceModel.Security; using System.ServiceModel.Security.Tokens; using System.Text; using System.Threading; using ClaimsIdentity = System.Security.Claims.ClaimsIdentity; using ClaimsPrincipal = System.Security.Claims.ClaimsPrincipal; using EXTENDED_NAME_FORMAT = System.ServiceModel.ComIntegration.EXTENDED_NAME_FORMAT; using SafeCloseHandle = System.IdentityModel.SafeCloseHandle; using SafeNativeMethods = System.ServiceModel.ComIntegration.SafeNativeMethods; using Win32Error = System.ServiceModel.ComIntegration.Win32Error; internal sealed class SecurityImpersonationBehavior { PrincipalPermissionMode principalPermissionMode; object roleProvider; bool impersonateCallerForAllOperations; Dictionary domainNameMap; Random random; const int maxDomainNameMapSize = 5; static WindowsPrincipal anonymousWindowsPrincipal; AuditLevel auditLevel = ServiceSecurityAuditBehavior.defaultMessageAuthenticationAuditLevel; AuditLogLocation auditLogLocation = ServiceSecurityAuditBehavior.defaultAuditLogLocation; bool suppressAuditFailure = ServiceSecurityAuditBehavior.defaultSuppressAuditFailure; SecurityImpersonationBehavior(DispatchRuntime dispatch) { this.principalPermissionMode = dispatch.PrincipalPermissionMode; this.impersonateCallerForAllOperations = dispatch.ImpersonateCallerForAllOperations; this.auditLevel = dispatch.MessageAuthenticationAuditLevel; this.auditLogLocation = dispatch.SecurityAuditLogLocation; this.suppressAuditFailure = dispatch.SuppressAuditFailure; if (dispatch.IsRoleProviderSet) { ApplyRoleProvider(dispatch); } this.domainNameMap = new Dictionary(maxDomainNameMapSize, StringComparer.OrdinalIgnoreCase); } public static SecurityImpersonationBehavior CreateIfNecessary(DispatchRuntime dispatch) { if (IsSecurityBehaviorNeeded(dispatch)) { return new SecurityImpersonationBehavior(dispatch); } else { return null; } } static WindowsPrincipal AnonymousWindowsPrincipal { get { if (anonymousWindowsPrincipal == null) anonymousWindowsPrincipal = new WindowsPrincipal(WindowsIdentity.GetAnonymous()); return anonymousWindowsPrincipal; } } [MethodImpl(MethodImplOptions.NoInlining)] void ApplyRoleProvider(DispatchRuntime dispatch) { this.roleProvider = dispatch.RoleProvider; } static bool IsSecurityBehaviorNeeded(DispatchRuntime dispatch) { if (AspNetEnvironment.Current.RequiresImpersonation) { return true; } if (dispatch.PrincipalPermissionMode != PrincipalPermissionMode.None) { return true; } // Impersonation behavior is required if // 1) Contract requires it or // 2) Contract allows it and config requires it for (int i = 0; i < dispatch.Operations.Count; i++) { DispatchOperation operation = dispatch.Operations[i]; if (operation.Impersonation == ImpersonationOption.Required) { return true; } else if (operation.Impersonation == ImpersonationOption.NotAllowed) { // a validation rule enforces that config cannot require impersonation in this case return false; } } // contract allows impersonation. Return true if config requires it. return dispatch.ImpersonateCallerForAllOperations; } [MethodImpl(MethodImplOptions.NoInlining)] IPrincipal SetCurrentThreadPrincipal(ServiceSecurityContext securityContext, out bool isThreadPrincipalSet) { IPrincipal result = null; IPrincipal principal = null; ClaimsPrincipal claimsPrincipal = OperationContext.Current.ClaimsPrincipal; if (this.principalPermissionMode == PrincipalPermissionMode.UseWindowsGroups) { principal = ( claimsPrincipal is WindowsPrincipal ) ? claimsPrincipal : GetWindowsPrincipal( securityContext ); } else if (this.principalPermissionMode == PrincipalPermissionMode.UseAspNetRoles) { principal = new RoleProviderPrincipal(this.roleProvider, securityContext); } else if (this.principalPermissionMode == PrincipalPermissionMode.Custom) { principal = GetCustomPrincipal(securityContext); } else if (this.principalPermissionMode == PrincipalPermissionMode.Always) { principal = claimsPrincipal ?? new ClaimsPrincipal( new ClaimsIdentity() ); } if (principal != null) { result = Thread.CurrentPrincipal; Thread.CurrentPrincipal = principal; isThreadPrincipalSet = true; } else { isThreadPrincipalSet = false; } return result; } [MethodImpl(MethodImplOptions.NoInlining)] static IPrincipal GetCustomPrincipal(ServiceSecurityContext securityContext) { object customPrincipal; if (securityContext.AuthorizationContext.Properties.TryGetValue(SecurityUtils.Principal, out customPrincipal) && customPrincipal is IPrincipal) return (IPrincipal)customPrincipal; else throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR.GetString(SR.NoPrincipalSpecifiedInAuthorizationContext))); } internal bool IsSecurityContextImpersonationRequired(ref MessageRpc rpc) { return ((rpc.Operation.Impersonation == ImpersonationOption.Required) || ((rpc.Operation.Impersonation == ImpersonationOption.Allowed) && this.impersonateCallerForAllOperations)); } internal bool IsImpersonationEnabledOnCurrentOperation(ref MessageRpc rpc) { return this.IsSecurityContextImpersonationRequired(ref rpc) || AspNetEnvironment.Current.RequiresImpersonation || this.principalPermissionMode != PrincipalPermissionMode.None; } [Fx.Tag.SecurityNote(Critical = "Calls SecurityCritical method StartImpersonation2." + "Caller must ensure that this method is called at an appropriate time and that impersonationContext out param is Dispose()'d correctly.")] [SecurityCritical] public void StartImpersonation(ref MessageRpc rpc, out IDisposable impersonationContext, out IPrincipal originalPrincipal, out bool isThreadPrincipalSet) { impersonationContext = null; originalPrincipal = null; isThreadPrincipalSet = false; ServiceSecurityContext securityContext; bool setThreadPrincipal = this.principalPermissionMode != PrincipalPermissionMode.None; bool isSecurityContextImpersonationOn = IsSecurityContextImpersonationRequired(ref rpc); if (setThreadPrincipal || isSecurityContextImpersonationOn) securityContext = GetAndCacheSecurityContext(ref rpc); else securityContext = null; if (setThreadPrincipal && securityContext != null) originalPrincipal = this.SetCurrentThreadPrincipal(securityContext, out isThreadPrincipalSet); if (isSecurityContextImpersonationOn || AspNetEnvironment.Current.RequiresImpersonation) { impersonationContext = StartImpersonation2(ref rpc, securityContext, isSecurityContextImpersonationOn); } } [Fx.Tag.SecurityNote(Critical = "Calls SecurityCritical method HostedImpersonationContext.Impersonate." + "Caller must ensure that this method is called at an appropriate time and that result is Dispose()'d correctly.")] [SecurityCritical] IDisposable StartImpersonation2(ref MessageRpc rpc, ServiceSecurityContext securityContext, bool isSecurityContextImpersonationOn) { IDisposable impersonationContext = null; try { if (isSecurityContextImpersonationOn) { if (securityContext == null) throw TraceUtility.ThrowHelperError(new InvalidOperationException(SR.GetString(SR.SFxSecurityContextPropertyMissingFromRequestMessage)), rpc.Request); WindowsIdentity impersonationToken = securityContext.WindowsIdentity; if (impersonationToken.User != null) { impersonationContext = impersonationToken.Impersonate(); } else if (securityContext.PrimaryIdentity is WindowsSidIdentity) { WindowsSidIdentity sidIdentity = (WindowsSidIdentity)securityContext.PrimaryIdentity; if (sidIdentity.SecurityIdentifier.IsWellKnown(WellKnownSidType.AnonymousSid)) { impersonationContext = new WindowsAnonymousIdentity().Impersonate(); } else { string fullyQualifiedDomainName = GetUpnFromDownlevelName(sidIdentity.Name); using (WindowsIdentity windowsIdentity = new WindowsIdentity(fullyQualifiedDomainName, SecurityUtils.AuthTypeKerberos)) { impersonationContext = windowsIdentity.Impersonate(); } } } else throw TraceUtility.ThrowHelperError(new InvalidOperationException(SR.GetString(SR.SecurityContextDoesNotAllowImpersonation, rpc.Operation.Action)), rpc.Request); } else if (AspNetEnvironment.Current.RequiresImpersonation) { if (rpc.HostingProperty != null) { impersonationContext = rpc.HostingProperty.Impersonate(); } } SecurityTraceRecordHelper.TraceImpersonationSucceeded(rpc.EventTraceActivity, rpc.Operation); // update the impersonation succeed audit if (AuditLevel.Success == (this.auditLevel & AuditLevel.Success)) { SecurityAuditHelper.WriteImpersonationSuccessEvent(this.auditLogLocation, this.suppressAuditFailure, rpc.Operation.Name, SecurityUtils.GetIdentityNamesFromContext(securityContext.AuthorizationContext)); } } catch (Exception ex) { if (Fx.IsFatal(ex)) { throw; } SecurityTraceRecordHelper.TraceImpersonationFailed(rpc.EventTraceActivity, rpc.Operation, ex); // // Update the impersonation failure audit // Copy SecurityAuthorizationBehavior.Audit level to here!!! // if (AuditLevel.Failure == (this.auditLevel & AuditLevel.Failure)) { try { string primaryIdentity; if (securityContext != null) primaryIdentity = SecurityUtils.GetIdentityNamesFromContext(securityContext.AuthorizationContext); else primaryIdentity = SecurityUtils.AnonymousIdentity.Name; SecurityAuditHelper.WriteImpersonationFailureEvent(this.auditLogLocation, this.suppressAuditFailure, rpc.Operation.Name, primaryIdentity, ex); } #pragma warning suppress 56500 catch (Exception auditException) { if (Fx.IsFatal(auditException)) throw; DiagnosticUtility.TraceHandledException(auditException, TraceEventType.Error); } } throw; } return impersonationContext; } public void StopImpersonation(ref MessageRpc rpc, IDisposable impersonationContext, IPrincipal originalPrincipal, bool isThreadPrincipalSet) { try { if (IsSecurityContextImpersonationRequired(ref rpc) || AspNetEnvironment.Current.RequiresImpersonation) { if (impersonationContext != null) { impersonationContext.Dispose(); } } if (isThreadPrincipalSet) { Thread.CurrentPrincipal = originalPrincipal; } } #pragma warning suppress 56500 // covered by FxCOP catch { string message = null; try { message = SR.GetString(SR.SFxRevertImpersonationFailed0); } finally { DiagnosticUtility.FailFast(message); } } } IPrincipal GetWindowsPrincipal(ServiceSecurityContext securityContext) { WindowsIdentity wid = securityContext.WindowsIdentity; if (!wid.IsAnonymous) return new WindowsPrincipal(wid); WindowsSidIdentity wsid = securityContext.PrimaryIdentity as WindowsSidIdentity; if (wsid != null) return new WindowsSidPrincipal(wsid, securityContext); return AnonymousWindowsPrincipal; } ServiceSecurityContext GetAndCacheSecurityContext(ref MessageRpc rpc) { ServiceSecurityContext securityContext = rpc.SecurityContext; if (!rpc.HasSecurityContext) { SecurityMessageProperty securityContextProperty = rpc.Request.Properties.Security; if (securityContextProperty == null) securityContext = null; // SecurityContext.Anonymous else { securityContext = securityContextProperty.ServiceSecurityContext; if (securityContext == null) throw TraceUtility.ThrowHelperError(new InvalidOperationException(SR.GetString(SR.SecurityContextMissing, rpc.Operation.Name)), rpc.Request); } rpc.SecurityContext = securityContext; rpc.HasSecurityContext = true; } return securityContext; } string GetUpnFromDownlevelName(string downlevelName) { if (downlevelName == null) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("downlevelName"); } int delimiterPos = downlevelName.IndexOf('\\'); if ((delimiterPos < 0) || (delimiterPos == 0) || (delimiterPos == downlevelName.Length - 1)) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperWarning(new InvalidOperationException(SR.GetString(SR.DownlevelNameCannotMapToUpn, downlevelName))); } string shortDomainName = downlevelName.Substring(0, delimiterPos + 1); string userName = downlevelName.Substring(delimiterPos + 1); string fullDomainName; bool found; // 1) Read from cache lock (this.domainNameMap) { found = this.domainNameMap.TryGetValue(shortDomainName, out fullDomainName); } // 2) Not found, do expensive look up if (!found) { uint capacity = 50; StringBuilder fullyQualifiedDomainName = new StringBuilder((int)capacity); if (!SafeNativeMethods.TranslateName(shortDomainName, EXTENDED_NAME_FORMAT.NameSamCompatible, EXTENDED_NAME_FORMAT.NameCanonical, fullyQualifiedDomainName, out capacity)) { int errorCode = Marshal.GetLastWin32Error(); if (errorCode == (int)Win32Error.ERROR_INSUFFICIENT_BUFFER) { fullyQualifiedDomainName = new StringBuilder((int)capacity); if (!SafeNativeMethods.TranslateName(shortDomainName, EXTENDED_NAME_FORMAT.NameSamCompatible, EXTENDED_NAME_FORMAT.NameCanonical, fullyQualifiedDomainName, out capacity)) { errorCode = Marshal.GetLastWin32Error(); throw DiagnosticUtility.ExceptionUtility.ThrowHelperWarning(new InvalidOperationException(SR.GetString(SR.DownlevelNameCannotMapToUpn, downlevelName), new Win32Exception(errorCode))); } } else { throw DiagnosticUtility.ExceptionUtility.ThrowHelperWarning(new InvalidOperationException(SR.GetString(SR.DownlevelNameCannotMapToUpn, downlevelName), new Win32Exception(errorCode))); } } // trim the trailing / from fqdn fullyQualifiedDomainName = fullyQualifiedDomainName.Remove(fullyQualifiedDomainName.Length - 1, 1); fullDomainName = fullyQualifiedDomainName.ToString(); // 3) Save in cache (remove a random item if cache is full) lock (this.domainNameMap) { if (this.domainNameMap.Count >= maxDomainNameMapSize) { if (this.random == null) { this.random = new Random(unchecked((int)DateTime.Now.Ticks)); } int victim = this.random.Next() % this.domainNameMap.Count; foreach (string key in this.domainNameMap.Keys) { if (victim <= 0) { this.domainNameMap.Remove(key); break; } --victim; } } this.domainNameMap[shortDomainName] = fullDomainName; } } return userName + "@" + fullDomainName; } class WindowsSidPrincipal : IPrincipal { WindowsSidIdentity identity; ServiceSecurityContext securityContext; public WindowsSidPrincipal(WindowsSidIdentity identity, ServiceSecurityContext securityContext) { this.identity = identity; this.securityContext = securityContext; } public IIdentity Identity { get { return this.identity; } } public bool IsInRole(string role) { if (role == null) throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("role"); NTAccount account = new NTAccount(role); Claim claim = Claim.CreateWindowsSidClaim((SecurityIdentifier)account.Translate(typeof(SecurityIdentifier))); AuthorizationContext authContext = this.securityContext.AuthorizationContext; for (int i = 0; i < authContext.ClaimSets.Count; i++) { ClaimSet claimSet = authContext.ClaimSets[i]; if (claimSet.ContainsClaim(claim)) return true; } return false; } } class WindowsAnonymousIdentity { public IDisposable Impersonate() { // PreSharp Bug: Call 'Marshal.GetLastWin32Error' or 'Marshal.GetHRForLastWin32Error' before any other interop call. #pragma warning suppress 56523 // The LastWin32Error can be ignored here. IntPtr threadHandle = SafeNativeMethods.GetCurrentThread(); SafeCloseHandle tokenHandle; if (!SafeNativeMethods.OpenCurrentThreadToken(threadHandle, TokenAccessLevels.Impersonate, true, out tokenHandle)) { int error = Marshal.GetLastWin32Error(); System.ServiceModel.Diagnostics.Utility.CloseInvalidOutSafeHandle(tokenHandle); if (error == (int)System.ServiceModel.ComIntegration.Win32Error.ERROR_NO_TOKEN) { tokenHandle = new SafeCloseHandle(IntPtr.Zero, false); } else { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new Win32Exception(error)); } } if (!SafeNativeMethods.ImpersonateAnonymousUserOnCurrentThread(threadHandle)) { int error = Marshal.GetLastWin32Error(); throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new Win32Exception(error)); } return new ImpersonationContext(threadHandle, tokenHandle); } class ImpersonationContext : IDisposable { IntPtr threadHandle; SafeCloseHandle tokenHandle; bool disposed = false; public ImpersonationContext(IntPtr threadHandle, SafeCloseHandle tokenHandle) { this.threadHandle = threadHandle; this.tokenHandle = tokenHandle; } void Undo() { // PreSharp Bug: Call 'Marshal.GetLastWin32Error' or 'Marshal.GetHRForLastWin32Error' before any other interop call. #pragma warning suppress 56523 // The LastWin32Error can be ignored here. Fx.Assert(this.threadHandle == SafeNativeMethods.GetCurrentThread(), ""); // We are in the Dispose method. If a failure occurs we just have to ignore it. // PreSharp Bug: Call 'Marshal.GetLastWin32Error' or 'Marshal.GetHRForLastWin32Error' before any other interop call. // #pragma warning suppress 56523 // The LastWin32Error can be ignored here. if (!SafeNativeMethods.SetCurrentThreadToken(IntPtr.Zero, this.tokenHandle)) { int error = Marshal.GetLastWin32Error(); throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new SecurityException(SR.GetString(SR.RevertImpersonationFailure, new Win32Exception(error).Message))); } tokenHandle.Close(); } public void Dispose() { if (!this.disposed) { Undo(); } this.disposed = true; } } } } }