File: System\ServiceModel\Dispatcher\SecurityImpersonationBehavior.cs
Project: ndp\cdf\src\WCF\ServiceModel\System.ServiceModel.csproj (System.ServiceModel)
//-----------------------------------------------------------------------------
// 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<string, string> 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<string, string>(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;
                }
            }
        }
    }
}