Skip to content

Commit

Permalink
TfsUserMappingTool: fix loading users from TFS connected to Active Di…
Browse files Browse the repository at this point in the history
…rectory (#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.
  • Loading branch information
MrHinsh authored Nov 20, 2024
2 parents 28fc3d7 + 72b0c36 commit 013ae47
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
/// <summary>
Expand All @@ -29,11 +21,17 @@ namespace MigrationTools.Processors
/// <processingtarget>Work Items</processingtarget>
public class TfsExportUsersForMappingProcessor : TfsProcessor
{
public TfsExportUsersForMappingProcessor(IOptions<TfsExportUsersForMappingProcessorOptions> options, TfsCommonTools tfsCommonTools, ProcessorEnricherContainer processorEnrichers, IServiceProvider services, ITelemetryLogger telemetry, ILogger<TfsExportUsersForMappingProcessor> logger) : base(options, tfsCommonTools, processorEnrichers, services, telemetry, logger)
public TfsExportUsersForMappingProcessor(
IOptions<TfsExportUsersForMappingProcessorOptions> options,
TfsCommonTools tfsCommonTools,
ProcessorEnricherContainer processorEnrichers,
IServiceProvider services,
ITelemetryLogger telemetry,
ILogger<TfsExportUsersForMappingProcessor> logger)
: base(options, tfsCommonTools, processorEnrichers, services, telemetry, logger)
{
}


new TfsExportUsersForMappingProcessorOptions Options => (TfsExportUsersForMappingProcessorOptions)base.Options;

new TfsTeamProjectEndpoint Source => (TfsTeamProjectEndpoint)base.Source;
Expand All @@ -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<IdentityMapData> usersToMap = new List<IdentityMapData>();
if (Options.OnlyListUsersInWorkItems)
{
Expand All @@ -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<string, string> 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<string, string> 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);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -29,7 +22,6 @@ public TfsUserMappingTool(IOptions<TfsUserMappingToolOptions> options, IServiceP
{
}


private List<string> GetUsersFromWorkItems(List<WorkItemData> workitems, List<string> identityFieldsToCheck)
{
List<string> foundUsers = new List<string>();
Expand All @@ -52,22 +44,6 @@ private List<string> GetUsersFromWorkItems(List<WorkItemData> workitems, List<st
return foundUsers;
}



private void MapUserIdentityField(FieldItem field)
{
if (Options.Enabled && Options.IdentityFieldsToCheck.Contains(field.ReferenceName))
{
Log.LogDebug($"TfsUserMappingTool::MapUserIdentityField [ReferenceName|{field.ReferenceName}]");
var mapps = GetMappingFileData();
if (mapps != null && mapps.ContainsKey(field.Value.ToString()))
{
field.Value = mapps[field.Value.ToString()];
}

}
}

public void MapUserIdentityField(Field field)
{
if (Options.Enabled && Options.IdentityFieldsToCheck.Contains(field.ReferenceName))
Expand All @@ -80,7 +56,6 @@ public void MapUserIdentityField(Field field)
field.Value = mapps[field.Value.ToString()];
Log.LogDebug($"TfsUserMappingTool::MapUserIdentityField::Map:[original|{original}][new|{field.Value}]");
}

}
}

Expand All @@ -99,78 +74,79 @@ private Dictionary<string, string> GetMappingFileData()
try
{
var fileMaps = Newtonsoft.Json.JsonConvert.DeserializeObject<List<IdentityMapData>>(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<string, string>();
Log.LogError($"TfsUserMappingTool::GetMappingFileData [UserMappingFile|{Options.UserMappingFile}] <-- invalid - No mapping are applied!");
}

}
return _UserMappings;
}

private List<IdentityItemData> 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<IdentityItemData> foundUsers = new List<IdentityItemData>();
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<IdentityMapData> 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<IGroupSecurityService>());
Log.LogDebug($"TfsUserMappingTool::GetUsersInSourceMappedToTarget [SourceUsersCount|{sourceUsers.Count}]");
Log.LogInformation($"TfsUserMappingTool::GetUsersInSourceMappedToTarget Loading identities from target server");
var targetUsers = GetUsersListFromServer(processor.Target.GetService<IGroupSecurityService>());
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<IdentityMapData> GetUsersInSourceMappedToTargetForWorkItems(TfsProcessor processor, List<WorkItemData> sourceWorkItems)
{
if (Options.Enabled)
{

Dictionary<string, string> result = new Dictionary<string, string>();
List<string> workItemUsers = GetUsersFromWorkItems(sourceWorkItems, Options.IdentityFieldsToCheck);
Log.LogDebug($"TfsUserMappingTool::GetUsersInSourceMappedToTargetForWorkItems [workItemUsers|{workItemUsers.Count}]");
Expand All @@ -188,7 +164,6 @@ public List<IdentityMapData> GetUsersInSourceMappedToTargetForWorkItems(TfsProce

public class CaseInsensativeStringComparer : IEqualityComparer<string>
{

public bool Equals(string x, string y)
{
return x?.IndexOf(y, StringComparison.OrdinalIgnoreCase) >= 0;
Expand Down
8 changes: 2 additions & 6 deletions src/MigrationTools/DataContracts/IdentityItemData.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace MigrationTools.DataContracts
namespace MigrationTools.DataContracts
{
public class IdentityItemData
{
Expand All @@ -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; }
}
}

0 comments on commit 013ae47

Please sign in to comment.