Skip to content
This repository has been archived by the owner on Mar 22, 2023. It is now read-only.

Commit

Permalink
Add --password-stdin option to securely provide creds (#41)
Browse files Browse the repository at this point in the history
* Add password-stdin to pipe access token

* Lint

* Update var naming
  • Loading branch information
ethanis authored Jun 2, 2022
1 parent 34e0241 commit f204b8d
Show file tree
Hide file tree
Showing 7 changed files with 65 additions and 44 deletions.
34 changes: 22 additions & 12 deletions src/Valet.UnitTests/Services/DockerServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ public async Task UpdateImageAsync_NoCredentialsProvided_PullsLatest_ReturnsTrue
$"pull {server}/{image}:{version}",
It.IsAny<string?>(),
It.IsAny<IEnumerable<(string, string)>?>(),
It.IsAny<bool>()
It.IsAny<bool>(),
null
)
).Returns(Task.CompletedTask);

Expand Down Expand Up @@ -68,10 +69,11 @@ public async Task UpdateImageAsync_InvalidCredentialsProvided_ReturnsFalse()
_processService.Setup(handler =>
handler.RunAsync(
"docker",
$"login {server} --password {password} --username {username}",
$"login {server} --username {username} --password-stdin",
It.IsAny<string?>(),
It.IsAny<IEnumerable<(string, string)>?>(),
It.IsAny<bool>()
It.IsAny<bool>(),
password
)
).Returns(Task.CompletedTask);

Expand Down Expand Up @@ -101,10 +103,11 @@ public async Task UpdateImageAsync_ValidCredentialsProvided_ReturnsTrue()
_processService.Setup(handler =>
handler.RunAsync(
"docker",
$"login {server} --password {password} --username {username}",
$"login {server} --username {username} --password-stdin",
It.IsAny<string?>(),
It.IsAny<IEnumerable<(string, string)>?>(),
It.IsAny<bool>()
It.IsAny<bool>(),
password
)
).Returns(Task.CompletedTask);

Expand All @@ -114,7 +117,8 @@ public async Task UpdateImageAsync_ValidCredentialsProvided_ReturnsTrue()
$"pull {server}/{image}:{version}",
It.IsAny<string?>(),
It.IsAny<IEnumerable<(string, string)>?>(),
It.IsAny<bool>()
It.IsAny<bool>(),
null
)
).Returns(Task.CompletedTask);

Expand Down Expand Up @@ -145,7 +149,8 @@ public async Task ExecuteCommandAsync_InvokesDocker_ReturnsTrue()
$"run --rm -t -v \"{Directory.GetCurrentDirectory()}\":/data {server}/{image}:{version} {string.Join(' ', arguments)}",
Directory.GetCurrentDirectory(),
new[] { new System.ValueTuple<string, string>("MSYS_NO_PATHCONV", "1") },
true
true,
null
)
).Returns(Task.CompletedTask);

Expand Down Expand Up @@ -175,7 +180,8 @@ public async Task ExecuteCommandAsync_InvokesDocker_WithEnvironmentVariables_Ret
$"run --rm -t --env GITHUB_ACCESS_TOKEN=foo --env GITHUB_INSTANCE_URL=https://github.fabrikam.com --env JENKINS_ACCESS_TOKEN=bar -v \"{Directory.GetCurrentDirectory()}\":/data {server}/{image}:{version} {string.Join(' ', arguments)}",
Directory.GetCurrentDirectory(),
new[] { new System.ValueTuple<string, string>("MSYS_NO_PATHCONV", "1") },
true
true,
null
)
).Returns(Task.CompletedTask);

Expand All @@ -196,7 +202,8 @@ public void VerifyDockerRunningAsync_IsRunning_NoException()
"info",
It.IsAny<string?>(),
It.IsAny<IEnumerable<(string, string)>?>(),
It.IsAny<bool>()
It.IsAny<bool>(),
null
)
).Returns(Task.CompletedTask);

Expand All @@ -214,7 +221,8 @@ public void VerifyDockerRunningAsync_NotRunning_ThrowsException()
"info",
It.IsAny<string?>(),
It.IsAny<IEnumerable<(string, string)>?>(),
It.IsAny<bool>()
It.IsAny<bool>(),
null
)
).ThrowsAsync(new Exception());

Expand All @@ -236,7 +244,8 @@ public void VerifyImagePresentAsync_IsPresent_NoException()
$"image inspect {server}/{image}:{version}",
It.IsAny<string?>(),
It.IsAny<IEnumerable<(string, string)>?>(),
It.IsAny<bool>()
It.IsAny<bool>(),
null
)
).Returns(Task.CompletedTask);

Expand All @@ -259,7 +268,8 @@ public void VerifyImagePresentAsync_NotPresent_ThrowsException()
$"image inspect {server}/{image}:{version}",
It.IsAny<string?>(),
It.IsAny<IEnumerable<(string, string)>?>(),
It.IsAny<bool>()
It.IsAny<bool>(),
null
)
).ThrowsAsync(new Exception());

