156 lines
5.8 KiB
C#
156 lines
5.8 KiB
C#
using AcDream.App.Configuration;
|
|
using AcDream.App.Platform;
|
|
|
|
namespace AcDream.App.Credentials;
|
|
|
|
/// <summary>
|
|
/// Campaign LA slice LA1: resolves a <c>--session-config</c> session's
|
|
/// credential reference — the App-side mirror of
|
|
/// <c>AcDream.Headless.Credentials.HeadlessCredentialResolver</c> (see that
|
|
/// type's own file for why this is an independent port rather than a shared
|
|
/// reference). Supports the same three providers with the same semantics:
|
|
/// <c>environment</c> (read an env var), <c>standardInput</c> (read one line
|
|
/// from stdin, mirroring <c>HeadlessCredentialResolver.ResolveStandardInput</c>),
|
|
/// and <c>file</c> (read a credential file relative to a base directory,
|
|
/// rejecting symlinks and, on Linux, group/other-readable permissions).
|
|
/// </summary>
|
|
internal sealed class AppCredentialResolver
|
|
{
|
|
private const UnixFileMode NonUserPermissionMask =
|
|
UnixFileMode.GroupRead
|
|
| UnixFileMode.GroupWrite
|
|
| UnixFileMode.GroupExecute
|
|
| UnixFileMode.OtherRead
|
|
| UnixFileMode.OtherWrite
|
|
| UnixFileMode.OtherExecute;
|
|
|
|
private readonly TextReader _standardInput;
|
|
private readonly string _credentialBaseDirectory;
|
|
private readonly bool _isLinux;
|
|
|
|
/// <summary>
|
|
/// <paramref name="isLinux"/> is the caller-supplied platform-policy
|
|
/// value from <c>GraphicalHostPlatformServices</c>. This file still uses
|
|
/// <c>RuntimePlatformGuard.IsLinuxRuntime</c> below as the narrow
|
|
/// CA1416-recognized runtime guard required before calling
|
|
/// <c>File.GetUnixFileMode</c>; it does not independently select the host
|
|
/// platform or bypass the platform-services owner.
|
|
/// </summary>
|
|
internal AppCredentialResolver(
|
|
TextReader standardInput,
|
|
string credentialBaseDirectory,
|
|
bool isLinux)
|
|
{
|
|
_standardInput = standardInput
|
|
?? throw new ArgumentNullException(nameof(standardInput));
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(credentialBaseDirectory);
|
|
_credentialBaseDirectory = Path.GetFullPath(credentialBaseDirectory);
|
|
_isLinux = isLinux;
|
|
}
|
|
|
|
internal AppCredentialSecret Resolve(
|
|
string sessionId,
|
|
SessionCredentialDescriptor credential)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(sessionId);
|
|
ArgumentNullException.ThrowIfNull(credential);
|
|
|
|
string value;
|
|
try
|
|
{
|
|
value = credential.Provider switch
|
|
{
|
|
SessionCredentialProviderKind.Environment =>
|
|
ResolveEnvironment(credential.Reference),
|
|
SessionCredentialProviderKind.StandardInput =>
|
|
ResolveStandardInput(credential.Reference),
|
|
SessionCredentialProviderKind.File =>
|
|
ResolveFile(credential.Reference),
|
|
_ => throw new AppCredentialException(
|
|
$"Session '{sessionId}' uses an unsupported credential provider."),
|
|
};
|
|
}
|
|
catch (AppCredentialException)
|
|
{
|
|
throw;
|
|
}
|
|
catch (Exception error)
|
|
when (error is IOException
|
|
or UnauthorizedAccessException
|
|
or ArgumentException
|
|
or NotSupportedException)
|
|
{
|
|
throw new AppCredentialException(
|
|
$"Credential '{credential.Reference}' for session '{sessionId}' could not be resolved.",
|
|
error);
|
|
}
|
|
|
|
try
|
|
{
|
|
return new AppCredentialSecret(credential.Reference, value.AsSpan());
|
|
}
|
|
finally
|
|
{
|
|
// The BCL returns immutable strings from environment, TextReader,
|
|
// and File APIs. Do not retain another copy in the resolver; the
|
|
// erasable char[] owner becomes the sole explicit retained copy.
|
|
value = string.Empty;
|
|
}
|
|
}
|
|
|
|
private static string ResolveEnvironment(string reference)
|
|
{
|
|
string? value = Environment.GetEnvironmentVariable(reference);
|
|
if (string.IsNullOrEmpty(value))
|
|
{
|
|
throw new AppCredentialException(
|
|
$"Credential environment reference '{reference}' is unavailable.");
|
|
}
|
|
return value;
|
|
}
|
|
|
|
private string ResolveStandardInput(string reference)
|
|
{
|
|
string? value = _standardInput.ReadLine();
|
|
if (string.IsNullOrEmpty(value))
|
|
{
|
|
throw new AppCredentialException(
|
|
$"Credential standard-input reference '{reference}' is unavailable.");
|
|
}
|
|
return value;
|
|
}
|
|
|
|
private string ResolveFile(string reference)
|
|
{
|
|
string path = Path.GetFullPath(reference, _credentialBaseDirectory);
|
|
var file = new FileInfo(path);
|
|
if (file.LinkTarget is not null)
|
|
{
|
|
throw new AppCredentialException(
|
|
$"Credential file reference '{reference}' cannot be a symbolic link.");
|
|
}
|
|
|
|
// RuntimePlatformGuard.IsLinuxRuntime is the CA1416-recognized guard
|
|
// for File.GetUnixFileMode below; _isLinux is the separate,
|
|
// caller-injected value tests use for deterministic cross-platform
|
|
// coverage (see the constructor's own doc).
|
|
if (RuntimePlatformGuard.IsLinuxRuntime && _isLinux)
|
|
{
|
|
UnixFileMode mode = File.GetUnixFileMode(path);
|
|
if ((mode & NonUserPermissionMask) != 0
|
|
|| (mode & UnixFileMode.UserRead) == 0)
|
|
{
|
|
throw new AppCredentialException(
|
|
$"Credential file reference '{reference}' must be readable only by its owner.");
|
|
}
|
|
}
|
|
|
|
string value = File.ReadAllText(path).TrimEnd('\r', '\n');
|
|
if (value.Length == 0)
|
|
{
|
|
throw new AppCredentialException(
|
|
$"Credential file reference '{reference}' is empty.");
|
|
}
|
|
return value;
|
|
}
|
|
}
|