Per-endpoint RunAs: Service / InteractiveUser / SpecificUser

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>
This commit is contained in:
2026-05-08 09:08:08 -04:00
parent 27e5264714
commit 24d8701b65
10 changed files with 701 additions and 20 deletions
@@ -42,4 +42,6 @@ public sealed class EndpointConfig
public bool Serialize { get; set; }
public CallbackConfig? Callback { get; set; }
public RunAsConfig? RunAs { get; set; }
}
+15
View File
@@ -53,3 +53,18 @@ public enum HttpsBindingKind
PfxFile = 1,
CertStoreThumbprint = 2,
}
public enum RunAsMode
{
/// <summary>Run as whatever account the service itself runs under (default).</summary>
Service = 0,
/// <summary>Run as a specific username + password (batch logon, no UI).</summary>
SpecificUser = 1,
/// <summary>
/// Run in the active console session under whoever is logged in at the keyboard.
/// Lets hooks pop interactive UI on the user's desktop.
/// </summary>
InteractiveUser = 2,
}
@@ -0,0 +1,21 @@
namespace WebhookServer.Core.Models;
public sealed class RunAsConfig
{
public RunAsMode Mode { get; set; } = RunAsMode.Service;
/// <summary>
/// "DOMAIN\user" or "user@upn" or just "user" (local). Required when
/// <see cref="Mode"/> is <see cref="RunAsMode.SpecificUser"/>.
/// </summary>
public string? Username { get; set; }
/// <summary>DPAPI-protected password for SpecificUser mode.</summary>
public ProtectedString? Password { get; set; }
/// <summary>
/// When true, load the user's profile (HKCU + AppData) before running.
/// Slower; only needed for hooks that read user-scope settings.
/// </summary>
public bool LoadProfile { get; set; }
}