Refactor SAML configuration deployment and enhance Authentik integration
- Removed SAML configuration deployment calls from PostInstanceInitService. - Updated DeploySamlConfigurationAsync to improve template fetching logic from Git and local directories. - Added Authentik flow and keypair models for better representation in the UI. - Enhanced SettingsViewModel to include Authentik settings with save and test functionality. - Updated UI to support Authentik configuration, including dropdowns for flows and keypairs. - Changed default CMS server name template to "app.ots-signs.com" across various files. - Improved password handling in SshDockerCliService for secure shell command execution. - Added new template file for settings-custom.php in the project structure.
This commit is contained in:
@@ -41,6 +41,70 @@ public class SshDockerCliService : IDockerCliService
|
||||
|
||||
public SshHost? CurrentHost => _currentHost;
|
||||
|
||||
private void EnsureHost()
|
||||
{
|
||||
if (_currentHost == null)
|
||||
throw new InvalidOperationException("No SSH host configured. Call SetHost() before using Docker commands.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escape password for safe use in shell scripts with proper quoting.
|
||||
/// Uses printf-safe escaping to avoid newline injection and special character issues.
|
||||
/// </summary>
|
||||
private string EscapePasswordForShell(string password)
|
||||
{
|
||||
if (string.IsNullOrEmpty(password))
|
||||
{
|
||||
_logger.LogWarning("Password is null or empty");
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Original password length: {Length} characters", password.Length);
|
||||
|
||||
// Use printf-safe format: escape single quotes and other problematic characters
|
||||
// Replace ' with '\'' (close quote, escaped quote, open quote)
|
||||
var escaped = password.Replace("'", "'\\''");
|
||||
|
||||
_logger.LogDebug("Escaped password length: {Length} characters (added {Extra} chars for escaping)",
|
||||
escaped.Length, escaped.Length - password.Length);
|
||||
_logger.LogDebug("Password first char: '{FirstChar}', last char: '{LastChar}'",
|
||||
password.Length > 0 ? password[0].ToString() : "N/A",
|
||||
password.Length > 0 ? password[^1].ToString() : "N/A");
|
||||
|
||||
return escaped;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test if the current host's password works with sudo by running a no-op sudo command.
|
||||
/// </summary>
|
||||
private async Task<(bool Success, string? Error)> TestSudoPasswordAsync()
|
||||
{
|
||||
EnsureHost();
|
||||
|
||||
if (string.IsNullOrEmpty(_currentHost!.Password))
|
||||
{
|
||||
return (false, "No password configured for SSH host");
|
||||
}
|
||||
|
||||
var escapedPassword = EscapePasswordForShell(_currentHost!.Password);
|
||||
var testCmd = $"printf '%s\\n' '{escapedPassword}' | sudo -S -v 2>&1";
|
||||
|
||||
_logger.LogInformation("Testing sudo password for host {Host} user {User}...",
|
||||
_currentHost!.Label, _currentHost!.Username);
|
||||
|
||||
var (exitCode, stdout, stderr) = await _ssh.RunCommandAsync(_currentHost!, testCmd, TimeSpan.FromSeconds(10));
|
||||
|
||||
if (exitCode == 0)
|
||||
{
|
||||
_logger.LogInformation("Sudo password test PASSED for {Host}", _currentHost!.Label);
|
||||
return (true, null);
|
||||
}
|
||||
|
||||
var error = (stderr ?? stdout ?? "unknown error").Trim();
|
||||
_logger.LogWarning("Sudo password test FAILED for {Host}: {Error}", _currentHost!.Label, error);
|
||||
return (false, error);
|
||||
}
|
||||
|
||||
public async Task<DeploymentResultDto> DeployStackAsync(string stackName, string composeYaml, bool resolveImage = false)
|
||||
{
|
||||
EnsureHost();
|
||||
@@ -184,12 +248,22 @@ public class SshDockerCliService : IDockerCliService
|
||||
// Single SSH command: create temp dir, mount NFS, mkdir -p all folders, unmount, cleanup
|
||||
// Use addr= to pin the server IP — avoids "Server address does not match proto= option"
|
||||
// errors when the hostname resolves to IPv6 but proto=tcp implies IPv4.
|
||||
// Properly escape password for shell use (handle special characters like single quotes)
|
||||
var escapedPassword = EscapePasswordForShell(_currentHost!.Password ?? string.Empty);
|
||||
|
||||
if (string.IsNullOrEmpty(escapedPassword))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"No password configured for SSH host {Host}. NFS operations may fail if sudo requires a password.",
|
||||
_currentHost!.Label);
|
||||
}
|
||||
|
||||
var script = $"""
|
||||
set -e
|
||||
MNT=$(mktemp -d)
|
||||
sudo mount -t nfs -o addr={nfsServer},nfsvers=4,proto=tcp,soft,timeo=50,retrans=2 {nfsServer}:/{exportPath} "$MNT"
|
||||
sudo mkdir -p {mkdirTargets}
|
||||
sudo umount "$MNT"
|
||||
printf '%s\n' '{escapedPassword}' | sudo -S mount -t nfs -o addr={nfsServer},nfsvers=4,proto=tcp,soft,timeo=50,retrans=2 {nfsServer}:/{exportPath} "$MNT"
|
||||
printf '%s\n' '{escapedPassword}' | sudo -S mkdir -p {mkdirTargets}
|
||||
printf '%s\n' '{escapedPassword}' | sudo -S umount "$MNT"
|
||||
rmdir "$MNT"
|
||||
""";
|
||||
|
||||
@@ -229,12 +303,22 @@ public class SshDockerCliService : IDockerCliService
|
||||
var folderList = folderNames.Select(f => $"\"$MNT{subPath}/{f}\"").ToList();
|
||||
var mkdirTargets = string.Join(" ", folderList);
|
||||
|
||||
// Properly escape password for shell use (handle special characters like single quotes)
|
||||
var escapedPassword = EscapePasswordForShell(_currentHost!.Password ?? string.Empty);
|
||||
|
||||
if (string.IsNullOrEmpty(escapedPassword))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"No password configured for SSH host {Host}. NFS operations may fail if sudo requires a password.",
|
||||
_currentHost!.Label);
|
||||
}
|
||||
|
||||
var script = $"""
|
||||
set -e
|
||||
MNT=$(mktemp -d)
|
||||
sudo mount -t nfs -o addr={nfsServer},nfsvers=4,proto=tcp,soft,timeo=50,retrans=2 {nfsServer}:/{exportPath} "$MNT"
|
||||
sudo mkdir -p {mkdirTargets}
|
||||
sudo umount "$MNT"
|
||||
printf '%s\n' '{escapedPassword}' | sudo -S mount -t nfs -o addr={nfsServer},nfsvers=4,proto=tcp,soft,timeo=50,retrans=2 {nfsServer}:/{exportPath} "$MNT"
|
||||
printf '%s\n' '{escapedPassword}' | sudo -S mkdir -p {mkdirTargets}
|
||||
printf '%s\n' '{escapedPassword}' | sudo -S umount "$MNT"
|
||||
rmdir "$MNT"
|
||||
""";
|
||||
|
||||
@@ -271,22 +355,44 @@ public class SshDockerCliService : IDockerCliService
|
||||
var subFolder = (nfsExportFolder ?? string.Empty).Trim('/');
|
||||
var subPath = string.IsNullOrEmpty(subFolder) ? string.Empty : $"/{subFolder}";
|
||||
|
||||
// Ensure parent directory exists, then write content via heredoc
|
||||
// Ensure parent directory exists
|
||||
var targetPath = $"$MNT{subPath}/{relativePath.TrimStart('/')}";
|
||||
var parentDir = $"$(dirname \"{targetPath}\")";
|
||||
|
||||
// Escape content for heredoc (replace any literal EOF that might appear in content)
|
||||
var safeContent = content.Replace("'", "'\\''");
|
||||
// Properly escape password for shell use (handle special characters like single quotes)
|
||||
var escapedPassword = EscapePasswordForShell(_currentHost!.Password ?? string.Empty);
|
||||
|
||||
_logger.LogInformation("NFS WriteFile: Host={Host}, User={User}, HasPassword={HasPw}, PwLen={PwLen}",
|
||||
_currentHost!.Label, _currentHost!.Username,
|
||||
!string.IsNullOrEmpty(_currentHost!.Password), _currentHost!.Password?.Length ?? 0);
|
||||
|
||||
if (string.IsNullOrEmpty(escapedPassword))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"No password configured for SSH host {Host}. NFS operations may fail if sudo requires a password.",
|
||||
_currentHost!.Label);
|
||||
return (false, "No password configured for SSH host");
|
||||
}
|
||||
|
||||
// Base64-encode the file content to avoid heredoc/stdin conflicts with sudo -S.
|
||||
// The heredoc approach fails because the shell's heredoc redirects stdin for the
|
||||
// entire pipeline, so sudo -S reads the PHP content instead of the password.
|
||||
var base64Content = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(content));
|
||||
|
||||
// Strategy: base64-decode content to a temp file (no sudo needed), then use
|
||||
// printf | sudo -S for each privileged command — matching the proven pattern
|
||||
// in EnsureNfsFoldersAsync. We avoid sudo -v timestamp caching because SSH
|
||||
// exec channels have no TTY and timestamps may not persist between commands.
|
||||
var script = $"""
|
||||
set -e
|
||||
TMPFILE=$(mktemp)
|
||||
echo '{base64Content}' | base64 -d > "$TMPFILE"
|
||||
MNT=$(mktemp -d)
|
||||
sudo mount -t nfs -o addr={nfsServer},nfsvers=4,proto=tcp,soft,timeo=50,retrans=2 {nfsServer}:/{exportPath} "$MNT"
|
||||
sudo mkdir -p {parentDir}
|
||||
sudo tee "{targetPath}" > /dev/null << 'OTSSIGNS_EOF'
|
||||
{content}
|
||||
OTSSIGNS_EOF
|
||||
sudo umount "$MNT"
|
||||
printf '%s\n' '{escapedPassword}' | sudo -S mount -t nfs -o addr={nfsServer},nfsvers=4,proto=tcp,soft,timeo=50,retrans=2 {nfsServer}:/{exportPath} "$MNT"
|
||||
printf '%s\n' '{escapedPassword}' | sudo -S mkdir -p {parentDir}
|
||||
printf '%s\n' '{escapedPassword}' | sudo -S cp "$TMPFILE" "{targetPath}"
|
||||
rm -f "$TMPFILE"
|
||||
printf '%s\n' '{escapedPassword}' | sudo -S umount "$MNT"
|
||||
rmdir "$MNT"
|
||||
""";
|
||||
|
||||
@@ -609,12 +715,6 @@ public class SshDockerCliService : IDockerCliService
|
||||
return serviceName.StartsWith(prefix) ? serviceName[prefix.Length..] : serviceName;
|
||||
}
|
||||
|
||||
private void EnsureHost()
|
||||
{
|
||||
if (_currentHost == null)
|
||||
throw new InvalidOperationException("No SSH host configured. Call SetHost() before using Docker commands.");
|
||||
}
|
||||
|
||||
public async Task<bool> RemoveStackVolumesAsync(string stackName)
|
||||
{
|
||||
EnsureHost();
|
||||
|
||||
Reference in New Issue
Block a user