Skip to content

Commit

Permalink
Improve validation for TfsEndpoint PAT tokens and message (#2368)
Browse files Browse the repository at this point in the history
Both @soodgautam and @nkofctg ran into issues with the `when you are
migrating to Azure DevOps you MUST provide an PAT so that we can call
the REST API for certain actions. For example, we would be unable to
deal with a Work item Type change.` message!

This was due to the `SourceName` and/or `TargetName` on the Processor
being set to null. The validator now checks for this!

It now checks for:

- [X] `SourceName` and/or `TargetName` being Set
- [X] `SourceName` and/or `TargetName` existing
- [X] `SourceName` and/or `TargetName` of correct type
  • Loading branch information
MrHinsh authored Sep 15, 2024
2 parents 927c611 + 770ec21 commit c1dc8d1
Show file tree
Hide file tree
Showing 16 changed files with 260 additions and 110 deletions.
8 changes: 4 additions & 4 deletions configuration.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"MigrationTools": {
"Version": "16.0",
"Endpoints": {
"Source2": {
"Source": {
"EndpointType": "TfsTeamProjectEndpoint",
"Collection": "https://dev.azure.com/nkdagility-preview/",
"Project": "migrationSource1",
Expand All @@ -25,7 +25,7 @@
"IterationPath": "Iteration"
}
},
"Target2": {
"Target": {
"EndpointType": "TfsTeamProjectEndpoint",
"Collection": "https://dev.azure.com/nkdagility-preview/",
"Project": "migrationTest5",
Expand Down Expand Up @@ -147,8 +147,8 @@
"PauseAfterEachWorkItem": false,
"AttachRevisionHistory": false,
"GenerateMigrationComment": true,
"SourceName": "Source2",
"TargetName": "Target2",
"SourceName": "Source",
"TargetName": "Target",
"WorkItemIDs": [ 12 ],
"MaxGracefulFailures": 0,
"SkipRevisionWithInvalidIterationPath": false,
Expand Down
20 changes: 10 additions & 10 deletions docs/Reference/Generated/MigrationTools.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace MigrationTools.Processors.Tests
Expand All @@ -25,7 +26,10 @@ public void OptionsValidator_Valid()
var validator = new TfsWorkItemMigrationProcessorOptionsValidator();
var x = new TfsWorkItemMigrationProcessorOptions();
x.WIQLQuery = "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @TeamProject";
Assert.IsTrue(validator.Validate(null, x).Succeeded);
x.SourceName = "source";
x.TargetName = "target";
ValidateOptionsResult result = validator.Validate(null, x);
Assert.IsTrue(result.Succeeded);
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ private static TfsTeamSettingsEndpointOptions GetTfsTeamEndPointOptions(string p
{
Collection = new Uri("https://dev.azure.com/nkdagility-preview/"),
Project = project,
Authentication = new TfsAuthenticationOptions()
Authentication = new Endpoints.Infrastructure.TfsAuthenticationOptions()
{
AuthenticationMode = AuthenticationMode.AccessToken,
AccessToken = TestingConstants.AccessToken,
Expand All @@ -82,7 +82,7 @@ private static TfsEndpointOptions GetTfsEndPointOptions(string project)
{
Collection = new Uri("https://dev.azure.com/nkdagility-preview/"),
Project = project,
Authentication = new TfsAuthenticationOptions()
Authentication = new Endpoints.Infrastructure.TfsAuthenticationOptions()
{
AuthenticationMode = AuthenticationMode.AccessToken,
AccessToken = TestingConstants.AccessToken,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using MigrationTools.Options;
using MigrationTools.Options.Infrastructure;
using Newtonsoft.Json;
Expand All @@ -11,7 +12,7 @@

namespace MigrationTools.Endpoints.Infrastructure
{
public class TfsAuthenticationOptions
public class TfsAuthenticationOptions : IValidateOptions<TfsAuthenticationOptions>
{
[JsonConverter(typeof(StringEnumConverter))]
public AuthenticationMode AuthenticationMode { get; set; }
Expand All @@ -20,5 +21,44 @@ public class TfsAuthenticationOptions

[JsonConverter(typeof(DefaultOnlyConverter<string>), "** removed as a secret ***")]
public string AccessToken { get; set; }

public ValidateOptionsResult Validate(string name, TfsAuthenticationOptions options)
{
var errors = new List<string>();
// Validate Authentication properties based on AuthenticationMode
switch (options.AuthenticationMode)
{
case AuthenticationMode.AccessToken:
if (string.IsNullOrWhiteSpace(options.AccessToken))
{
errors.Add("The AccessToken must not be null or empty when AuthenticationMode is set to 'AccessToken'. You must provide a PAT to use 'AccessToken' as the authentication mode. You can set this through the config at 'MigrationTools:Endpoints:{name}:Authentication:AccessToken', or you can set an environment variable of 'MigrationTools__Endpoints__{name}__Authentication__AccessToken'. Check the docs on https://nkdagility.com/learn/azure-devops-migration-tools/Reference/Endpoints/TfsTeamProjectEndpoint/");
}
break;

case AuthenticationMode.Windows:
if (options.NetworkCredentials == null)
{
errors.Add("The NetworkCredentials must be provided when AuthenticationMode is set to 'Windows'.");
} else
{
ValidateOptionsResult result = options.NetworkCredentials.Validate(name, options.NetworkCredentials);
if (!result.Succeeded)
{
errors.AddRange(result.Failures);
}
}
break;
case AuthenticationMode.Prompt:
break;
default:
errors.Add($"The AuthenticationMode '{options.AuthenticationMode}' is not supported.");
break;
}
if (errors.Any())
{
return ValidateOptionsResult.Fail(errors);
}
return ValidateOptionsResult.Success;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Microsoft.Extensions.Options;
using MigrationTools.Endpoints.Infrastructure;
using Newtonsoft.Json;
Expand Down Expand Up @@ -55,14 +56,22 @@ public ValidateOptionsResult Validate(string name, TfsEndpointOptions options)
// Validate ReflectedWorkItemIdField - Must not be null or empty
if (string.IsNullOrWhiteSpace(options.ReflectedWorkItemIdField))
{
errors.Add("The ReflectedWorkItemIdField property must not be null or empty.");
errors.Add("The ReflectedWorkItemIdField property must not be null or empty. Check the docs on https://nkdagility.com/learn/azure-devops-migration-tools/setup/reflectedworkitemid/");
}

// Validate LanguageMaps - Must exist
if (options.LanguageMaps == null)
{
errors.Add("The LanguageMaps property must exist.");
}
else
{
ValidateOptionsResult lmr= options.LanguageMaps.Validate(name, options.LanguageMaps);
if (lmr != ValidateOptionsResult.Success)
{
errors.AddRange(lmr.Failures);
}
}

// Validate Authentication - Must exist
if (options.Authentication == null)
Expand All @@ -71,34 +80,17 @@ public ValidateOptionsResult Validate(string name, TfsEndpointOptions options)
}
else
{
// Validate Authentication properties based on AuthenticationMode
switch (options.Authentication.AuthenticationMode)
ValidateOptionsResult lmr = options.Authentication.Validate(name, options.Authentication);
if (lmr != ValidateOptionsResult.Success)
{
case AuthenticationMode.AccessToken:
if (string.IsNullOrWhiteSpace(options.Authentication.AccessToken))
{
errors.Add("The AccessToken must not be null or empty when AuthenticationMode is set to 'AccessToken'.");
}
break;

case AuthenticationMode.Windows:
if (options.Authentication.NetworkCredentials == null)
{
errors.Add("The NetworkCredentials must be provided when AuthenticationMode is set to 'Windows'.");
}
break;
case AuthenticationMode.Prompt:
break;
default:
errors.Add($"The AuthenticationMode '{options.Authentication.AuthenticationMode}' is not supported.");
break;
errors.AddRange(lmr.Failures);
}
}

// Return failure if there are errors, otherwise success
if (errors.Count > 0)
if (errors.Any())
{
return ValidateOptionsResult.Fail(string.Join(Environment.NewLine, errors));
return ValidateOptionsResult.Fail(errors);
}

return ValidateOptionsResult.Success;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,31 @@
namespace MigrationTools.Endpoints
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Options;

namespace MigrationTools.Endpoints
{
public class TfsLanguageMapOptions
public class TfsLanguageMapOptions : IValidateOptions<TfsLanguageMapOptions>
{
public string AreaPath { get; set; }
public string IterationPath { get; set; }

public ValidateOptionsResult Validate(string name, TfsLanguageMapOptions options)
{
var errors = new List<string>();

if (string.IsNullOrWhiteSpace(options.AreaPath))
{
errors.Add("The AreaPath property must not be null or empty.");
}
if (string.IsNullOrWhiteSpace(options.IterationPath))
{
errors.Add("The IterationPath property must not be null or empty.");
}
if (errors.Any())
{
ValidateOptionsResult.Fail(errors);
}
return ValidateOptionsResult.Success;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using MigrationTools;
using MigrationTools.Clients;
using MigrationTools.Enrichers;
using MigrationTools.Exceptions;
using MigrationTools.Tools;

namespace MigrationTools.Processors.Infrastructure
Expand All @@ -19,9 +20,31 @@ protected TfsProcessor(IOptions<ProcessorOptions> options, TfsCommonTools tfsCom

}

new public TfsTeamProjectEndpoint Source => (TfsTeamProjectEndpoint)base.Source;
new public TfsTeamProjectEndpoint Source
{
get
{
var endpoint = base.Source as TfsTeamProjectEndpoint;
if (endpoint == null)
{
throw new ConfigurationValidationException(Options, ValidateOptionsResult.Fail($"The Endpoint '{Options.SourceName}' specified for `{this.GetType().Name}` is of the wrong type! {nameof(TfsTeamProjectEndpoint)} was expected."));
}
return endpoint;
}
}

new public TfsTeamProjectEndpoint Target => (TfsTeamProjectEndpoint)base.Target;
new public TfsTeamProjectEndpoint Target
{
get
{
var endpoint = base.Target as TfsTeamProjectEndpoint;
if (endpoint == null)
{
throw new ConfigurationValidationException(Options, ValidateOptionsResult.Fail($"The Endpoint '{Options.TargetName}' specified for `{this.GetType().Name}` is of the wrong type! {nameof(TfsTeamProjectEndpoint)} was expected."));
}
return endpoint;
}
}

new public TfsCommonTools CommonTools => (TfsCommonTools)base.CommonTools;

Expand Down
Loading

0 comments on commit c1dc8d1

Please sign in to comment.