using AcDream.App.Configuration;
using AcDream.App.Platform;
namespace AcDream.App.Credentials;
///
/// Campaign LA slice LA1: resolves a --session-config session's
/// credential reference — the App-side mirror of
/// AcDream.Headless.Credentials.HeadlessCredentialResolver (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:
/// environment (read an env var), standardInput (read one line
/// from stdin, mirroring HeadlessCredentialResolver.ResolveStandardInput),
/// and file (read a credential file relative to a base directory,
/// rejecting symlinks and, on Linux, group/other-readable permissions).
///
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;
///
/// is the caller-supplied platform-policy
/// value from GraphicalHostPlatformServices. This file still uses
/// RuntimePlatformGuard.IsLinuxRuntime below as the narrow
/// CA1416-recognized runtime guard required before calling
/// File.GetUnixFileMode; it does not independently select the host
/// platform or bypass the platform-services owner.
///
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;
}
}