feat(kiosk): revert kiosk (shell launcher + escapes) on wizard success
This commit is contained in:
@@ -33,6 +33,7 @@ public sealed class ApplyService(IProcessRunner runner, IAccountService accounts
|
||||
await bitlocker.EnableAsync(req.BitLockerPin, ct);
|
||||
|
||||
progress.Report(new("Finishing up", 95));
|
||||
await bootstrap.RevertKioskAsync(ct); // revert kiosk before account deletion (SID must still resolve)
|
||||
await bootstrap.TearDownAsync(req.BootstrapUser, ct); // last — only after success
|
||||
progress.Report(new("Done", 100));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,25 @@
|
||||
namespace SilverOS.Welcome.Core.Apply;
|
||||
public sealed class BootstrapService(IProcessRunner runner) : IBootstrapService
|
||||
{
|
||||
public async Task RevertKioskAsync(CancellationToken ct = default)
|
||||
{
|
||||
// Remove sm-bootstrap custom shell entry + disable Shell Launcher's per-user entry.
|
||||
await Ps(
|
||||
"$c='root\\\\standardcimv2\\\\embedded';" +
|
||||
"$w=Get-CimInstance -Namespace $c -ClassName WESL_UserSetting -EA SilentlyContinue;" +
|
||||
"if($w){" +
|
||||
"$sid=(New-Object System.Security.Principal.NTAccount('sm-bootstrap')).Translate([System.Security.Principal.SecurityIdentifier]).Value;" +
|
||||
"Invoke-CimMethod -InputObject $w -MethodName RemoveCustomShell -Arguments @{Sid=$sid} -EA SilentlyContinue | Out-Null;" +
|
||||
"Invoke-CimMethod -InputObject $w -MethodName SetEnabled -Arguments @{Enabled=$false} -EA SilentlyContinue | Out-Null" +
|
||||
"}",
|
||||
"Revert Shell Launcher", ct);
|
||||
// Revert escape policies set by Configure-Kiosk.ps1.
|
||||
await Ps(
|
||||
"$s='HKLM:\\\\SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Policies\\\\System';" +
|
||||
"Remove-ItemProperty $s -Name DisableTaskMgr,DisableLockWorkstation,HideFastUserSwitching -EA SilentlyContinue",
|
||||
"Revert escape policies", ct);
|
||||
}
|
||||
|
||||
public async Task TearDownAsync(string bootstrapUser, CancellationToken ct = default)
|
||||
{
|
||||
const string key = "'HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon'";
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
namespace SilverOS.Welcome.Core.Apply;
|
||||
public interface IBootstrapService { Task TearDownAsync(string bootstrapUser, CancellationToken ct = default); }
|
||||
public interface IBootstrapService
|
||||
{
|
||||
Task RevertKioskAsync(CancellationToken ct = default);
|
||||
Task TearDownAsync(string bootstrapUser, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
using Moq;
|
||||
using SilverOS.Welcome.Core.Apply;
|
||||
|
||||
public class BootstrapServiceRevertKioskTests
|
||||
{
|
||||
private static Mock<IProcessRunner> Ok()
|
||||
{
|
||||
var m = new Mock<IProcessRunner>();
|
||||
m.Setup(r => r.RunAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new ProcessResult(0, "", ""));
|
||||
return m;
|
||||
}
|
||||
|
||||
private static Mock<IProcessRunner> Fail()
|
||||
{
|
||||
var m = new Mock<IProcessRunner>();
|
||||
m.Setup(r => r.RunAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new ProcessResult(1, "", "the operation failed"));
|
||||
return m;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RevertKioskAsync_throws_on_nonzero_exit()
|
||||
{
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
new BootstrapService(Fail().Object).RevertKioskAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RevertKioskAsync_removes_custom_shell_and_disables_shell_launcher()
|
||||
{
|
||||
var run = Ok();
|
||||
await new BootstrapService(run.Object).RevertKioskAsync();
|
||||
// First call: Shell Launcher revert — must reference WESL_UserSetting and RemoveCustomShell + SetEnabled.
|
||||
run.Verify(r => r.RunAsync("powershell.exe", It.Is<string>(s =>
|
||||
s.Contains("WESL_UserSetting") &&
|
||||
s.Contains("RemoveCustomShell") &&
|
||||
s.Contains("SetEnabled")),
|
||||
It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RevertKioskAsync_reverts_escape_policies()
|
||||
{
|
||||
var run = Ok();
|
||||
await new BootstrapService(run.Object).RevertKioskAsync();
|
||||
// Second call: policy revert — must remove the three escape policy values.
|
||||
run.Verify(r => r.RunAsync("powershell.exe", It.Is<string>(s =>
|
||||
s.Contains("Remove-ItemProperty") &&
|
||||
s.Contains("DisableTaskMgr") &&
|
||||
s.Contains("DisableLockWorkstation") &&
|
||||
s.Contains("HideFastUserSwitching")),
|
||||
It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApplyService_calls_revert_kiosk_before_teardown()
|
||||
{
|
||||
var order = new List<string>();
|
||||
var run = new Mock<IProcessRunner>();
|
||||
run.Setup(r => r.RunAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<string, string, CancellationToken>((_, a, _) =>
|
||||
{
|
||||
if (a.Contains("Invoke-Hardening")) order.Add("modules");
|
||||
})
|
||||
.ReturnsAsync(new ProcessResult(0, "", ""));
|
||||
|
||||
var acct = new Mock<IAccountService>();
|
||||
acct.Setup(a => a.CreateAccountsAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.Callback(() => order.Add("accounts"))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var bl = new Mock<IBitLockerService>();
|
||||
bl.Setup(b => b.EnableAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.Callback(() => order.Add("bitlocker"))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var boot = new Mock<IBootstrapService>();
|
||||
boot.Setup(b => b.RevertKioskAsync(It.IsAny<CancellationToken>()))
|
||||
.Callback(() => order.Add("revert-kiosk"))
|
||||
.Returns(Task.CompletedTask);
|
||||
boot.Setup(b => b.TearDownAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.Callback(() => order.Add("teardown"))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var sut = new ApplyService(run.Object, acct.Object, bl.Object, boot.Object, "C:\\hard");
|
||||
var flavour = new SilverOS.Welcome.Core.Flavours.FlavourManifest
|
||||
{
|
||||
Id = "daily-driver",
|
||||
Hardening = new SilverOS.Welcome.Core.Flavours.HardeningSpec { Modules = new[] { "00" } }
|
||||
};
|
||||
var req = new ApplyRequest(flavour, "alice", "pw", "adminpw", "123456", "sm-bootstrap");
|
||||
|
||||
await sut.RunAsync(req, new Progress<ApplyProgress>(_ => { }));
|
||||
|
||||
// revert-kiosk must precede teardown so the sm-bootstrap SID still resolves.
|
||||
Assert.Equal(new[] { "modules", "accounts", "bitlocker", "revert-kiosk", "teardown" }, order);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user