diff --git a/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsExportUsersForMappingProcessor.cs b/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsExportUsersForMappingProcessor.cs index 89f5c576f..34cec19c5 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsExportUsersForMappingProcessor.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsExportUsersForMappingProcessor.cs @@ -2,23 +2,15 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MigrationTools; using MigrationTools.Clients; -using MigrationTools._EngineV1.Configuration; -using MigrationTools._EngineV1.Configuration.Processing; - using MigrationTools.DataContracts; -using MigrationTools.DataContracts.Process; -using MigrationTools.EndpointEnrichers; using MigrationTools.Enrichers; using MigrationTools.Processors.Infrastructure; using MigrationTools.Tools; using Newtonsoft.Json; - namespace MigrationTools.Processors { /// @@ -29,11 +21,17 @@ namespace MigrationTools.Processors /// Work Items public class TfsExportUsersForMappingProcessor : TfsProcessor { - public TfsExportUsersForMappingProcessor(IOptions options, TfsCommonTools tfsCommonTools, ProcessorEnricherContainer processorEnrichers, IServiceProvider services, ITelemetryLogger telemetry, ILogger logger) : base(options, tfsCommonTools, processorEnrichers, services, telemetry, logger) + public TfsExportUsersForMappingProcessor( + IOptions options, + TfsCommonTools tfsCommonTools, + ProcessorEnricherContainer processorEnrichers, + IServiceProvider services, + ITelemetryLogger telemetry, + ILogger logger) + : base(options, tfsCommonTools, processorEnrichers, services, telemetry, logger) { } - new TfsExportUsersForMappingProcessorOptions Options => (TfsExportUsersForMappingProcessorOptions)base.Options; new TfsTeamProjectEndpoint Source => (TfsTeamProjectEndpoint)base.Source; @@ -44,13 +42,12 @@ protected override void InternalExecute() { Stopwatch stopwatch = Stopwatch.StartNew(); - if(string.IsNullOrEmpty(CommonTools.UserMapping.Options.UserMappingFile)) + if (string.IsNullOrEmpty(CommonTools.UserMapping.Options.UserMappingFile)) { Log.LogError("UserMappingFile is not set"); - throw new ArgumentNullException("UserMappingFile must be set on the TfsUserMappingToolOptions in CommonEnrichersConfig."); - } - + } + List usersToMap = new List(); if (Options.OnlyListUsersInWorkItems) { @@ -68,14 +65,20 @@ protected override void InternalExecute() Log.LogInformation("Found {usersToMap} total mapped", usersToMap.Count); } - usersToMap = usersToMap.Where(x => x.Source.FriendlyName != x.target?.FriendlyName).ToList(); + usersToMap = usersToMap.Where(x => x.Source.FriendlyName != x.Target?.FriendlyName).ToList(); Log.LogInformation("Filtered to {usersToMap} total viable mappings", usersToMap.Count); - Dictionary usermappings = usersToMap.ToDictionary(x => x.Source.FriendlyName, x => x.target?.FriendlyName); - System.IO.File.WriteAllText(CommonTools.UserMapping.Options.UserMappingFile, Newtonsoft.Json.JsonConvert.SerializeObject(usermappings, Formatting.Indented)); + Dictionary usermappings = []; + foreach (IdentityMapData userMapping in usersToMap) + { + // We cannot use ToDictionary(), because there can be multiple users with the same friendly name and so + // it would throw with duplicate key. This way we just overwrite the value – last item in source wins. + usermappings[userMapping.Source.FriendlyName] = userMapping.Target?.FriendlyName; + } + System.IO.File.WriteAllText(CommonTools.UserMapping.Options.UserMappingFile, JsonConvert.SerializeObject(usermappings, Formatting.Indented)); Log.LogInformation("Writen to: {LocalExportJsonFile}", CommonTools.UserMapping.Options.UserMappingFile); - ////////////////////////////////////////////////// + stopwatch.Stop(); - Log.LogInformation("DONE in {Elapsed} seconds"); + Log.LogInformation("DONE in {Elapsed} seconds", stopwatch.Elapsed); } } -} \ No newline at end of file +} diff --git a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsUserMappingTool.cs b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsUserMappingTool.cs index 0b68e63b1..c807eabc6 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsUserMappingTool.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsUserMappingTool.cs @@ -1,18 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.TeamFoundation.Common; using Microsoft.TeamFoundation.Server; using Microsoft.TeamFoundation.WorkItemTracking.Client; -using Microsoft.VisualStudio.Services.Commerce; using MigrationTools.DataContracts; -using MigrationTools.Enrichers; -using MigrationTools.Processors; using MigrationTools.Processors.Infrastructure; using MigrationTools.Tools.Infrastructure; @@ -29,7 +22,6 @@ public TfsUserMappingTool(IOptions options, IServiceP { } - private List GetUsersFromWorkItems(List workitems, List identityFieldsToCheck) { List foundUsers = new List(); @@ -52,22 +44,6 @@ private List GetUsersFromWorkItems(List workitems, List GetMappingFileData() try { var fileMaps = Newtonsoft.Json.JsonConvert.DeserializeObject>(fileData); - _UserMappings = fileMaps.ToDictionary(x => x.Source.FriendlyName, x => x.target?.FriendlyName); + _UserMappings = fileMaps.ToDictionary(x => x.Source.FriendlyName, x => x.Target?.FriendlyName); } catch (Exception) { _UserMappings = new Dictionary(); Log.LogError($"TfsUserMappingTool::GetMappingFileData [UserMappingFile|{Options.UserMappingFile}] <-- invalid - No mapping are applied!"); } - } return _UserMappings; } private List GetUsersListFromServer(IGroupSecurityService gss) { - Identity SIDS = gss.ReadIdentity(SearchFactor.AccountName, "Project Collection Valid Users", QueryMembership.Expanded); - var people = SIDS.Members.ToList().Where(x => x.Contains("\\")).Select(x => x); + Identity allIdentities = gss.ReadIdentity(SearchFactor.AccountName, "Project Collection Valid Users", QueryMembership.Expanded); + Log.LogInformation("TfsUserMappingTool::GetUsersListFromServer Found {count} identities (users and groups) in server.", allIdentities.Members.Length); List foundUsers = new List(); - Log.LogTrace("TfsUserMappingTool::GetUsersListFromServer:foundUsers\\ {@foundUsers}", foundUsers); - foreach (string user in people) + foreach (string sid in allIdentities.Members) { - Log.LogDebug("TfsUserMappingTool::GetUsersListFromServer::[user:{user}] Atempting to load user", user); + Log.LogDebug("TfsUserMappingTool::GetUsersListFromServer::[user:{user}] Atempting to load user", sid); try { - var bits = user.Split('\\'); - Identity sids = gss.ReadIdentity(SearchFactor.AccountName, bits[1], QueryMembership.Expanded); - if (sids != null) + Identity identity = gss.ReadIdentity(SearchFactor.Sid, sid, QueryMembership.Expanded); + if (identity is null) { - foundUsers.Add(new IdentityItemData() { FriendlyName = sids.DisplayName, AccountName = sids.AccountName }); + Log.LogDebug("TfsUserMappingTool::GetUsersListFromServer::[user:{user}] ReadIdentity returned null", sid); + } + else if ((identity.Type == IdentityType.WindowsUser) || (identity.Type == IdentityType.UnknownIdentityType)) + { + // UnknownIdentityType is set for users in Azure Entra ID. + foundUsers.Add(new IdentityItemData() + { + FriendlyName = identity.DisplayName, + AccountName = identity.AccountName + }); } else { - Log.LogDebug("TfsUserMappingTool::GetUsersListFromServer::[user:{user}] ReadIdentity returned null for {@bits}", user, bits); + Log.LogDebug("TfsUserMappingTool::GetUsersListFromServer::[user:{user}] Not applicable identity type {identityType}", sid, identity.Type); } - } catch (Exception ex) { Telemetry.TrackException(ex, null); - Log.LogWarning("TfsUserMappingTool::GetUsersListFromServer::[user:{user}] Failed With {Exception}", user, ex.Message); + Log.LogWarning("TfsUserMappingTool::GetUsersListFromServer::[user:{user}] Failed With {Exception}", sid, ex.Message); } - } + Log.LogInformation("TfsUserMappingTool::GetUsersListFromServer {count} user identities are applicable for mapping", foundUsers.Count); return foundUsers; } - public List GetUsersInSourceMappedToTarget(TfsProcessor processor) { Log.LogDebug("TfsUserMappingTool::GetUsersInSourceMappedToTarget"); if (Options.Enabled) { + Log.LogInformation($"TfsUserMappingTool::GetUsersInSourceMappedToTarget Loading identities from source server"); var sourceUsers = GetUsersListFromServer(processor.Source.GetService()); - Log.LogDebug($"TfsUserMappingTool::GetUsersInSourceMappedToTarget [SourceUsersCount|{sourceUsers.Count}]"); + Log.LogInformation($"TfsUserMappingTool::GetUsersInSourceMappedToTarget Loading identities from target server"); var targetUsers = GetUsersListFromServer(processor.Target.GetService()); - Log.LogDebug($"TfsUserMappingTool::GetUsersInSourceMappedToTarget [targetUsersCount|{targetUsers.Count}]"); - return sourceUsers.Select(sUser => new IdentityMapData { Source = sUser, target = targetUsers.SingleOrDefault(tUser => tUser.FriendlyName == sUser.FriendlyName) }).ToList(); + return sourceUsers.Select(sUser => new IdentityMapData { Source = sUser, Target = targetUsers.SingleOrDefault(tUser => tUser.FriendlyName == sUser.FriendlyName) }).ToList(); } else { Log.LogWarning("TfsUserMappingTool is disabled in settings. You may have users in the source that are not mapped to the target. "); return null; } - } - public List GetUsersInSourceMappedToTargetForWorkItems(TfsProcessor processor, List sourceWorkItems) { if (Options.Enabled) { - Dictionary result = new Dictionary(); List workItemUsers = GetUsersFromWorkItems(sourceWorkItems, Options.IdentityFieldsToCheck); Log.LogDebug($"TfsUserMappingTool::GetUsersInSourceMappedToTargetForWorkItems [workItemUsers|{workItemUsers.Count}]"); @@ -188,7 +164,6 @@ public List GetUsersInSourceMappedToTargetForWorkItems(TfsProce public class CaseInsensativeStringComparer : IEqualityComparer { - public bool Equals(string x, string y) { return x?.IndexOf(y, StringComparison.OrdinalIgnoreCase) >= 0; diff --git a/src/MigrationTools/DataContracts/IdentityItemData.cs b/src/MigrationTools/DataContracts/IdentityItemData.cs index 9dcae21a8..71346b48a 100644 --- a/src/MigrationTools/DataContracts/IdentityItemData.cs +++ b/src/MigrationTools/DataContracts/IdentityItemData.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace MigrationTools.DataContracts +namespace MigrationTools.DataContracts { public class IdentityItemData { @@ -13,6 +9,6 @@ public class IdentityItemData public class IdentityMapData { public IdentityItemData Source { get; set; } - public IdentityItemData target { get; set; } + public IdentityItemData Target { get; set; } } }