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; } }