Skip to content

Commit 013ae47

Browse files
authored
TfsUserMappingTool: fix loading users from TFS connected to Active Directory (#2522)
`TfsUserMappingTool` is not working correctly in our scenario, which is: - As our source server, we have on-premise TFS 2018 connected to on-premise Active Directory. - As our target server, we have Azure DevOps connected to Azure Entra ID (formerly Azure Active Directory). `TfsUserMappingTool` is not working at all, because **it will not load any user** from our on-premise TFS server. The problem is this part: ```cs var people = SIDS.Members.ToList().Where(x => x.Contains("\\")).Select(x => x); ``` It processes only users, whose SID contains `\` character. But none of our users in TFS contains this. All data in `SIDS.Members` are SIDs of some kind of identity and we need to process them all. So the new logic is this: - All SIDs are processed, so identity for every one of them is retrieved from the server. - Identity type is checked if we can use this identity. Allowed identity types for mapping are `WindowsUser` and `UnknownIdentityType`. - All identities in Entra ID have type `UnknownIdentityType`. This works as expected and loads correct user lists from TFS and DevOps.
2 parents 28fc3d7 + 72b0c36 commit 013ae47

File tree

3 files changed

+48
-74
lines changed

3 files changed

+48
-74
lines changed

src/MigrationTools.Clients.TfsObjectModel/Processors/TfsExportUsersForMappingProcessor.cs

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,15 @@
22
using System.Collections.Generic;
33
using System.Diagnostics;
44
using System.Linq;
5-
using Microsoft.Extensions.DependencyInjection;
65
using Microsoft.Extensions.Logging;
76
using Microsoft.Extensions.Options;
8-
using MigrationTools;
97
using MigrationTools.Clients;
10-
using MigrationTools._EngineV1.Configuration;
11-
using MigrationTools._EngineV1.Configuration.Processing;
12-
138
using MigrationTools.DataContracts;
14-
using MigrationTools.DataContracts.Process;
15-
using MigrationTools.EndpointEnrichers;
169
using MigrationTools.Enrichers;
1710
using MigrationTools.Processors.Infrastructure;
1811
using MigrationTools.Tools;
1912
using Newtonsoft.Json;
2013

21-
2214
namespace MigrationTools.Processors
2315
{
2416
/// <summary>
@@ -29,11 +21,17 @@ namespace MigrationTools.Processors
2921
/// <processingtarget>Work Items</processingtarget>
3022
public class TfsExportUsersForMappingProcessor : TfsProcessor
3123
{
32-
public TfsExportUsersForMappingProcessor(IOptions<TfsExportUsersForMappingProcessorOptions> options, TfsCommonTools tfsCommonTools, ProcessorEnricherContainer processorEnrichers, IServiceProvider services, ITelemetryLogger telemetry, ILogger<TfsExportUsersForMappingProcessor> logger) : base(options, tfsCommonTools, processorEnrichers, services, telemetry, logger)
24+
public TfsExportUsersForMappingProcessor(
25+
IOptions<TfsExportUsersForMappingProcessorOptions> options,
26+
TfsCommonTools tfsCommonTools,
27+
ProcessorEnricherContainer processorEnrichers,
28+
IServiceProvider services,
29+
ITelemetryLogger telemetry,
30+
ILogger<TfsExportUsersForMappingProcessor> logger)
31+
: base(options, tfsCommonTools, processorEnrichers, services, telemetry, logger)
3332
{
3433
}
3534

36-
3735
new TfsExportUsersForMappingProcessorOptions Options => (TfsExportUsersForMappingProcessorOptions)base.Options;
3836

3937
new TfsTeamProjectEndpoint Source => (TfsTeamProjectEndpoint)base.Source;
@@ -44,13 +42,12 @@ protected override void InternalExecute()
4442
{
4543
Stopwatch stopwatch = Stopwatch.StartNew();
4644

47-
if(string.IsNullOrEmpty(CommonTools.UserMapping.Options.UserMappingFile))
45+
if (string.IsNullOrEmpty(CommonTools.UserMapping.Options.UserMappingFile))
4846
{
4947
Log.LogError("UserMappingFile is not set");
50-
5148
throw new ArgumentNullException("UserMappingFile must be set on the TfsUserMappingToolOptions in CommonEnrichersConfig.");
52-
}
53-
49+
}
50+
5451
List<IdentityMapData> usersToMap = new List<IdentityMapData>();
5552
if (Options.OnlyListUsersInWorkItems)
5653
{
@@ -68,14 +65,20 @@ protected override void InternalExecute()
6865
Log.LogInformation("Found {usersToMap} total mapped", usersToMap.Count);
6966
}
7067

71-
usersToMap = usersToMap.Where(x => x.Source.FriendlyName != x.target?.FriendlyName).ToList();
68+
usersToMap = usersToMap.Where(x => x.Source.FriendlyName != x.Target?.FriendlyName).ToList();
7269
Log.LogInformation("Filtered to {usersToMap} total viable mappings", usersToMap.Count);
73-
Dictionary<string, string> usermappings = usersToMap.ToDictionary(x => x.Source.FriendlyName, x => x.target?.FriendlyName);
74-
System.IO.File.WriteAllText(CommonTools.UserMapping.Options.UserMappingFile, Newtonsoft.Json.JsonConvert.SerializeObject(usermappings, Formatting.Indented));
70+
Dictionary<string, string> usermappings = [];
71+
foreach (IdentityMapData userMapping in usersToMap)
72+
{
73+
// We cannot use ToDictionary(), because there can be multiple users with the same friendly name and so
74+
// it would throw with duplicate key. This way we just overwrite the value – last item in source wins.
75+
usermappings[userMapping.Source.FriendlyName] = userMapping.Target?.FriendlyName;
76+
}
77+
System.IO.File.WriteAllText(CommonTools.UserMapping.Options.UserMappingFile, JsonConvert.SerializeObject(usermappings, Formatting.Indented));
7578
Log.LogInformation("Writen to: {LocalExportJsonFile}", CommonTools.UserMapping.Options.UserMappingFile);
76-
//////////////////////////////////////////////////
79+
7780
stopwatch.Stop();
78-
Log.LogInformation("DONE in {Elapsed} seconds");
81+
Log.LogInformation("DONE in {Elapsed} seconds", stopwatch.Elapsed);
7982
}
8083
}
81-
}
84+
}

src/MigrationTools.Clients.TfsObjectModel/Tools/TfsUserMappingTool.cs

Lines changed: 23 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,11 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4-
using System.Text;
5-
using System.Threading.Tasks;
6-
using Microsoft.Extensions.DependencyInjection;
74
using Microsoft.Extensions.Logging;
85
using Microsoft.Extensions.Options;
9-
using Microsoft.TeamFoundation.Common;
106
using Microsoft.TeamFoundation.Server;
117
using Microsoft.TeamFoundation.WorkItemTracking.Client;
12-
using Microsoft.VisualStudio.Services.Commerce;
138
using MigrationTools.DataContracts;
14-
using MigrationTools.Enrichers;
15-
using MigrationTools.Processors;
169
using MigrationTools.Processors.Infrastructure;
1710
using MigrationTools.Tools.Infrastructure;
1811

@@ -29,7 +22,6 @@ public TfsUserMappingTool(IOptions<TfsUserMappingToolOptions> options, IServiceP
2922
{
3023
}
3124

32-
3325
private List<string> GetUsersFromWorkItems(List<WorkItemData> workitems, List<string> identityFieldsToCheck)
3426
{
3527
List<string> foundUsers = new List<string>();
@@ -52,22 +44,6 @@ private List<string> GetUsersFromWorkItems(List<WorkItemData> workitems, List<st
5244
return foundUsers;
5345
}
5446

55-
56-
57-
private void MapUserIdentityField(FieldItem field)
58-
{
59-
if (Options.Enabled && Options.IdentityFieldsToCheck.Contains(field.ReferenceName))
60-
{
61-
Log.LogDebug($"TfsUserMappingTool::MapUserIdentityField [ReferenceName|{field.ReferenceName}]");
62-
var mapps = GetMappingFileData();
63-
if (mapps != null && mapps.ContainsKey(field.Value.ToString()))
64-
{
65-
field.Value = mapps[field.Value.ToString()];
66-
}
67-
68-
}
69-
}
70-
7147
public void MapUserIdentityField(Field field)
7248
{
7349
if (Options.Enabled && Options.IdentityFieldsToCheck.Contains(field.ReferenceName))
@@ -80,7 +56,6 @@ public void MapUserIdentityField(Field field)
8056
field.Value = mapps[field.Value.ToString()];
8157
Log.LogDebug($"TfsUserMappingTool::MapUserIdentityField::Map:[original|{original}][new|{field.Value}]");
8258
}
83-
8459
}
8560
}
8661

@@ -99,78 +74,79 @@ private Dictionary<string, string> GetMappingFileData()
9974
try
10075
{
10176
var fileMaps = Newtonsoft.Json.JsonConvert.DeserializeObject<List<IdentityMapData>>(fileData);
102-
_UserMappings = fileMaps.ToDictionary(x => x.Source.FriendlyName, x => x.target?.FriendlyName);
77+
_UserMappings = fileMaps.ToDictionary(x => x.Source.FriendlyName, x => x.Target?.FriendlyName);
10378
}
10479
catch (Exception)
10580
{
10681
_UserMappings = new Dictionary<string, string>();
10782
Log.LogError($"TfsUserMappingTool::GetMappingFileData [UserMappingFile|{Options.UserMappingFile}] <-- invalid - No mapping are applied!");
10883
}
109-
11084
}
11185
return _UserMappings;
11286
}
11387

11488
private List<IdentityItemData> GetUsersListFromServer(IGroupSecurityService gss)
11589
{
116-
Identity SIDS = gss.ReadIdentity(SearchFactor.AccountName, "Project Collection Valid Users", QueryMembership.Expanded);
117-
var people = SIDS.Members.ToList().Where(x => x.Contains("\\")).Select(x => x);
90+
Identity allIdentities = gss.ReadIdentity(SearchFactor.AccountName, "Project Collection Valid Users", QueryMembership.Expanded);
91+
Log.LogInformation("TfsUserMappingTool::GetUsersListFromServer Found {count} identities (users and groups) in server.", allIdentities.Members.Length);
11892

11993
List<IdentityItemData> foundUsers = new List<IdentityItemData>();
120-
Log.LogTrace("TfsUserMappingTool::GetUsersListFromServer:foundUsers\\ {@foundUsers}", foundUsers);
121-
foreach (string user in people)
94+
foreach (string sid in allIdentities.Members)
12295
{
123-
Log.LogDebug("TfsUserMappingTool::GetUsersListFromServer::[user:{user}] Atempting to load user", user);
96+
Log.LogDebug("TfsUserMappingTool::GetUsersListFromServer::[user:{user}] Atempting to load user", sid);
12497
try
12598
{
126-
var bits = user.Split('\\');
127-
Identity sids = gss.ReadIdentity(SearchFactor.AccountName, bits[1], QueryMembership.Expanded);
128-
if (sids != null)
99+
Identity identity = gss.ReadIdentity(SearchFactor.Sid, sid, QueryMembership.Expanded);
100+
if (identity is null)
129101
{
130-
foundUsers.Add(new IdentityItemData() { FriendlyName = sids.DisplayName, AccountName = sids.AccountName });
102+
Log.LogDebug("TfsUserMappingTool::GetUsersListFromServer::[user:{user}] ReadIdentity returned null", sid);
103+
}
104+
else if ((identity.Type == IdentityType.WindowsUser) || (identity.Type == IdentityType.UnknownIdentityType))
105+
{
106+
// UnknownIdentityType is set for users in Azure Entra ID.
107+
foundUsers.Add(new IdentityItemData()
108+
{
109+
FriendlyName = identity.DisplayName,
110+
AccountName = identity.AccountName
111+
});
131112
}
132113
else
133114
{
134-
Log.LogDebug("TfsUserMappingTool::GetUsersListFromServer::[user:{user}] ReadIdentity returned null for {@bits}", user, bits);
115+
Log.LogDebug("TfsUserMappingTool::GetUsersListFromServer::[user:{user}] Not applicable identity type {identityType}", sid, identity.Type);
135116
}
136-
137117
}
138118
catch (Exception ex)
139119
{
140120
Telemetry.TrackException(ex, null);
141-
Log.LogWarning("TfsUserMappingTool::GetUsersListFromServer::[user:{user}] Failed With {Exception}", user, ex.Message);
121+
Log.LogWarning("TfsUserMappingTool::GetUsersListFromServer::[user:{user}] Failed With {Exception}", sid, ex.Message);
142122
}
143-
144123
}
124+
Log.LogInformation("TfsUserMappingTool::GetUsersListFromServer {count} user identities are applicable for mapping", foundUsers.Count);
145125
return foundUsers;
146126
}
147127

148-
149128
public List<IdentityMapData> GetUsersInSourceMappedToTarget(TfsProcessor processor)
150129
{
151130
Log.LogDebug("TfsUserMappingTool::GetUsersInSourceMappedToTarget");
152131
if (Options.Enabled)
153132
{
133+
Log.LogInformation($"TfsUserMappingTool::GetUsersInSourceMappedToTarget Loading identities from source server");
154134
var sourceUsers = GetUsersListFromServer(processor.Source.GetService<IGroupSecurityService>());
155-
Log.LogDebug($"TfsUserMappingTool::GetUsersInSourceMappedToTarget [SourceUsersCount|{sourceUsers.Count}]");
135+
Log.LogInformation($"TfsUserMappingTool::GetUsersInSourceMappedToTarget Loading identities from target server");
156136
var targetUsers = GetUsersListFromServer(processor.Target.GetService<IGroupSecurityService>());
157-
Log.LogDebug($"TfsUserMappingTool::GetUsersInSourceMappedToTarget [targetUsersCount|{targetUsers.Count}]");
158-
return sourceUsers.Select(sUser => new IdentityMapData { Source = sUser, target = targetUsers.SingleOrDefault(tUser => tUser.FriendlyName == sUser.FriendlyName) }).ToList();
137+
return sourceUsers.Select(sUser => new IdentityMapData { Source = sUser, Target = targetUsers.SingleOrDefault(tUser => tUser.FriendlyName == sUser.FriendlyName) }).ToList();
159138
}
160139
else
161140
{
162141
Log.LogWarning("TfsUserMappingTool is disabled in settings. You may have users in the source that are not mapped to the target. ");
163142
return null;
164143
}
165-
166144
}
167145

168-
169146
public List<IdentityMapData> GetUsersInSourceMappedToTargetForWorkItems(TfsProcessor processor, List<WorkItemData> sourceWorkItems)
170147
{
171148
if (Options.Enabled)
172149
{
173-
174150
Dictionary<string, string> result = new Dictionary<string, string>();
175151
List<string> workItemUsers = GetUsersFromWorkItems(sourceWorkItems, Options.IdentityFieldsToCheck);
176152
Log.LogDebug($"TfsUserMappingTool::GetUsersInSourceMappedToTargetForWorkItems [workItemUsers|{workItemUsers.Count}]");
@@ -188,7 +164,6 @@ public List<IdentityMapData> GetUsersInSourceMappedToTargetForWorkItems(TfsProce
188164

189165
public class CaseInsensativeStringComparer : IEqualityComparer<string>
190166
{
191-
192167
public bool Equals(string x, string y)
193168
{
194169
return x?.IndexOf(y, StringComparison.OrdinalIgnoreCase) >= 0;
Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
using System;
2-
using System.Collections.Generic;
3-
using System.Text;
4-
5-
namespace MigrationTools.DataContracts
1+
namespace MigrationTools.DataContracts
62
{
73
public class IdentityItemData
84
{
@@ -13,6 +9,6 @@ public class IdentityItemData
139
public class IdentityMapData
1410
{
1511
public IdentityItemData Source { get; set; }
16-
public IdentityItemData target { get; set; }
12+
public IdentityItemData Target { get; set; }
1713
}
1814
}

0 commit comments

Comments
 (0)