Skip to content

Commit

Permalink
Add Kerberos support (#684)
Browse files Browse the repository at this point in the history
<!--
For the checkboxes below you must check each one to indicate that you
either did the relevant task, or considered it and decided there was
nothing that needed doing
-->

Issue #678 

Added support for kerberos authentication when calling BBS API's. To do
this all we need to do is configure our HttpClient to have
`UseDefaultCredentials = true` and NOT set the normal Authorization
header that we set. This needs to be set at the time we construct the
HttpClient. Similar to how we did the NoSSL support, I added a couple
different named HttpClient's into our DI container ("Default" and
"Kerberos") configured appropriately. Then I added a new
`CreateKerberos()` function to `BbsApiFactory` that doesn't require a
username/password. Finally I added a 2nd constructor to `BbsApi` that
doesn't require a username/password and doesn't attempt to set the
Authorization header.

I added a `--kerberos` flag to both `bbs2gh generate-script` and `bbs2gh
migrate-repo`. For now this is a hidden option.

- [x] Did you write/update appropriate tests
- [x] Release notes updated (if appropriate)
- [x] Appropriate logging output
- [x] Issue linked
- [x] Docs updated (or issue created)

<!--
For docs we should review the docs at:

https://docs.github.com/en/early-access/github/migrating-with-github-enterprise-importer
and the README.md in this repo

If a doc update is required based on the changes in this PR, it is
sufficient to create an issue and link to it here. The doc update can be
made later/separately.

The process to update the docs can be found here:
https://github.com/github/docs-early-access#opening-prs

The markdown files are here: 

https://github.com/github/docs-early-access/tree/main/content/github/migrating-with-github-enterprise-importer
-->
  • Loading branch information
dylan-smith authored Oct 17, 2022
1 parent e97ddd6 commit 837f39b
Show file tree
Hide file tree
Showing 11 changed files with 262 additions and 27 deletions.
14 changes: 11 additions & 3 deletions src/Octoshift/BbsClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,17 @@ public class BbsClient
private readonly OctoLogger _log;
private readonly RetryPolicy _retryPolicy;

public BbsClient(OctoLogger log, HttpClient httpClient, IVersionProvider versionProvider, RetryPolicy retryPolicy, string username, string password)
public BbsClient(OctoLogger log, HttpClient httpClient, IVersionProvider versionProvider, RetryPolicy retryPolicy, string username, string password) :
this(log, httpClient, versionProvider, retryPolicy)
{
if (_httpClient != null)
{
var authCredentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{username}:{password}"));
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authCredentials);
}
}

