24d8701b65
Native per-endpoint identity instead of the schtasks bridge: - Service (default) keeps the existing path - hooks inherit the service account (SYSTEM by default, or whatever you installed under). - SpecificUser binds ProcessStartInfo.UserName / Password / Domain so the hook runs in a batch logon session as the named account. Useful for AD-write hooks that should NOT run as SYSTEM. - InteractiveUser uses WTSQueryUserToken(WTSGetActiveConsoleSessionId) + DuplicateTokenEx + CreateProcessAsUser to drop the child into the logged-in user's session with their environment block. This is the real fix for "calc.exe should pop up on my desktop" - no Task Scheduler bridge required. Stdio is captured via inheritable anonymous pipes so the hook still returns stdout/stderr to the caller normally. Implementation: - New RunAsMode enum + RunAsConfig model on EndpointConfig - ConfigStore round-trips RunAs.Password through DPAPI alongside bearer/HMAC/PFX secrets - AdminPipeServer's secret-merge logic preserves the encrypted blob when the GUI saves an endpoint without re-typing the password - New WebhookServer.Core.Execution.Native namespace with NativeMethods (P/Invoke) and InteractiveProcessLauncher (token-based launcher) - ProcessExecutor branches on RunAs.Mode; the Service/SpecificUser paths share .NET's Process; InteractiveUser uses the launcher - GUI editor gets a "Run as" section: dropdown + conditional username/password/load-profile fields under SpecificUser Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
155 lines
4.8 KiB
C#
155 lines
4.8 KiB
C#
using System.Runtime.Versioning;
|
|
using System.Text.Json;
|
|
using System.Windows;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using WebhookServer.Core.Models;
|
|
using WebhookServer.Core.Storage;
|
|
|
|
namespace WebhookServer.Gui.ViewModels;
|
|
|
|
[SupportedOSPlatform("windows")]
|
|
public sealed partial class EndpointEditorViewModel : ObservableObject
|
|
{
|
|
public EndpointConfig Endpoint { get; }
|
|
public bool IsNew { get; }
|
|
|
|
[ObservableProperty] private bool _accepted;
|
|
|
|
public EndpointEditorViewModel(EndpointConfig template, bool isNew)
|
|
{
|
|
// Deep clone via JSON so cancel-on-close cleanly drops edits.
|
|
var json = JsonSerializer.Serialize(template, ConfigJson.Compact);
|
|
Endpoint = JsonSerializer.Deserialize<EndpointConfig>(json, ConfigJson.Compact)!;
|
|
Endpoint.Bearer ??= new BearerOptions();
|
|
Endpoint.Hmac ??= new HmacOptions();
|
|
Endpoint.RunAs ??= new RunAsConfig();
|
|
Endpoint.RunAs.Password ??= new ProtectedString();
|
|
IsNew = isNew;
|
|
}
|
|
|
|
public Array AuthModes { get; } = Enum.GetValues(typeof(AuthMode));
|
|
public Array ExecutorTypes { get; } = Enum.GetValues(typeof(ExecutorType));
|
|
public Array ResponseModes { get; } = Enum.GetValues(typeof(ResponseMode));
|
|
|
|
/// <summary>
|
|
/// Proxy for <see cref="EndpointConfig.AuthMode"/> that emits change notifications
|
|
/// for the visibility flags so the bearer/HMAC sections show/hide reactively.
|
|
/// </summary>
|
|
public AuthMode SelectedAuthMode
|
|
{
|
|
get => Endpoint.AuthMode;
|
|
set
|
|
{
|
|
if (Endpoint.AuthMode == value) return;
|
|
Endpoint.AuthMode = value;
|
|
OnPropertyChanged();
|
|
OnPropertyChanged(nameof(BearerVisible));
|
|
OnPropertyChanged(nameof(HmacVisible));
|
|
}
|
|
}
|
|
|
|
public Visibility BearerVisible =>
|
|
Endpoint.AuthMode == AuthMode.Bearer ? Visibility.Visible : Visibility.Collapsed;
|
|
|
|
public Visibility HmacVisible =>
|
|
Endpoint.AuthMode == AuthMode.Hmac ? Visibility.Visible : Visibility.Collapsed;
|
|
|
|
public Array RunAsModes { get; } = Enum.GetValues(typeof(RunAsMode));
|
|
|
|
public RunAsMode SelectedRunAsMode
|
|
{
|
|
get => Endpoint.RunAs?.Mode ?? RunAsMode.Service;
|
|
set
|
|
{
|
|
Endpoint.RunAs ??= new RunAsConfig();
|
|
if (Endpoint.RunAs.Mode == value) return;
|
|
Endpoint.RunAs.Mode = value;
|
|
OnPropertyChanged();
|
|
OnPropertyChanged(nameof(SpecificUserVisible));
|
|
}
|
|
}
|
|
|
|
public Visibility SpecificUserVisible =>
|
|
SelectedRunAsMode == RunAsMode.SpecificUser ? Visibility.Visible : Visibility.Collapsed;
|
|
|
|
public string RunAsUsername
|
|
{
|
|
get => Endpoint.RunAs?.Username ?? "";
|
|
set
|
|
{
|
|
Endpoint.RunAs ??= new RunAsConfig();
|
|
Endpoint.RunAs.Username = string.IsNullOrEmpty(value) ? null : value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
public string RunAsPassword
|
|
{
|
|
get => Endpoint.RunAs?.Password?.Plaintext ?? "";
|
|
set
|
|
{
|
|
Endpoint.RunAs ??= new RunAsConfig();
|
|
Endpoint.RunAs.Password ??= new ProtectedString();
|
|
Endpoint.RunAs.Password.Plaintext = string.IsNullOrEmpty(value) ? null : value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
public bool RunAsLoadProfile
|
|
{
|
|
get => Endpoint.RunAs?.LoadProfile ?? false;
|
|
set
|
|
{
|
|
Endpoint.RunAs ??= new RunAsConfig();
|
|
Endpoint.RunAs.LoadProfile = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
public string AllowedClientsText
|
|
{
|
|
get => string.Join(Environment.NewLine, Endpoint.AllowedClients);
|
|
set
|
|
{
|
|
Endpoint.AllowedClients = (value ?? "").Split(new[] { '\r', '\n', ',' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
public string ExecutableArgsText
|
|
{
|
|
get => string.Join(" ", Endpoint.ExecutableArgs);
|
|
set
|
|
{
|
|
Endpoint.ExecutableArgs = (value ?? "").Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
public string BearerSecret
|
|
{
|
|
get => Endpoint.Bearer?.Secret.Plaintext ?? "";
|
|
set
|
|
{
|
|
Endpoint.Bearer ??= new BearerOptions();
|
|
Endpoint.Bearer.Secret.Plaintext = string.IsNullOrEmpty(value) ? null : value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
public string HmacSecret
|
|
{
|
|
get => Endpoint.Hmac?.Secret.Plaintext ?? "";
|
|
set
|
|
{
|
|
Endpoint.Hmac ??= new HmacOptions();
|
|
Endpoint.Hmac.Secret.Plaintext = string.IsNullOrEmpty(value) ? null : value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void Save() => Accepted = true;
|
|
}
|