< Summary - CoreWCF Coverage — PR #1766

Information
Class: CoreWCF.Dispatcher.SecurityImpersonationBehavior
Assembly: CoreWCF.Primitives
File(s): /home/runner/work/CoreWCF/CoreWCF/src/CoreWCF.Primitives/src/CoreWCF/Dispatcher/SecurityImpersonationBehavior.cs
Line coverage
36%
Covered lines: 60
Uncovered lines: 106
Coverable lines: 166
Total lines: 505
Line coverage: 36.1%
Branch coverage
32%
Covered branches: 33
Total branches: 102
Branch coverage: 32.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/CoreWCF/CoreWCF/src/CoreWCF.Primitives/src/CoreWCF/Dispatcher/SecurityImpersonationBehavior.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4using System;
 5using System.Collections.Generic;
 6using System.DirectoryServices;
 7using System.Runtime.CompilerServices;
 8using System.Security.Principal;
 9using System.Threading;
 10using CoreWCF.Description;
 11using CoreWCF.Diagnostics;
 12using CoreWCF.IdentityModel.Claims;
 13using CoreWCF.IdentityModel.Policy;
 14using CoreWCF.Runtime;
 15using CoreWCF.Security;
 16using CoreWCF.Security.Tokens;
 17using ClaimsIdentity = System.Security.Claims.ClaimsIdentity;
 18using ClaimsPrincipal = System.Security.Claims.ClaimsPrincipal;
 19
 20namespace CoreWCF.Dispatcher
 21{
 22    internal sealed class SecurityImpersonationBehavior
 23    {
 24        private readonly PrincipalPermissionMode _principalPermissionMode;
 25        private readonly bool _impersonateCallerForAllOperations;
 26        private readonly Dictionary<string, string> _ncNameMap;
 27
 28        //Dictionary<string, string> domainNameMap;
 29        private Random _random;
 30        private const int maxDomainNameMapSize = 5;
 31        private static WindowsPrincipal s_anonymousWindowsPrincipal;
 32        private static string s_directoryServerName = null;
 33
 34        //AuditLevel auditLevel = ServiceSecurityAuditBehavior.defaultMessageAuthenticationAuditLevel;
 35        //AuditLogLocation auditLogLocation = ServiceSecurityAuditBehavior.defaultAuditLogLocation;
 36        //bool suppressAuditFailure = ServiceSecurityAuditBehavior.defaultSuppressAuditFailure;
 37
 63438        private SecurityImpersonationBehavior(DispatchRuntime dispatch)
 39        {
 63440            _principalPermissionMode = dispatch.PrincipalPermissionMode;
 63441            _impersonateCallerForAllOperations = dispatch.ImpersonateCallerForAllOperations;
 42            //this.auditLevel = dispatch.MessageAuthenticationAuditLevel;
 43            //this.auditLogLocation = dispatch.SecurityAuditLogLocation;
 44            //this.suppressAuditFailure = dispatch.SuppressAuditFailure;
 63445            _ncNameMap = new Dictionary<string, string>(maxDomainNameMapSize, StringComparer.OrdinalIgnoreCase);
 63446        }
 47
 48        public static SecurityImpersonationBehavior CreateIfNecessary(DispatchRuntime dispatch)
 49        {
 66150            if (IsSecurityBehaviorNeeded(dispatch))
 51            {
 63452                return new SecurityImpersonationBehavior(dispatch);
 53            }
 54            else
 55            {
 2756                return null;
 57            }
 58        }
 59
 60        private static WindowsPrincipal AnonymousWindowsPrincipal
 61        {
 62            get
 63            {
 064                if (s_anonymousWindowsPrincipal == null)
 65                {
 066                    s_anonymousWindowsPrincipal = new WindowsPrincipal(WindowsIdentity.GetAnonymous());
 67                }
 68
 069                return s_anonymousWindowsPrincipal;
 70            }
 71        }
 72
 73        private static bool IsSecurityBehaviorNeeded(DispatchRuntime dispatch)
 74        {
 66175            if (dispatch.PrincipalPermissionMode != PrincipalPermissionMode.None)
 76            {
 63477                return true;
 78            }
 79
 80            // Impersonation behavior is required if
 81            // 1) Contract requires it or
 82            // 2) Contract allows it and config requires it
 5483            for (int i = 0; i < dispatch.Operations.Count; i++)
 84            {
 485                DispatchOperation operation = dispatch.Operations[i];
 86
 487                if (operation.Impersonation == ImpersonationOption.Required)
 88                {
 089                    return true;
 90                }
 491                else if (operation.Impersonation == ImpersonationOption.NotAllowed)
 92                {
 93                    // a validation rule enforces that config cannot require impersonation in this case
 494                    return false;
 95                }
 96            }
 97
 98            // contract allows impersonation. Return true if config requires it.
 2399            return dispatch.ImpersonateCallerForAllOperations;
 100        }
 101
 102        [MethodImpl(MethodImplOptions.NoInlining)]
 103        private IPrincipal SetCurrentThreadPrincipal(ServiceSecurityContext securityContext, out bool isThreadPrincipalS
 104        {
 79105            IPrincipal result = null;
 79106            IPrincipal principal = null;
 107
 79108            ClaimsPrincipal claimsPrincipal = OperationContext.Current.ClaimsPrincipal;
 109
 79110            if (_principalPermissionMode == PrincipalPermissionMode.UseWindowsGroups)
 111            {
 76112                if (claimsPrincipal is WindowsPrincipal)
 113                {
 0114                    principal = claimsPrincipal;
 115                }
 76116                else if (securityContext.PrimaryIdentity != null && securityContext.PrimaryIdentity is GenericIdentity)
 117                {
 76118                    principal = new ClaimsPrincipal(securityContext.PrimaryIdentity);
 119                }
 120                else
 121                {
 0122                    principal = GetWindowsPrincipal(securityContext);
 123                }
 124            }
 3125            else if (_principalPermissionMode == PrincipalPermissionMode.Custom)
 126            {
 3127                principal = GetCustomPrincipal(securityContext);
 128            }
 0129            else if (_principalPermissionMode == PrincipalPermissionMode.Always)
 130            {
 0131                principal = claimsPrincipal ?? new ClaimsPrincipal(new ClaimsIdentity());
 132            }
 133
 79134            if (principal != null)
 135            {
 79136                result = Thread.CurrentPrincipal;
 79137                Thread.CurrentPrincipal = principal;
 79138                isThreadPrincipalSet = true;
 139            }
 140            else
 141            {
 0142                isThreadPrincipalSet = false;
 143            }
 144
 79145            return result;
 146        }
 147
 148        [MethodImpl(MethodImplOptions.NoInlining)]
 149        private static IPrincipal GetCustomPrincipal(ServiceSecurityContext securityContext)
 150        {
 3151            if (securityContext.AuthorizationContext.Properties.TryGetValue(SecurityUtils.Principal, out object obj) && 
 152            {
 3153                return customPrincipal;
 154            }
 155            else
 156            {
 0157                throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR.NoPrincipalSp
 158            }
 159        }
 160
 161        internal bool IsSecurityContextImpersonationRequired(MessageRpc rpc)
 162        {
 2486163            return ((rpc.Operation.Impersonation == ImpersonationOption.Required)
 2486164                || ((rpc.Operation.Impersonation == ImpersonationOption.Allowed) && _impersonateCallerForAllOperations))
 165        }
 166
 167        internal bool IsImpersonationEnabledOnCurrentOperation(MessageRpc rpc)
 168        {
 0169            return IsSecurityContextImpersonationRequired(rpc) ||
 0170                    _principalPermissionMode != PrincipalPermissionMode.None;
 171        }
 172
 173        public T RunImpersonated<T>(MessageRpc rpc, Func<T> func)
 174        {
 2486175            T returnValue = default;
 2486176            IPrincipal originalPrincipal = null;
 2486177            bool isThreadPrincipalSet = false;
 178            ServiceSecurityContext securityContext;
 2486179            bool setThreadPrincipal = _principalPermissionMode != PrincipalPermissionMode.None;
 2486180            bool isSecurityContextImpersonationOn = IsSecurityContextImpersonationRequired(rpc);
 2486181            if (setThreadPrincipal || isSecurityContextImpersonationOn)
 182            {
 2486183                securityContext = GetAndCacheSecurityContext(rpc);
 184            }
 185            else
 186            {
 0187                securityContext = null;
 188            }
 189
 2486190            if (setThreadPrincipal && securityContext != null)
 191            {
 79192                originalPrincipal = SetCurrentThreadPrincipal(securityContext, out isThreadPrincipalSet);
 193            }
 194
 195            try
 196            {
 2486197                if (isSecurityContextImpersonationOn)
 198                {
 0199                    returnValue = RunImpersonated2(rpc, securityContext, isSecurityContextImpersonationOn, func);
 200                }
 201                else
 202                {
 2486203                    returnValue = func();
 204                }
 2486205            }
 206            finally
 207            {
 2486208                if (isThreadPrincipalSet)
 209                {
 79210                    Thread.CurrentPrincipal = originalPrincipal;
 211                }
 2486212            }
 213
 2486214            return returnValue;
 215        }
 216
 217        private T RunImpersonated2<T>(MessageRpc rpc, ServiceSecurityContext securityContext, bool isSecurityContextImpe
 218        {
 0219            T returnValue = default;
 220            try
 221            {
 0222                if (isSecurityContextImpersonationOn)
 223                {
 0224                    if (securityContext == null)
 225                    {
 0226                        throw TraceUtility.ThrowHelperError(new InvalidOperationException(SR.SFxSecurityContextPropertyM
 227                    }
 228
 0229                    WindowsIdentity impersonationToken = securityContext.WindowsIdentity;
 0230                    if (impersonationToken.User != null)
 231                    {
 0232                        returnValue = WindowsIdentity.RunImpersonated(impersonationToken.AccessToken, func);
 233                    }
 0234                    else if (securityContext.PrimaryIdentity is WindowsSidIdentity sidIdentity)
 235                    {
 0236                        if (sidIdentity.SecurityIdentifier.IsWellKnown(WellKnownSidType.AnonymousSid))
 237                        {
 238                            // This requires P/Invokes to achieve on Windows. Not sure how to achieve it on Linux. For n
 239                            // A strategy is needed to cleanly move code which makes P/Invoke calls into a different pac
 240                            // to request support in WindowsIdentity in a future release of .NET Core and require that t
 0241                            throw new PlatformNotSupportedException("Anonymous impersonation");
 242                        }
 243                        else
 244                        {
 0245                            string fullyQualifiedDomainName = GetUpnFromDownlevelName(sidIdentity.Name);
 0246                            using (WindowsIdentity windowsIdentity = new WindowsIdentity(fullyQualifiedDomainName))
 247                            {
 0248                                returnValue = WindowsIdentity.RunImpersonated(windowsIdentity.AccessToken, func);
 0249                            }
 250                        }
 251                    }
 252                    else
 253                    {
 0254                        throw TraceUtility.ThrowHelperError(new InvalidOperationException(SR.Format(SR.SecurityContextDo
 255                    }
 256                }
 257
 258                //SecurityTraceRecordHelper.TraceImpersonationSucceeded(rpc.EventTraceActivity, rpc.Operation);
 259
 260                // update the impersonation succeed audit
 261                //if (AuditLevel.Success == (this.auditLevel & AuditLevel.Success))
 262                //{
 263                //    SecurityAuditHelper.WriteImpersonationSuccessEvent(this.auditLogLocation,
 264                //        this.suppressAuditFailure, rpc.Operation.Name, SecurityUtils.GetIdentityNamesFromContext(secur
 265                //}
 0266            }
 267            catch (Exception ex)
 268            {
 0269                if (Fx.IsFatal(ex))
 270                {
 0271                    throw;
 272                }
 273                //SecurityTraceRecordHelper.TraceImpersonationFailed(rpc.EventTraceActivity, rpc.Operation, ex);
 274
 275                //
 276                // Update the impersonation failure audit
 277                // Copy SecurityAuthorizationBehavior.Audit level to here!!!
 278                //
 279                //                if (AuditLevel.Failure == (this.auditLevel & AuditLevel.Failure))
 280                //                {
 281                //                    try
 282                //                    {
 283                //                        string primaryIdentity;
 284                //                        if (securityContext != null)
 285                //                            primaryIdentity = SecurityUtils.GetIdentityNamesFromContext(securityContex
 286                //                        else
 287                //                            primaryIdentity = SecurityUtils.AnonymousIdentity.Name;
 288
 289                //                        SecurityAuditHelper.WriteImpersonationFailureEvent(this.auditLogLocation,
 290                //                            this.suppressAuditFailure, rpc.Operation.Name, primaryIdentity, ex);
 291                //                    }
 292                //#pragma warning suppress 56500
 293                //                    catch (Exception auditException)
 294                //                    {
 295                //                        if (Fx.IsFatal(auditException))
 296                //                            throw;
 297
 298                //                        DiagnosticUtility.TraceHandledException(auditException, TraceEventType.Error);
 299                //                    }
 300                //                }
 301                throw;
 302            }
 303
 0304            return returnValue;
 305        }
 306
 307        private IPrincipal GetWindowsPrincipal(ServiceSecurityContext securityContext)
 308        {
 0309            WindowsIdentity wid = securityContext.WindowsIdentity;
 0310            if (!wid.IsAnonymous)
 311            {
 0312                return new WindowsPrincipal(wid);
 313            }
 314
 0315            if (securityContext.PrimaryIdentity is WindowsSidIdentity wsid)
 316            {
 0317                return new WindowsSidPrincipal(wsid, securityContext);
 318            }
 319
 0320            return AnonymousWindowsPrincipal;
 321        }
 322
 323        private ServiceSecurityContext GetAndCacheSecurityContext(MessageRpc rpc)
 324        {
 2486325            ServiceSecurityContext securityContext = rpc.SecurityContext;
 326
 2486327            if (!rpc.HasSecurityContext)
 328            {
 2486329                SecurityMessageProperty securityContextProperty = rpc.Request.Properties.Security;
 2486330                if (securityContextProperty == null)
 331                {
 2407332                    securityContext = null; // SecurityContext.Anonymous
 333                }
 334                else
 335                {
 79336                    securityContext = securityContextProperty.ServiceSecurityContext;
 79337                    if (securityContext == null)
 338                    {
 0339                        throw TraceUtility.ThrowHelperError(new InvalidOperationException(SR.Format(SR.SecurityContextMi
 340                    }
 341                }
 342
 2486343                rpc.SecurityContext = securityContext;
 2486344                rpc.HasSecurityContext = true;
 345            }
 346
 2486347            return securityContext;
 348        }
 349
 350        private string GetUpnFromDownlevelName(string downlevelName)
 351        {
 352            // On Desktop this code calls SECUR32.DLL!TranslateName to translate just the domain part of the downlevel n
 353            // It then removes the trailing slash and joines username, '@' and the canonical name to create the Upn name
 354            // to make future lookups quicker. This is wrong and only works by happy accident of convention. Here's the 
 355            //
 356            // An organization Example Inc. has multiple domains for different departments in the company. Two of them a
 357            // in the domain are first name followed by initial of last name. The AD domain names for these are engineer
 358            // same forest. The format of the UPN account names are firstname.lastname@example.org. The engineer employe
 359            // his UPN name is bob.smith@example.org. The implementation on Desktop will translate his domain account na
 360            //
 361            // The incorrect implementation on Desktop allows for a small cache which makes lookups really fast because 
 362            // the domain name correctly, every account mapping would need to be cached. For now we're only caching the 
 363            // call to the DC to get the mapping. There are two potential improvements that can be done here.
 364            //   1. Use a MRU cache to cache the full mapping.
 365            //   2. Further up the call stack we actually have the user SID. It might be better to lookup the user by SI
 366
 0367            if (downlevelName == null)
 368            {
 0369                throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull(nameof(downlevelName));
 370            }
 0371            int delimiterPos = downlevelName.IndexOf('\\');
 0372            if ((delimiterPos < 0) || (delimiterPos == 0) || (delimiterPos == downlevelName.Length - 1))
 373            {
 0374                throw DiagnosticUtility.ExceptionUtility.ThrowHelperWarning(new InvalidOperationException(SR.Format(SR.D
 375            }
 0376            string shortDomainName = downlevelName.Substring(0, delimiterPos + 1);
 0377            string userName = downlevelName.Substring(delimiterPos + 1);
 0378            string ncName = null;
 379            bool found;
 380
 381            // 1) Read from cache
 0382            lock (_ncNameMap)
 383            {
 0384                found = _ncNameMap.TryGetValue(shortDomainName, out ncName);
 0385            }
 386
 387            // 2) Not found, do expensive look up
 0388            if (!found || s_directoryServerName == null)
 389            {
 0390                using (DirectoryEntry rootDse = new DirectoryEntry("LDAP://RootDSE"))
 391                {
 392                    // No need to re-check and/or update under lock as the retrieved value will be the same each time. T
 393                    // if this is retrieved more than once.
 0394                    if (s_directoryServerName == null)
 395                    {
 0396                        s_directoryServerName = rootDse.Properties["dnsHostName"].Value.ToString();
 397                    }
 398
 0399                    if (!found)
 400                    {
 401                        // Retrieve the Configuration Naming Context from RootDSE
 0402                        string configNC = rootDse.Properties["configurationNamingContext"].Value.ToString();
 403
 0404                        DirectoryEntry configSearchRoot = new DirectoryEntry("LDAP://" + configNC);
 0405                        DirectorySearcher configSearch = new DirectorySearcher(configSearchRoot)
 0406                        {
 0407                            Filter = $"(&(NETBIOSName={shortDomainName})(objectClass=crossRef))"
 0408                        };
 409
 410                        // Configure search to return ncname attribute
 0411                        configSearch.PropertiesToLoad.Add("ncname");
 412
 0413                        SearchResult forestPartition = configSearch.FindOne();
 0414                        if (forestPartition == null)
 415                        {
 0416                            throw DiagnosticUtility.ExceptionUtility.ThrowHelperWarning(new InvalidOperationException(SR
 417                        }
 418
 0419                        ncName = forestPartition.Properties["ncname"][0].ToString();
 420
 421                        // Save in cache (remove a random item if cache is full)
 0422                        lock (_ncNameMap)
 423                        {
 0424                            if (_ncNameMap.Count >= maxDomainNameMapSize)
 425                            {
 0426                                if (_random == null)
 427                                {
 0428                                    _random = new Random(unchecked((int)DateTime.Now.Ticks));
 429                                }
 0430                                int victim = _random.Next() % _ncNameMap.Count;
 0431                                foreach (string key in _ncNameMap.Keys)
 432                                {
 0433                                    if (victim <= 0)
 434                                    {
 0435                                        _ncNameMap.Remove(key);
 0436                                        break;
 437                                    }
 0438                                    --victim;
 439                                }
 440                            }
 0441                            _ncNameMap[shortDomainName] = ncName;
 0442                        }
 443                    }
 0444                }
 445            }
 446
 0447            string ldapDomainEntryPath = @"LDAP://" + s_directoryServerName + @"/" + ncName;
 0448            using (DirectoryEntry domainEntry = new DirectoryEntry(ldapDomainEntryPath))
 449            {
 0450                using (DirectorySearcher searcher = new DirectorySearcher(domainEntry))
 451                {
 0452                    searcher.SearchScope = SearchScope.Subtree;
 0453                    searcher.PropertiesToLoad.Add("userPrincipalName");
 0454                    searcher.Filter = $"(&(objectClass=user)(samAccountName={userName}))";
 455
 0456                    SearchResult userResult = searcher.FindOne();
 0457                    if (userResult == null)
 458                    {
 0459                        throw DiagnosticUtility.ExceptionUtility.ThrowHelperWarning(new InvalidOperationException(SR.For
 460                    }
 461
 0462                    return userResult.Properties["userPrincipalName"][0].ToString();
 463                }
 464            }
 0465        }
 466
 467        private class WindowsSidPrincipal : IPrincipal
 468        {
 469            private readonly WindowsSidIdentity _identity;
 470            private readonly ServiceSecurityContext _securityContext;
 471
 0472            public WindowsSidPrincipal(WindowsSidIdentity identity, ServiceSecurityContext securityContext)
 473            {
 0474                _identity = identity;
 0475                _securityContext = securityContext;
 0476            }
 477
 478            public IIdentity Identity
 479            {
 0480                get { return _identity; }
 481            }
 482
 483            public bool IsInRole(string role)
 484            {
 0485                if (role == null)
 486                {
 0487                    throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull(nameof(role));
 488                }
 489
 0490                NTAccount account = new NTAccount(role);
 0491                Claim claim = Claim.CreateWindowsSidClaim((SecurityIdentifier)account.Translate(typeof(SecurityIdentifie
 0492                AuthorizationContext authContext = _securityContext.AuthorizationContext;
 0493                for (int i = 0; i < authContext.ClaimSets.Count; i++)
 494                {
 0495                    ClaimSet claimSet = authContext.ClaimSets[i];
 0496                    if (claimSet.ContainsClaim(claim))
 497                    {
 0498                        return true;
 499                    }
 500                }
 0501                return false;
 502            }
 503        }
 504    }
 505}