public BbsClient(OctoLogger log, HttpClient httpClient, IVersionProvider versionProvider, RetryPolicy retryPolicy)
{
_log = log;
_httpClient = httpClient;
Expand All @@ -23,8 +33,6 @@ public BbsClient(OctoLogger log, HttpClient httpClient, IVersionProvider version
if (_httpClient != null)
{
_httpClient.DefaultRequestHeaders.Add("accept", "application/json");
var authCredentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{username}:{password}"));
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authCredentials);
_httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("OctoshiftCLI", versionProvider?.GetCurrentVersion()));
if (versionProvider?.GetVersionComments() is { } comments)
{
Expand Down
13 changes: 13 additions & 0 deletions src/OctoshiftCLI.Tests/BbsClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,19 @@ public void It_Adds_The_Authorization_Header()
httpClient.DefaultRequestHeaders.Authorization.Scheme.Should().Be("Basic");
}

[Fact]
public void It_Doesnt_Add_Authorization_Header_When_No_Credentials_Passed()
{
// Arrange
using var httpClient = new HttpClient(MockHttpHandlerForGet().Object);

// Act
_ = new BbsClient(_mockOctoLogger.Object, httpClient, null, _retryPolicy);

// Assert
httpClient.DefaultRequestHeaders.Authorization.Should().BeNull();
}

[Fact]
public async Task GetAsync_Encodes_The_Url()
{
Expand Down
59 changes: 59 additions & 0 deletions src/OctoshiftCLI.Tests/bbs2gh/BbsApiFactoryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System.Linq;
using System.Net.Http;
using FluentAssertions;
using Moq;
using OctoshiftCLI.BbsToGithub;
using Xunit;

namespace OctoshiftCLI.Tests.bbs2gh.Commands
{
public class BbsApiFactoryTests
{
private const string BBS_SERVER_URL = "http://bbs.contoso.com:7990";

private readonly Mock<OctoLogger> _mockOctoLogger = TestHelpers.CreateMock<OctoLogger>();
private readonly Mock<EnvironmentVariableProvider> _mockEnvironmentVariableProvider = TestHelpers.CreateMock<EnvironmentVariableProvider>();
private readonly Mock<IHttpClientFactory> _mockHttpClientFactory = new Mock<IHttpClientFactory>();

private readonly BbsApiFactory _bbsApiFactory;

public BbsApiFactoryTests()
{
_bbsApiFactory = new BbsApiFactory(_mockOctoLogger.Object, _mockHttpClientFactory.Object, _mockEnvironmentVariableProvider.Object, null, null);
}

[Fact]
public void Should_Create_BbsApi_For_Source_Bbs_Api_With_Kerberos()
{
using var httpClient = new HttpClient();

_mockHttpClientFactory
.Setup(x => x.CreateClient("Kerberos"))
.Returns(httpClient);

// Act
var githubApi = _bbsApiFactory.CreateKerberos(BBS_SERVER_URL);

// Assert
githubApi.Should().NotBeNull();
httpClient.DefaultRequestHeaders.Accept.First().MediaType.Should().Be("application/json");
}

[Fact]
public void Should_Create_BbsApi_For_Source_Bbs_Api_With_Default()
{
using var httpClient = new HttpClient();

_mockHttpClientFactory
.Setup(x => x.CreateClient("Default"))
.Returns(httpClient);

// Act
var githubApi = _bbsApiFactory.Create(BBS_SERVER_URL, "user", "pass");

// Assert
githubApi.Should().NotBeNull();
httpClient.DefaultRequestHeaders.Accept.First().MediaType.Should().Be("application/json");
}
}
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,84 @@
using System;
using FluentAssertions;
using Moq;
using OctoshiftCLI.BbsToGithub;
using OctoshiftCLI.BbsToGithub.Commands;
using OctoshiftCLI.Contracts;
using Xunit;

namespace OctoshiftCLI.Tests.BbsToGithub.Commands;

public class GenerateScriptCommandTests
{
private const string BBS_SERVER_URL = "http://bbs.contoso.com:7990";

private readonly Mock<IServiceProvider> _mockServiceProvider = new();
private readonly Mock<BbsApiFactory> _mockBbsApiFactory = TestHelpers.CreateMock<BbsApiFactory>();
private readonly Mock<OctoLogger> _mockOctoLogger = TestHelpers.CreateMock<OctoLogger>();
private readonly Mock<EnvironmentVariableProvider> _mockEnvironmentVariableProvider = TestHelpers.CreateMock<EnvironmentVariableProvider>();
private readonly Mock<FileSystemProvider> _mockFileSystemProvider = TestHelpers.CreateMock<FileSystemProvider>();
private readonly Mock<IVersionProvider> _mockVersionProvider = new();

private readonly GenerateScriptCommand _command = new();

public GenerateScriptCommandTests()
{
_mockServiceProvider.Setup(m => m.GetService(typeof(OctoLogger))).Returns(_mockOctoLogger.Object);
_mockServiceProvider.Setup(m => m.GetService(typeof(EnvironmentVariableProvider))).Returns(_mockEnvironmentVariableProvider.Object);
_mockServiceProvider.Setup(m => m.GetService(typeof(FileSystemProvider))).Returns(_mockFileSystemProvider.Object);
_mockServiceProvider.Setup(m => m.GetService(typeof(IVersionProvider))).Returns(_mockVersionProvider.Object);
_mockServiceProvider.Setup(m => m.GetService(typeof(BbsApiFactory))).Returns(_mockBbsApiFactory.Object);
}

[Fact]
public void Should_Have_Options()
{
var command = new GenerateScriptCommand();
command.Should().NotBeNull();
command.Name.Should().Be("generate-script");
command.Options.Count.Should().Be(10);

TestHelpers.VerifyCommandOption(command.Options, "bbs-server-url", true);
TestHelpers.VerifyCommandOption(command.Options, "github-org", true);
TestHelpers.VerifyCommandOption(command.Options, "bbs-username", false);
TestHelpers.VerifyCommandOption(command.Options, "bbs-password", false);
TestHelpers.VerifyCommandOption(command.Options, "bbs-shared-home", false);
TestHelpers.VerifyCommandOption(command.Options, "ssh-user", false);
TestHelpers.VerifyCommandOption(command.Options, "ssh-private-key", false);
TestHelpers.VerifyCommandOption(command.Options, "ssh-port", false);
TestHelpers.VerifyCommandOption(command.Options, "output", false);
TestHelpers.VerifyCommandOption(command.Options, "verbose", false);
_command.Should().NotBeNull();
_command.Name.Should().Be("generate-script");
_command.Options.Count.Should().Be(11);

TestHelpers.VerifyCommandOption(_command.Options, "bbs-server-url", true);
TestHelpers.VerifyCommandOption(_command.Options, "github-org", true);
TestHelpers.VerifyCommandOption(_command.Options, "bbs-username", false);
TestHelpers.VerifyCommandOption(_command.Options, "bbs-password", false);
TestHelpers.VerifyCommandOption(_command.Options, "bbs-shared-home", false);
TestHelpers.VerifyCommandOption(_command.Options, "ssh-user", false);
TestHelpers.VerifyCommandOption(_command.Options, "ssh-private-key", false);
TestHelpers.VerifyCommandOption(_command.Options, "ssh-port", false);
TestHelpers.VerifyCommandOption(_command.Options, "output", false);
TestHelpers.VerifyCommandOption(_command.Options, "kerberos", false, true);
TestHelpers.VerifyCommandOption(_command.Options, "verbose", false);
}

[Fact]
public void It_Gets_A_Kerberos_HttpClient_When_Kerberos_Is_True()
{
var args = new GenerateScriptCommandArgs
{
BbsServerUrl = BBS_SERVER_URL,
Kerberos = true
};

_command.BuildHandler(args, _mockServiceProvider.Object);

_mockBbsApiFactory.Verify(m => m.CreateKerberos(BBS_SERVER_URL));
}

[Fact]
public void It_Gets_A_Default_HttpClient_When_Kerberos_Is_Not_Set()
{
var bbsTestUser = "user";
var bbsTestPassword = "password";

var args = new GenerateScriptCommandArgs
{
BbsServerUrl = BBS_SERVER_URL,
BbsUsername = bbsTestUser,
BbsPassword = bbsTestPassword,
};

_command.BuildHandler(args, _mockServiceProvider.Object);

_mockBbsApiFactory.Verify(m => m.Create(BBS_SERVER_URL, bbsTestUser, bbsTestPassword));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public void Should_Have_Options()
var command = new MigrateRepoCommand();
command.Should().NotBeNull();
command.Name.Should().Be("migrate-repo");
command.Options.Count.Should().Be(22);
command.Options.Count.Should().Be(23);

TestHelpers.VerifyCommandOption(command.Options, "bbs-server-url", false);
TestHelpers.VerifyCommandOption(command.Options, "bbs-project", false);
Expand All @@ -71,6 +71,7 @@ public void Should_Have_Options()
TestHelpers.VerifyCommandOption(command.Options, "smb-user", false, true);
TestHelpers.VerifyCommandOption(command.Options, "smb-password", false, true);
TestHelpers.VerifyCommandOption(command.Options, "wait", false);
TestHelpers.VerifyCommandOption(command.Options, "kerberos", false, true);
TestHelpers.VerifyCommandOption(command.Options, "verbose", false);
}

Expand Down Expand Up @@ -188,4 +189,33 @@ public void BuildHandler_Creates_Azure_Api_Factory_When_Azure_Storage_Connection

_mockAzureApiFactory.Verify(m => m.Create(AZURE_STORAGE_CONNECTION_STRING));
}

[Fact]
public void It_Gets_A_Kerberos_HttpClient_When_Kerberos_Is_True()
{
var args = new MigrateRepoCommandArgs
{
BbsServerUrl = BBS_SERVER_URL,
Kerberos = true
};

_command.BuildHandler(args, _mockServiceProvider.Object);

_mockBbsApiFactory.Verify(m => m.CreateKerberos(BBS_SERVER_URL));
}

[Fact]
public void It_Gets_A_Default_HttpClient_When_Kerberos_Is_Not_Set()
{
var args = new MigrateRepoCommandArgs
{
BbsServerUrl = BBS_SERVER_URL,
BbsUsername = BBS_USERNAME,
BbsPassword = BBS_PASSWORD,
};

_command.BuildHandler(args, _mockServiceProvider.Object);

_mockBbsApiFactory.Verify(m => m.Create(BBS_SERVER_URL, BBS_USERNAME, BBS_PASSWORD));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,42 @@ public async Task Two_Projects_Two_Repos_Each_All_Options()
_mockEnvironmentVariableProvider.Verify(m => m.BbsPassword(), Times.Never);
}

[Fact]
public async Task One_Repo_With_Kerberos()
{
// Arrange
_mockBbsApi.Setup(m => m.GetProjects()).ReturnsAsync(new[]
{
(Id: 1, Key: BBS_FOO_PROJECT_KEY, Name: BBS_FOO_PROJECT_NAME),
});
_mockBbsApi.Setup(m => m.GetRepos(BBS_FOO_PROJECT_KEY)).ReturnsAsync(new[]
{
(Id: 1, Slug: BBS_FOO_REPO_1_SLUG, Name: BBS_FOO_REPO_1_NAME),
});

const string migrateRepoCommand = $"Exec {{ gh bbs2gh migrate-repo --bbs-server-url \"{BBS_SERVER_URL}\" --bbs-username \"{BBS_USERNAME}\" --bbs-shared-home \"{BBS_SHARED_HOME}\" --bbs-project \"{BBS_FOO_PROJECT_KEY}\" --bbs-repo \"{BBS_FOO_REPO_1_SLUG}\" --ssh-user \"{SSH_USER}\" --ssh-private-key \"{SSH_PRIVATE_KEY}\" --ssh-port {SSH_PORT} --github-org \"{GITHUB_ORG}\" --github-repo \"{BBS_FOO_PROJECT_KEY}-{BBS_FOO_REPO_1_SLUG}\" --verbose --wait --kerberos }}";

// Act
var args = new GenerateScriptCommandArgs
{
BbsServerUrl = BBS_SERVER_URL,
GithubOrg = GITHUB_ORG,
BbsUsername = BBS_USERNAME,
BbsPassword = BBS_PASSWORD,
BbsSharedHome = BBS_SHARED_HOME,
SshUser = SSH_USER,
SshPrivateKey = SSH_PRIVATE_KEY,
SshPort = SSH_PORT,
Output = new FileInfo(OUTPUT),
Verbose = true,
Kerberos = true,
};
await _handler.Handle(args);

// Assert
_mockFileSystemProvider.Verify(m => m.WriteAllTextAsync(It.IsAny<string>(), It.Is<string>(script => script.Contains(migrateRepoCommand))));
}

[Fact]
public async Task Generated_Script_Contains_The_Cli_Version_Comment()
{
Expand Down
18 changes: 14 additions & 4 deletions src/bbs2gh/BbsApiFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ namespace OctoshiftCLI.BbsToGithub
public class BbsApiFactory
{
private readonly OctoLogger _octoLogger;
private readonly HttpClient _client;
private readonly IHttpClientFactory _clientFactory;
private readonly EnvironmentVariableProvider _environmentVariableProvider;
private readonly IVersionProvider _versionProvider;
private readonly RetryPolicy _retryPolicy;

public BbsApiFactory(OctoLogger octoLogger, HttpClient client, EnvironmentVariableProvider environmentVariableProvider, IVersionProvider versionProvider, RetryPolicy retryPolicy)
public BbsApiFactory(OctoLogger octoLogger, IHttpClientFactory clientFactory, EnvironmentVariableProvider environmentVariableProvider, IVersionProvider versionProvider, RetryPolicy retryPolicy)
{
_octoLogger = octoLogger;
_client = client;
_clientFactory = clientFactory;
_environmentVariableProvider = environmentVariableProvider;
_versionProvider = versionProvider;
_retryPolicy = retryPolicy;
Expand All @@ -25,7 +25,17 @@ public virtual BbsApi Create(string bbsServerUrl, string bbsUsername, string bbs
bbsUsername ??= _environmentVariableProvider.BbsUsername();
bbsPassword ??= _environmentVariableProvider.BbsPassword();

var bbsClient = new BbsClient(_octoLogger, _client, _versionProvider, _retryPolicy, bbsUsername, bbsPassword);
var httpClient = _clientFactory.CreateClient("Default");

var bbsClient = new BbsClient(_octoLogger, httpClient, _versionProvider, _retryPolicy, bbsUsername, bbsPassword);
return new BbsApi(bbsClient, bbsServerUrl, _octoLogger);
}

public virtual BbsApi CreateKerberos(string bbsServerUrl)
{
var httpClient = _clientFactory.CreateClient("Kerberos");

var bbsClient = new BbsClient(_octoLogger, httpClient, _versionProvider, _retryPolicy);
return new BbsApi(bbsClient, bbsServerUrl, _octoLogger);
}
}
Expand Down
9 changes: 8 additions & 1 deletion src/bbs2gh/Commands/GenerateScriptCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public GenerateScriptCommand() : base(
AddOption(SshPrivateKey);
AddOption(SshPort);
AddOption(Output);
AddOption(Kerberos);
AddOption(Verbose);
}

Expand Down Expand Up @@ -65,6 +66,11 @@ public GenerateScriptCommand() : base(
name: "--output",
getDefaultValue: () => new FileInfo("./migrate.ps1"));

public Option<bool> Kerberos { get; } = new(
name: "--kerberos",
description: "Use Kerberos authentication for Bitbucket Server.")
{ IsHidden = true };

public Option<bool> Verbose { get; } = new("--verbose");

public override GenerateScriptCommandHandler BuildHandler(GenerateScriptCommandArgs args, IServiceProvider sp)
Expand All @@ -85,7 +91,7 @@ public override GenerateScriptCommandHandler BuildHandler(GenerateScriptCommandA
var environmentVariableProvider = sp.GetRequiredService<EnvironmentVariableProvider>();

var bbsApiFactory = sp.GetRequiredService<BbsApiFactory>();
var bbsApi = bbsApiFactory.Create(args.BbsServerUrl, args.BbsUsername, args.BbsPassword);
var bbsApi = args.Kerberos ? bbsApiFactory.CreateKerberos(args.BbsServerUrl) : bbsApiFactory.Create(args.BbsServerUrl, args.BbsUsername, args.BbsPassword);

return new GenerateScriptCommandHandler(log, versionProvider, fileSystemProvider, bbsApi, environmentVariableProvider);
}
Expand All @@ -102,5 +108,6 @@ public class GenerateScriptCommandArgs
public string SshPrivateKey { get; set; }
public string SshPort { get; set; }
public FileInfo Output { get; set; }
public bool Kerberos { get; set; }
public bool Verbose { get; set; }
}
Loading

0 comments on commit 837f39b

Please sign in to comment.