Expand Down
5 changes: 3 additions & 2 deletions src/Valet/App.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public App(IDockerService dockerService)
_dockerService = dockerService;
}

public async Task<int> UpdateValetAsync(string? username = null, string? password = null)
public async Task<int> UpdateValetAsync(string? username = null, string? password = null, bool passwordStdin = false)
{
await _dockerService.VerifyDockerRunningAsync().ConfigureAwait(false);

Expand All @@ -26,7 +26,8 @@ await _dockerService.UpdateImageAsync(
ValetContainerRegistry,
"latest",
username,
password
password,
passwordStdin
);

return 0;
Expand Down
9 changes: 8 additions & 1 deletion src/Valet/Commands/Update.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,21 @@ public class Update : BaseCommand
IsRequired = false,
};

private static readonly Option<bool> PasswordStdInOption = new(new[] { "--password-stdin" })
{
Description = "Access token from standard input to authenticate with GHCR (requires read:packages scope).",
IsRequired = false,
};

protected override Command GenerateCommand(App app)
{
var command = base.GenerateCommand(app);

command.AddOption(UsernameOption);
command.AddOption(PasswordOption);
command.AddOption(PasswordStdInOption);

command.Handler = CommandHandler.Create((string? username, string? password) => app.UpdateValetAsync(username, password));
command.Handler = CommandHandler.Create((string? username, string? password, bool passwordStdin) => app.UpdateValetAsync(username, password, passwordStdin));

return command;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Valet/Interfaces/IDockerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ namespace Valet.Interfaces;

public interface IDockerService
{
Task UpdateImageAsync(string image, string server, string version, string? username, string? password);
Task UpdateImageAsync(string image, string server, string version, string? username, string? password, bool passwordStdin = false);

Task ExecuteCommandAsync(string image, string server, string version, params string[] arguments);

Expand Down
3 changes: 2 additions & 1 deletion src/Valet/Interfaces/IProcessService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Task RunAsync(
string arguments,
string? cwd = null,
IEnumerable<(string, string)>? environmentVariables = null,
bool output = true
bool output = true,
string? inputForStdIn = null
);
}
12 changes: 9 additions & 3 deletions src/Valet/Services/DockerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,24 @@ public DockerService(IProcessService processService)
_processService = processService;
}

public async Task UpdateImageAsync(string image, string server, string version, string? username, string? password)
public async Task UpdateImageAsync(string image, string server, string version, string? username, string? password, bool passwordStdin = false)
{
if (passwordStdin && Console.IsInputRedirected)
{
password = await Console.In.ReadToEndAsync();
}

if (!string.IsNullOrWhiteSpace(username) && !string.IsNullOrWhiteSpace(password))
{
await _processService.RunAsync(
"docker",
$"login {server} --password {password} --username {username}"
$"login {server} --username {username} --password-stdin",
inputForStdIn: password
).ConfigureAwait(false);
}
else
{
Console.WriteLine("No GHCR credentials provided.");
Console.WriteLine("INFO: using cached credentials because no GHCR credentials were provided.");
}

await _processService.RunAsync(
Expand Down
44 changes: 20 additions & 24 deletions src/Valet/Services/ProcessService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ namespace Valet.Services;

public class ProcessService : IProcessService
{
public Task RunAsync(
public async Task RunAsync(
string filename,
string arguments,
string? cwd = null,
IEnumerable<(string, string)>? environmentVariables = null,
bool output = true)
bool output = true,
string? inputForStdIn = null)
{
var tcs = new TaskCompletionSource<bool>();
var cts = new CancellationTokenSource();

var startInfo = new ProcessStartInfo
Expand All @@ -21,6 +21,7 @@ public Task RunAsync(
Arguments = arguments,
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = true,
UseShellExecute = false,
WorkingDirectory = cwd,
CreateNoWindow = true
Expand All @@ -34,37 +35,32 @@ public Task RunAsync(
}
}

var process = new Process
using var process = new Process
{
StartInfo = startInfo,
EnableRaisingEvents = true,
EnableRaisingEvents = true
};
process.Start();

void OnProcessExited(object? sender, EventArgs args)
if (!string.IsNullOrWhiteSpace(inputForStdIn))
{
process.Exited -= OnProcessExited;

cts.Cancel();
if (process.ExitCode == 0)
{
tcs.TrySetResult(true);
}
else
{
var error = process.StandardError.ReadToEnd();
tcs.TrySetException(new Exception(error));
}

process.Dispose();
var writer = process.StandardInput;
writer.AutoFlush = true;
await writer.WriteAsync(inputForStdIn);
writer.Close();
}

process.Exited += OnProcessExited;
process.Start();

ReadStream(process.StandardOutput, output, cts.Token);
ReadStream(process.StandardError, output, cts.Token);

return tcs.Task;
await process.WaitForExitAsync(cts.Token);

cts.Cancel();
if (process.ExitCode != 0)
{
var error = await process.StandardError.ReadToEndAsync();
throw new Exception(error);
}
}

private void ReadStream(StreamReader reader, bool output, CancellationToken ctx)
Expand Down

0 comments on commit f204b8d

Please sign in to comment.