Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Breaking

- **`ICredentialManager` members now take a `CancellationToken`** (#74). Every member gains a
trailing `CancellationToken cancellationToken = default`. Existing *callers* keep compiling —
the parameter is optional — but any external **implementation** of the interface must update
its signatures. The three built-in backends are updated. `ICredentialCollector` and
`ICredentialSummaryProvider` are untouched, so the provider packages are unaffected.

The libsecret backend now binds the token to a `GCancellable` for every call, so
cancellation reaches libsecret rather than stopping at the managed boundary. The Keychain
backend honours the token at entry; Security.framework's `SecItem*` API offers no
mid-call cancellation, and that is documented rather than faked.

**What this does not fix, verified rather than assumed:** it does not rescue the KWallet
wedge that prompted the issue. Against `ksecretd` the cancellation callback does fire, but
the call still never returns, because ksecretd emits the prompt's `Completed` signal with an
`ao` payload where libsecret expects `o` — libsecret's `secret_service_real_prompt_async`
GTask is then *"finalized without ever returning"* and its nested main loop never exits.
That is an upstream libsecret/KWallet incompatibility, and it is recorded in the backend's
remarks instead of being papered over.

This makes the next release **2.0.0**; the version bump itself is a separate release PR.

### Changed

- **The libsecret backend is now documented as GNOME Keyring only** (#19). It was described
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,14 @@ protected override async Task<int> ExecuteAsync(CommandContext context, Settings
collector.ProviderName,
settings.AccountName,
environment,
credentialData).ConfigureAwait(false);
credentialData, cancellationToken).ConfigureAwait(false);

AnsiConsole.MarkupLine($"[green]Successfully added credential with ID: {accountId}[/]");

// Ask if user wants to select this credential as active
if (await AnsiConsole.ConfirmAsync("Do you want to set this as the active credential for this environment?", cancellationToken: cancellationToken).ConfigureAwait(false))
{
_ = await _credentialManager.SelectCredentialAsync(accountId).ConfigureAwait(false);
_ = await _credentialManager.SelectCredentialAsync(accountId, cancellationToken).ConfigureAwait(false);
AnsiConsole.MarkupLine("[green]Credential selected as active.[/]");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ protected override async Task<int> ExecuteAsync(CommandContext context, Settings
else
{
// Interactive selection
var providers = await _credentialManager.GetProviderNamesAsync().ConfigureAwait(false);
var providers = await _credentialManager.GetProviderNamesAsync(cancellationToken).ConfigureAwait(false);
if (!providers.Any())
{
AnsiConsole.MarkupLine("[yellow]No credentials found.[/]");
Expand All @@ -41,7 +41,7 @@ protected override async Task<int> ExecuteAsync(CommandContext context, Settings
var allCredentials = new List<CredentialSummary>();
foreach (var provider in providers)
{
var credentials = await _credentialManager.ListCredentialsAsync(provider).ConfigureAwait(false);
var credentials = await _credentialManager.ListCredentialsAsync(provider, cancellationToken).ConfigureAwait(false);
allCredentials.AddRange(credentials);
}

Expand Down Expand Up @@ -70,7 +70,7 @@ protected override async Task<int> ExecuteAsync(CommandContext context, Settings
return 0;
}

var success = await _credentialManager.DeleteCredentialAsync(accountId).ConfigureAwait(false);
var success = await _credentialManager.DeleteCredentialAsync(accountId, cancellationToken).ConfigureAwait(false);

if (success)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ protected override async Task<int> ExecuteAsync(CommandContext context, Settings
}

var service = new CredentialPortabilityService(_credentialManager);
var result = await service.ExportAsync(passphrase).ConfigureAwait(false);
var result = await service.ExportAsync(passphrase, cancellationToken).ConfigureAwait(false);

if (result.Count == 0)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ protected override async Task<int> ExecuteAsync(CommandContext context, Settings
var resolver = BuildConflictResolver(settings.OnConflict, forcedResolution);

var service = new CredentialPortabilityService(_credentialManager);
var result = await service.ImportAsync(bundle, passphrase, resolver).ConfigureAwait(false);
var result = await service.ImportAsync(bundle, passphrase, resolver, cancellationToken).ConfigureAwait(false);

AnsiConsole.MarkupLine(
$"[green]Import complete:[/] {result.Added} added, {result.Overwritten} overwritten, {result.Skipped} skipped.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ protected override async Task<int> ExecuteAsync(CommandContext context, Settings
{
if (string.IsNullOrWhiteSpace(settings.Provider))
{
var providers = await _credentialManager.GetProviderNamesAsync().ConfigureAwait(false);
var providers = await _credentialManager.GetProviderNamesAsync(cancellationToken).ConfigureAwait(false);

if (!providers.Any())
{
Expand All @@ -50,13 +50,13 @@ protected override async Task<int> ExecuteAsync(CommandContext context, Settings

foreach (var provider in providers)
{
await DisplayCredentialsForProvider(provider).ConfigureAwait(false);
await DisplayCredentialsForProvider(provider, cancellationToken).ConfigureAwait(false);
AnsiConsole.WriteLine();
}
}
else
{
await DisplayCredentialsForProvider(settings.Provider).ConfigureAwait(false);
await DisplayCredentialsForProvider(settings.Provider, cancellationToken).ConfigureAwait(false);
}

return 0;
Expand All @@ -69,9 +69,9 @@ protected override async Task<int> ExecuteAsync(CommandContext context, Settings
}

[System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "<Pending>")]
private async Task DisplayCredentialsForProvider(string provider)
private async Task DisplayCredentialsForProvider(string provider, CancellationToken cancellationToken)
{
var credentials = (await _credentialManager.ListCredentialsAsync(provider).ConfigureAwait(false)).ToList();
var credentials = (await _credentialManager.ListCredentialsAsync(provider, cancellationToken).ConfigureAwait(false)).ToList();

if (credentials.Count == 0)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ protected override async Task<int> ExecuteAsync(CommandContext context, Settings
else
{
// Interactive selection
var providers = await _credentialManager.GetProviderNamesAsync().ConfigureAwait(false);
var providers = await _credentialManager.GetProviderNamesAsync(cancellationToken).ConfigureAwait(false);
if (!providers.Any())
{
AnsiConsole.MarkupLine("[yellow]No credentials found.[/]");
Expand All @@ -42,7 +42,7 @@ protected override async Task<int> ExecuteAsync(CommandContext context, Settings
var allCredentials = new List<CredentialSummary>();
foreach (var provider in providers)
{
var credentials = await _credentialManager.ListCredentialsAsync(provider).ConfigureAwait(false);
var credentials = await _credentialManager.ListCredentialsAsync(provider, cancellationToken).ConfigureAwait(false);
allCredentials.AddRange(credentials);
}

Expand All @@ -63,7 +63,7 @@ protected override async Task<int> ExecuteAsync(CommandContext context, Settings
accountId = allCredentials[selectedIndex].AccountId;
}

var success = await _credentialManager.SelectCredentialAsync(accountId).ConfigureAwait(false);
var success = await _credentialManager.SelectCredentialAsync(accountId, cancellationToken).ConfigureAwait(false);

if (success)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ namespace NextIteration.SpectreConsole.Auth.Persistence
/// </remarks>
internal static class AtomicFile
{
internal static async Task WriteAllTextAsync(string path, string contents, UnixFileMode? unixMode = null)
internal static async Task WriteAllTextAsync(string path, string contents, UnixFileMode? unixMode = null, CancellationToken cancellationToken = default)
{
var tempPath = BuildTempPath(path);
try
{
await WriteTempAsync(tempPath, System.Text.Encoding.UTF8.GetBytes(contents), unixMode).ConfigureAwait(false);
await WriteTempAsync(tempPath, System.Text.Encoding.UTF8.GetBytes(contents), unixMode, cancellationToken).ConfigureAwait(false);
await ReplaceAtomicallyAsync(tempPath, path).ConfigureAwait(false);
}
catch
Expand All @@ -54,7 +54,7 @@ internal static async Task WriteAllTextAsync(string path, string contents, UnixF
/// <c>accounts export</c> writes to a path the user chooses — exporting to <c>/tmp</c>
/// or any shared directory exposed every secret in the store for that window (#54).
/// </remarks>
private static async Task WriteTempAsync(string tempPath, byte[] bytes, UnixFileMode? unixMode)
private static async Task WriteTempAsync(string tempPath, byte[] bytes, UnixFileMode? unixMode, CancellationToken cancellationToken)
{
var options = new FileStreamOptions
{
Expand All @@ -71,7 +71,7 @@ private static async Task WriteTempAsync(string tempPath, byte[] bytes, UnixFile
var stream = new FileStream(tempPath, options);
await using (stream.ConfigureAwait(false))
{
await stream.WriteAsync(bytes).ConfigureAwait(false);
await stream.WriteAsync(bytes, cancellationToken).ConfigureAwait(false);
}
}

Expand All @@ -92,7 +92,7 @@ private static async Task WriteTempAsync(string tempPath, byte[] bytes, UnixFile
/// the winner. Used for the keystore, where an overwrite silently destroys every
/// credential already encrypted under the replaced key.
/// </remarks>
internal static async Task<bool> TryWriteNewAsync(string path, byte[] bytes, UnixFileMode? unixMode = null)
internal static async Task<bool> TryWriteNewAsync(string path, byte[] bytes, UnixFileMode? unixMode = null, CancellationToken cancellationToken = default)
{
if (File.Exists(path))
{
Expand All @@ -102,7 +102,7 @@ internal static async Task<bool> TryWriteNewAsync(string path, byte[] bytes, Uni
var tempPath = BuildTempPath(path);
try
{
await WriteTempAsync(tempPath, bytes, unixMode).ConfigureAwait(false);
await WriteTempAsync(tempPath, bytes, unixMode, cancellationToken).ConfigureAwait(false);

// Deliberately not overwrite:true. A racing creator that already landed
// its keystore must win; we then fall back to reading theirs.
Expand All @@ -122,12 +122,12 @@ internal static async Task<bool> TryWriteNewAsync(string path, byte[] bytes, Uni
}
}

internal static async Task WriteAllBytesAsync(string path, byte[] bytes, UnixFileMode? unixMode = null)
internal static async Task WriteAllBytesAsync(string path, byte[] bytes, UnixFileMode? unixMode = null, CancellationToken cancellationToken = default)
{
var tempPath = BuildTempPath(path);
try
{
await WriteTempAsync(tempPath, bytes, unixMode).ConfigureAwait(false);
await WriteTempAsync(tempPath, bytes, unixMode, cancellationToken).ConfigureAwait(false);
await ReplaceAtomicallyAsync(tempPath, path).ConfigureAwait(false);
}
catch
Expand Down
Loading