fix: keep static VNC host-managed (#13)
* Revert "feat: add managed static macOS VNC login (#12)"
This reverts commit 4333327f56.
* fix: keep static VNC host-managed
This commit is contained in:
parent
4333327f56
commit
04f24e9135
@ -6,7 +6,6 @@
|
||||
|
||||
- Added `--desktop`, `--browser`, and `crabbox vnc` for optional Linux UI/browser leases, including loopback-only VNC with per-lease passwords and headless browser support without a desktop.
|
||||
- Added static macOS/Windows VNC endpoint discovery, including SSH-tunneled loopback VNC and trusted static direct VNC on `host:5900`.
|
||||
- Added `crabbox vnc --managed-login` for static macOS hosts, creating a dedicated Crabbox login and printing reusable VNC credentials.
|
||||
- Added `crabbox vnc --open` to start the SSH tunnel and launch the local VNC client for managed desktop leases.
|
||||
- Added a minimal XFCE desktop profile with panel/window manager for managed VNC leases.
|
||||
- Clarified static macOS/Windows VNC as existing-host access, not Crabbox-created boxes, so `--open` no longer launches an OS credential prompt unless `--host-managed` is passed.
|
||||
@ -14,6 +13,7 @@
|
||||
### Fixed
|
||||
|
||||
- Quoted `crabbox vnc` tunnel key paths so macOS `Application Support` lease keys can be pasted directly into a shell.
|
||||
- Removed the static macOS managed-login path so static host VNC cannot be mistaken for a Crabbox-created external instance.
|
||||
- Fixed native Windows `--shell` runs so multi-statement PowerShell scripts keep their quotes instead of being re-parsed by a nested PowerShell process.
|
||||
- Skipped Linux-only GitHub Actions hydration stop markers on native Windows static targets.
|
||||
|
||||
|
||||
11
docs/cli.md
11
docs/cli.md
@ -232,9 +232,6 @@ Flags:
|
||||
--debug print sync timing and itemized rsync output
|
||||
--junit <paths> comma-separated remote JUnit XML paths to attach to run history
|
||||
--open open local VNC client for `crabbox vnc`
|
||||
--host-managed allow opening host-managed static VNC
|
||||
--managed-login create/reuse a Crabbox-managed static macOS VNC login
|
||||
--managed-user <user> static managed VNC login user
|
||||
--reclaim claim an existing lease for the current repo
|
||||
--timing-json print a final JSON timing record
|
||||
--blacksmith-org <org> Blacksmith organization
|
||||
@ -317,8 +314,6 @@ static:
|
||||
user: steipete
|
||||
port: "22"
|
||||
workRoot: /Users/steipete/crabbox
|
||||
managedLogin: true
|
||||
managedUser: crabbox
|
||||
```
|
||||
|
||||
Static Windows target:
|
||||
@ -339,12 +334,6 @@ static:
|
||||
archive. `windows.mode: wsl2` runs commands through `wsl.exe --exec bash -lc`
|
||||
and uses rsync inside WSL2, so `static.workRoot` should be a WSL path.
|
||||
|
||||
`crabbox vnc --managed-login` is currently macOS-only. It uses SSH plus sudo to
|
||||
create or reuse the configured `static.managedUser`, grants Apple Remote Desktop
|
||||
access, and prints the generated VNC credentials. Static Windows VNC remains
|
||||
host-managed until the target exposes a management channel such as SSH/WinRM and
|
||||
a supported VNC server setup path.
|
||||
|
||||
`crabbox warmup --market spot|on-demand` and `crabbox run --market spot|on-demand`
|
||||
override `capacity.market` for a single AWS lease. Use this for temporary quota
|
||||
or capacity shifts without rewriting repo config.
|
||||
|
||||
@ -8,7 +8,6 @@ crabbox warmup --desktop
|
||||
crabbox vnc --id blue-lobster
|
||||
crabbox vnc --id blue-lobster --open
|
||||
crabbox vnc --provider ssh --target macos --static-host mac-studio.local
|
||||
crabbox vnc --provider ssh --target macos --static-host mac-studio.local --managed-login
|
||||
```
|
||||
|
||||
The command resolves the lease like `crabbox ssh`, claims and touches it like
|
||||
@ -40,25 +39,14 @@ hosts, Crabbox first tries the same SSH tunnel to
|
||||
`127.0.0.1:5900` on the target. If a static host exposes VNC directly on
|
||||
`host:5900`, Crabbox prints that endpoint instead. Direct static VNC is
|
||||
operator-managed and should be limited to a trusted network such as Tailscale or
|
||||
LAN.
|
||||
LAN. Opening a static macOS or Windows target means opening that existing
|
||||
machine, not an external Crabbox instance.
|
||||
|
||||
Static host credentials are host-managed. On macOS, the built-in Screen Sharing
|
||||
server uses the host's Screen Sharing or macOS account authentication. On
|
||||
Windows, the prompt belongs to the installed VNC server. Crabbox does not print
|
||||
or synthesize those passwords.
|
||||
|
||||
For static macOS hosts reachable over SSH, `--managed-login` creates or reuses a
|
||||
dedicated local account, enables Apple Remote Desktop access for that account,
|
||||
skips first-run Setup Assistant panes, and prints the username/password that
|
||||
Crabbox generated. The password is stored on the target under the SSH user's
|
||||
Crabbox state directory and is reused on later calls. Override the account name
|
||||
with `--managed-user <name>`.
|
||||
|
||||
Static Windows managed login is not automatic yet. Crabbox needs a management
|
||||
channel such as SSH/WinRM plus a known VNC server setup path before it can
|
||||
create a Windows account and set the VNC server password. Without that, Windows
|
||||
VNC remains host-managed and requires `--host-managed`.
|
||||
|
||||
`--open` refuses host-managed static VNC by default so a host OS password prompt
|
||||
is not mistaken for a Crabbox-created box. Pass `--host-managed` only when you
|
||||
intentionally want to open that existing host's VNC login prompt.
|
||||
@ -73,18 +61,17 @@ Security boundary:
|
||||
|
||||
Provider behavior:
|
||||
|
||||
- Brokered and direct AWS/Hetzner Linux leases support `vnc` only when created
|
||||
with `--desktop`.
|
||||
- Brokered and direct AWS/Hetzner leases are Linux-only in this release. They
|
||||
support `vnc` only when created with `--desktop`.
|
||||
- Static Linux can participate if the operator already configured Xvfb and
|
||||
loopback-bound x11vnc.
|
||||
- Static macOS can participate when Screen Sharing or another VNC-compatible
|
||||
service is already available on `127.0.0.1:5900` over SSH or directly on
|
||||
`host:5900`. `--managed-login` can create a dedicated local macOS account for
|
||||
Crabbox-managed VNC authentication on that existing Mac.
|
||||
`host:5900`. This reuses an existing Mac; it does not create a macOS Crabbox.
|
||||
Credentials are host-managed.
|
||||
- Static native Windows can participate when a VNC server is already available
|
||||
on `127.0.0.1:5900` over SSH or directly on `host:5900`. Crabbox does not
|
||||
create a Windows Crabbox, or install or configure the Windows VNC server in
|
||||
this release.
|
||||
create a Windows Crabbox, or install or configure the Windows VNC server.
|
||||
- Blacksmith Testbox does not support managed VNC in this release.
|
||||
|
||||
Flags:
|
||||
@ -101,7 +88,5 @@ Flags:
|
||||
--local-port <port>
|
||||
--open
|
||||
--host-managed
|
||||
--managed-login
|
||||
--managed-user <user>
|
||||
--reclaim
|
||||
```
|
||||
|
||||
@ -77,29 +77,25 @@ Security rules:
|
||||
Provider notes:
|
||||
|
||||
- Hetzner and AWS brokered Linux leases are the primary target because Crabbox
|
||||
controls cloud-init and firewall shape there.
|
||||
controls cloud-init and firewall shape there. Brokered macOS and Windows
|
||||
desktop leases do not exist in this release.
|
||||
- Static SSH Linux hosts can participate when the operator accepts responsibility
|
||||
for packages and display services.
|
||||
- Static macOS hosts are existing Macs, not Crabbox-created boxes. They can
|
||||
participate when Screen Sharing or another
|
||||
VNC-compatible service is already available on `127.0.0.1:5900` over SSH or
|
||||
directly on `host:5900`. `crabbox vnc --managed-login` can create a dedicated
|
||||
local macOS account, grant Apple Remote Desktop access to it, and print the
|
||||
generated credentials. Without `--managed-login`, credentials are
|
||||
host-managed because Apple Remote Desktop authentication still belongs to the
|
||||
target host.
|
||||
directly on `host:5900`. Credentials are host-managed because Apple Remote
|
||||
Desktop authentication still belongs to the target host.
|
||||
- Static Windows hosts are existing Windows machines, not Crabbox-created boxes.
|
||||
They can participate only when the operator already provides a VNC-compatible
|
||||
service on `127.0.0.1:5900` for SSH tunneling or, for trusted static networks,
|
||||
directly on `host:5900`. Opening Windows requires `--host-managed` because the
|
||||
password prompt belongs to the target OS, not Crabbox. Managed Windows login
|
||||
still needs a management channel such as SSH/WinRM plus a supported VNC server
|
||||
setup path.
|
||||
password prompt belongs to the target OS, not Crabbox.
|
||||
- Blacksmith Testbox can run headless browser automation today, but VNC takeover
|
||||
needs a Blacksmith-supported SSH tunnel or connection-info API before Crabbox
|
||||
can offer the same `vnc` command there.
|
||||
- Crabbox-managed Windows VNC installers are still out of scope for this
|
||||
release.
|
||||
- Crabbox-managed macOS and Windows VNC installers are still out of scope for
|
||||
this release.
|
||||
|
||||
For Mantis, the first consumer should be a Discord QA lane:
|
||||
|
||||
|
||||
@ -40,8 +40,7 @@ This page maps user-facing behavior back to implementation files. Keep docs desc
|
||||
- CLI cloud-init bootstrap: `internal/cli/bootstrap.go`
|
||||
- Worker cloud-init bootstrap: `worker/src/bootstrap.ts`
|
||||
- Desktop/browser capability flags, env injection, and VNC checks: `internal/cli/capabilities.go`, `internal/cli/run.go`
|
||||
- VNC tunnel command and static managed macOS login: `internal/cli/vnc.go`,
|
||||
`internal/cli/static_vnc.go`
|
||||
- VNC tunnel command: `internal/cli/vnc.go`
|
||||
- Interactive desktop/VNC contract: `docs/features/interactive-desktop-vnc.md`
|
||||
|
||||
Bootstrap is intentionally tiny unless optional lease capabilities are requested:
|
||||
|
||||
@ -198,8 +198,6 @@ Environment:
|
||||
CRABBOX_DESKTOP Provision or require desktop/VNC capability
|
||||
CRABBOX_BROWSER Provision or require browser capability
|
||||
CRABBOX_STATIC_HOST Static SSH host for provider=ssh
|
||||
CRABBOX_STATIC_MANAGED_LOGIN Enable static managed VNC login where supported
|
||||
CRABBOX_STATIC_MANAGED_USER Static managed VNC login user
|
||||
CRABBOX_OWNER Usage owner override
|
||||
CRABBOX_ORG Usage org override
|
||||
CRABBOX_CONFIG Optional config path
|
||||
|
||||
@ -198,10 +198,10 @@ func availableLocalVNCPort() string {
|
||||
func resolveVNCEndpoint(ctx context.Context, cfg Config, target SSHTarget) (vncEndpoint, error) {
|
||||
if isStaticProvider(cfg.Provider) {
|
||||
if err := probeLoopbackVNC(ctx, target, "2", "1"); err == nil {
|
||||
return vncEndpoint{Host: "127.0.0.1", Port: managedVNCPort, Managed: cfg.Static.ManagedLogin}, nil
|
||||
return vncEndpoint{Host: "127.0.0.1", Port: managedVNCPort}, nil
|
||||
}
|
||||
if tcpReachable(ctx, target.Host, managedVNCPort, 2*time.Second) {
|
||||
return vncEndpoint{Direct: true, Host: target.Host, Port: managedVNCPort, Managed: cfg.Static.ManagedLogin}, nil
|
||||
return vncEndpoint{Direct: true, Host: target.Host, Port: managedVNCPort}, nil
|
||||
}
|
||||
return vncEndpoint{}, exit(5, "target does not expose VNC through SSH loopback 127.0.0.1:5900 or direct %s:%s", target.Host, managedVNCPort)
|
||||
}
|
||||
|
||||
@ -97,14 +97,12 @@ type BlacksmithConfig struct {
|
||||
}
|
||||
|
||||
type StaticConfig struct {
|
||||
ID string
|
||||
Name string
|
||||
Host string
|
||||
User string
|
||||
Port string
|
||||
WorkRoot string
|
||||
ManagedLogin bool
|
||||
ManagedUser string
|
||||
ID string
|
||||
Name string
|
||||
Host string
|
||||
User string
|
||||
Port string
|
||||
WorkRoot string
|
||||
}
|
||||
|
||||
type ResultsConfig struct {
|
||||
@ -331,14 +329,12 @@ type fileBlacksmithConfig struct {
|
||||
}
|
||||
|
||||
type fileStaticConfig struct {
|
||||
ID string `yaml:"id,omitempty"`
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Host string `yaml:"host,omitempty"`
|
||||
User string `yaml:"user,omitempty"`
|
||||
Port string `yaml:"port,omitempty"`
|
||||
WorkRoot string `yaml:"workRoot,omitempty"`
|
||||
ManagedLogin *bool `yaml:"managedLogin,omitempty"`
|
||||
ManagedUser string `yaml:"managedUser,omitempty"`
|
||||
ID string `yaml:"id,omitempty"`
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Host string `yaml:"host,omitempty"`
|
||||
User string `yaml:"user,omitempty"`
|
||||
Port string `yaml:"port,omitempty"`
|
||||
WorkRoot string `yaml:"workRoot,omitempty"`
|
||||
}
|
||||
|
||||
type fileResultsConfig struct {
|
||||
@ -693,12 +689,6 @@ func applyFileConfig(cfg *Config, file fileConfig) {
|
||||
if file.Static.WorkRoot != "" {
|
||||
cfg.Static.WorkRoot = file.Static.WorkRoot
|
||||
}
|
||||
if file.Static.ManagedLogin != nil {
|
||||
cfg.Static.ManagedLogin = *file.Static.ManagedLogin
|
||||
}
|
||||
if file.Static.ManagedUser != "" {
|
||||
cfg.Static.ManagedUser = file.Static.ManagedUser
|
||||
}
|
||||
}
|
||||
if file.Results != nil && len(file.Results.JUnit) > 0 {
|
||||
cfg.Results.JUnit = appendUniqueStrings(nil, file.Results.JUnit...)
|
||||
@ -799,10 +789,6 @@ func applyEnv(cfg *Config) {
|
||||
cfg.Static.User = getenv("CRABBOX_STATIC_USER", cfg.Static.User)
|
||||
cfg.Static.Port = getenv("CRABBOX_STATIC_PORT", cfg.Static.Port)
|
||||
cfg.Static.WorkRoot = getenv("CRABBOX_STATIC_WORK_ROOT", cfg.Static.WorkRoot)
|
||||
if value, ok := getenvBool("CRABBOX_STATIC_MANAGED_LOGIN"); ok {
|
||||
cfg.Static.ManagedLogin = value
|
||||
}
|
||||
cfg.Static.ManagedUser = getenv("CRABBOX_STATIC_MANAGED_USER", cfg.Static.ManagedUser)
|
||||
if idleTimeout := os.Getenv("CRABBOX_BLACKSMITH_IDLE_TIMEOUT"); idleTimeout != "" {
|
||||
applyLeaseDuration(&cfg.Blacksmith.IdleTimeout, idleTimeout)
|
||||
}
|
||||
|
||||
@ -99,14 +99,12 @@ func (a App) configShow(args []string) error {
|
||||
"debug": cfg.Blacksmith.Debug,
|
||||
},
|
||||
"static": map[string]any{
|
||||
"id": cfg.Static.ID,
|
||||
"name": cfg.Static.Name,
|
||||
"host": cfg.Static.Host,
|
||||
"user": cfg.Static.User,
|
||||
"port": cfg.Static.Port,
|
||||
"workRoot": cfg.Static.WorkRoot,
|
||||
"managedLogin": cfg.Static.ManagedLogin,
|
||||
"managedUser": cfg.Static.ManagedUser,
|
||||
"id": cfg.Static.ID,
|
||||
"name": cfg.Static.Name,
|
||||
"host": cfg.Static.Host,
|
||||
"user": cfg.Static.User,
|
||||
"port": cfg.Static.Port,
|
||||
"workRoot": cfg.Static.WorkRoot,
|
||||
},
|
||||
"results": map[string]any{
|
||||
"junit": cfg.Results.JUnit,
|
||||
@ -147,7 +145,7 @@ func (a App) configShow(args []string) error {
|
||||
fmt.Fprintf(a.Stdout, "capacity market=%s strategy=%s fallback=%s regions=%s\n", cfg.Capacity.Market, cfg.Capacity.Strategy, cfg.Capacity.Fallback, blank(strings.Join(cfg.Capacity.Regions, ","), "-"))
|
||||
fmt.Fprintf(a.Stdout, "actions repo=%s workflow=%s job=%s ref=%s runner_version=%s ephemeral=%t labels=%s\n", blank(cfg.Actions.Repo, "-"), blank(cfg.Actions.Workflow, "-"), blank(cfg.Actions.Job, "-"), blank(cfg.Actions.Ref, "-"), cfg.Actions.RunnerVersion, cfg.Actions.Ephemeral, blank(strings.Join(cfg.Actions.RunnerLabels, ","), "-"))
|
||||
fmt.Fprintf(a.Stdout, "blacksmith org=%s workflow=%s job=%s ref=%s idle_timeout=%s debug=%t\n", blank(cfg.Blacksmith.Org, "-"), blank(cfg.Blacksmith.Workflow, "-"), blank(cfg.Blacksmith.Job, "-"), blank(cfg.Blacksmith.Ref, "-"), cfg.Blacksmith.IdleTimeout, cfg.Blacksmith.Debug)
|
||||
fmt.Fprintf(a.Stdout, "static id=%s name=%s host=%s user=%s port=%s work_root=%s managed_login=%t managed_user=%s\n", blank(cfg.Static.ID, "-"), blank(cfg.Static.Name, "-"), blank(cfg.Static.Host, "-"), blank(cfg.Static.User, "-"), blank(cfg.Static.Port, "-"), blank(cfg.Static.WorkRoot, "-"), cfg.Static.ManagedLogin, blank(cfg.Static.ManagedUser, "-"))
|
||||
fmt.Fprintf(a.Stdout, "static id=%s name=%s host=%s user=%s port=%s work_root=%s\n", blank(cfg.Static.ID, "-"), blank(cfg.Static.Name, "-"), blank(cfg.Static.Host, "-"), blank(cfg.Static.User, "-"), blank(cfg.Static.Port, "-"), blank(cfg.Static.WorkRoot, "-"))
|
||||
fmt.Fprintf(a.Stdout, "results junit=%s\n", blank(strings.Join(cfg.Results.JUnit, ","), "-"))
|
||||
fmt.Fprintf(a.Stdout, "cache pnpm=%t npm=%t docker=%t git=%t max_gb=%d purge_on_release=%t\n", cfg.Cache.Pnpm, cfg.Cache.Npm, cfg.Cache.Docker, cfg.Cache.Git, cfg.Cache.MaxGB, cfg.Cache.PurgeOnRelease)
|
||||
fmt.Fprintf(a.Stdout, "aws region=%s root_gb=%d ssh_cidrs=%s\n", cfg.AWSRegion, cfg.AWSRootGB, blank(strings.Join(cfg.AWSSSHCIDRs, ","), "-"))
|
||||
|
||||
@ -109,8 +109,6 @@ static:
|
||||
user: peter
|
||||
port: "22"
|
||||
workRoot: /home/peter/crabbox
|
||||
managedLogin: true
|
||||
managedUser: cbx-ui
|
||||
results:
|
||||
junit:
|
||||
- junit.xml
|
||||
@ -191,7 +189,7 @@ ssh:
|
||||
if cfg.Blacksmith.Org != "openclaw" || cfg.Blacksmith.Workflow != ".github/workflows/blacksmith-testbox.yml" || cfg.Blacksmith.Job != "hydrate" || cfg.Blacksmith.Ref != "main" || cfg.Blacksmith.IdleTimeout != 90*time.Minute || !cfg.Blacksmith.Debug {
|
||||
t.Fatalf("blacksmith config not loaded: %#v", cfg.Blacksmith)
|
||||
}
|
||||
if cfg.Static.Host != "win-dev.local" || cfg.Static.User != "peter" || cfg.Static.Port != "22" || cfg.WorkRoot != "/home/peter/crabbox" || !cfg.Static.ManagedLogin || cfg.Static.ManagedUser != "cbx-ui" {
|
||||
if cfg.Static.Host != "win-dev.local" || cfg.Static.User != "peter" || cfg.Static.Port != "22" || cfg.WorkRoot != "/home/peter/crabbox" {
|
||||
t.Fatalf("static config not loaded: static=%#v workRoot=%s", cfg.Static, cfg.WorkRoot)
|
||||
}
|
||||
if len(cfg.Results.JUnit) != 1 || cfg.Results.JUnit[0] != "junit.xml" {
|
||||
@ -220,8 +218,6 @@ func TestEnvOverridesConfig(t *testing.T) {
|
||||
t.Setenv("CRABBOX_COORDINATOR_ADMIN_TOKEN", "env-admin-secret")
|
||||
t.Setenv("CRABBOX_TARGET", "macos")
|
||||
t.Setenv("CRABBOX_STATIC_HOST", "mac.local")
|
||||
t.Setenv("CRABBOX_STATIC_MANAGED_LOGIN", "true")
|
||||
t.Setenv("CRABBOX_STATIC_MANAGED_USER", "cbx-env")
|
||||
path := userConfigPath()
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
@ -249,7 +245,7 @@ func TestEnvOverridesConfig(t *testing.T) {
|
||||
if cfg.CoordAdminToken != "env-admin-secret" {
|
||||
t.Fatalf("unexpected admin token state: %q", cfg.CoordAdminToken)
|
||||
}
|
||||
if cfg.TargetOS != targetMacOS || cfg.Static.Host != "mac.local" || !cfg.Static.ManagedLogin || cfg.Static.ManagedUser != "cbx-env" {
|
||||
if cfg.TargetOS != targetMacOS || cfg.Static.Host != "mac.local" {
|
||||
t.Fatalf("unexpected target env: target=%s static=%#v", cfg.TargetOS, cfg.Static)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,99 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const staticManagedVNCPasswordName = "macos-vnc.password"
|
||||
|
||||
type staticManagedVNCLogin struct {
|
||||
User string
|
||||
Password string
|
||||
PasswordPath string
|
||||
}
|
||||
|
||||
func ensureStaticManagedVNCLogin(ctx context.Context, cfg Config, target SSHTarget) (staticManagedVNCLogin, error) {
|
||||
if !isStaticProvider(cfg.Provider) || !cfg.Static.ManagedLogin {
|
||||
return staticManagedVNCLogin{}, nil
|
||||
}
|
||||
user := strings.TrimSpace(firstNonEmpty(cfg.Static.ManagedUser, "crabbox"))
|
||||
if user == "" {
|
||||
return staticManagedVNCLogin{}, exit(2, "static managed VNC login requires --managed-user")
|
||||
}
|
||||
switch target.TargetOS {
|
||||
case targetMacOS:
|
||||
return ensureStaticMacOSManagedVNCLogin(ctx, cfg, target, user)
|
||||
case targetWindows:
|
||||
return staticManagedVNCLogin{}, exit(2, "static managed VNC login for target=windows requires SSH/WinRM plus a supported VNC server setup path; this target only exposes an existing host-managed VNC service")
|
||||
default:
|
||||
return staticManagedVNCLogin{}, exit(2, "static managed VNC login is supported for target=macos only in this release")
|
||||
}
|
||||
}
|
||||
|
||||
func ensureStaticMacOSManagedVNCLogin(ctx context.Context, cfg Config, target SSHTarget, user string) (staticManagedVNCLogin, error) {
|
||||
root := strings.TrimSpace(cfg.Static.WorkRoot)
|
||||
out, err := runSSHOutput(ctx, target, staticMacOSManagedVNCLoginScript(user, root))
|
||||
if err != nil {
|
||||
return staticManagedVNCLogin{}, exit(5, "configure static macOS managed VNC login over SSH: %v", err)
|
||||
}
|
||||
values := parseEnvLines(out)
|
||||
if values["USER"] == "" || values["PASSWORD"] == "" || values["PASSWORD_FILE"] == "" {
|
||||
return staticManagedVNCLogin{}, exit(5, "static macOS managed VNC login did not return credentials")
|
||||
}
|
||||
return staticManagedVNCLogin{
|
||||
User: values["USER"],
|
||||
Password: values["PASSWORD"],
|
||||
PasswordPath: values["PASSWORD_FILE"],
|
||||
}, nil
|
||||
}
|
||||
|
||||
func staticMacOSManagedVNCLoginScript(user, root string) string {
|
||||
rootArg := ``
|
||||
if strings.TrimSpace(root) != "" {
|
||||
rootArg = shellQuote(root)
|
||||
}
|
||||
return fmt.Sprintf(`set -eu
|
||||
user=%s
|
||||
root=%s
|
||||
if [ -z "$root" ]; then
|
||||
root="$HOME/crabbox"
|
||||
fi
|
||||
secret_dir="$root/.crabbox"
|
||||
secret_file="$secret_dir/%s"
|
||||
sudo -n mkdir -p "$secret_dir"
|
||||
sudo -n chmod 700 "$secret_dir"
|
||||
if [ ! -s "$secret_file" ]; then
|
||||
pw="$(LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c 18)"
|
||||
printf '%%s\n' "$pw" | sudo -n tee "$secret_file" >/dev/null
|
||||
sudo -n chmod 600 "$secret_file"
|
||||
fi
|
||||
pw="$(sudo -n cat "$secret_file")"
|
||||
if ! id "$user" >/dev/null 2>&1; then
|
||||
sudo -n sysadminctl -addUser "$user" -fullName "Crabbox" -password "$pw" -home "/Users/$user" -shell /bin/zsh >/dev/null
|
||||
sudo -n createhomedir -c -u "$user" >/dev/null 2>&1 || true
|
||||
else
|
||||
sudo -n dscl . -passwd "/Users/$user" "$pw"
|
||||
sudo -n pwpolicy -u "$user" -clearaccountpolicies >/dev/null 2>&1 || true
|
||||
fi
|
||||
home="$(dscl . -read "/Users/$user" NFSHomeDirectory | sed 's/NFSHomeDirectory: //')"
|
||||
uid="$(id -u "$user")"
|
||||
sudo -n mkdir -p "$home/Library/Preferences"
|
||||
sudo -n chown -R "$user":staff "$home/Library" >/dev/null 2>&1 || true
|
||||
sudo -n /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart -activate -configure -allowAccessFor -specifiedUsers >/dev/null
|
||||
sudo -n /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart -configure -access -on -privs -all -users "$user" >/dev/null
|
||||
sudo -n /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart -configure -clientopts -setreqperm -reqperm no >/dev/null
|
||||
sudo -n launchctl asuser "$uid" sudo -u "$user" defaults write com.apple.SetupAssistant DidSeeAccessibility -bool true >/dev/null 2>&1 || true
|
||||
sudo -n launchctl asuser "$uid" sudo -u "$user" defaults write com.apple.SetupAssistant DidSeeSiriSetup -bool true >/dev/null 2>&1 || true
|
||||
sudo -n launchctl asuser "$uid" sudo -u "$user" defaults write com.apple.SetupAssistant DidSeePrivacy -bool true >/dev/null 2>&1 || true
|
||||
sudo -n launchctl asuser "$uid" sudo -u "$user" defaults write com.apple.SetupAssistant DidSeeCloudSetup -bool true >/dev/null 2>&1 || true
|
||||
sudo -n launchctl asuser "$uid" sudo -u "$user" defaults write com.apple.SetupAssistant LastSeenCloudProductVersion "$(sw_vers -productVersion)" >/dev/null 2>&1 || true
|
||||
sudo -n pkill -u "$user" -x "Setup Assistant" >/dev/null 2>&1 || true
|
||||
sudo -n pkill -u "$user" -x SetupAssistant >/dev/null 2>&1 || true
|
||||
sudo -n /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart -restart -agent >/dev/null
|
||||
printf 'USER=%%s\n' "$user"
|
||||
printf 'PASSWORD_FILE=%%s\n' "$secret_file"
|
||||
printf 'PASSWORD=%%s\n' "$pw"
|
||||
`, shellQuote(user), rootArg, staticManagedVNCPasswordName)
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStaticMacOSManagedVNCLoginScript(t *testing.T) {
|
||||
got := staticMacOSManagedVNCLoginScript("cbx user", "/Users/admin/crabbox root")
|
||||
for _, want := range []string{
|
||||
"user='cbx user'",
|
||||
"root='/Users/admin/crabbox root'",
|
||||
"sysadminctl -addUser",
|
||||
"dscl . -passwd",
|
||||
"kickstart -configure -access -on -privs -all -users \"$user\"",
|
||||
"DidSeeAccessibility",
|
||||
"PASSWORD_FILE=%s",
|
||||
"PASSWORD=%s",
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("script missing %q:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureStaticManagedVNCLoginRejectsWindows(t *testing.T) {
|
||||
cfg := baseConfig()
|
||||
cfg.Provider = staticProvider
|
||||
cfg.Static.ManagedLogin = true
|
||||
target := SSHTarget{TargetOS: targetWindows, WindowsMode: windowsModeNormal}
|
||||
_, err := ensureStaticManagedVNCLogin(context.Background(), cfg, target)
|
||||
if err == nil || !strings.Contains(err.Error(), "requires SSH/WinRM") {
|
||||
t.Fatalf("err=%v", err)
|
||||
}
|
||||
}
|
||||
29
internal/cli/target_test.go
Normal file
29
internal/cli/target_test.go
Normal file
@ -0,0 +1,29 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateProviderTargetRejectsBrokeredNonLinux(t *testing.T) {
|
||||
for _, target := range []string{targetMacOS, targetWindows} {
|
||||
cfg := baseConfig()
|
||||
cfg.Provider = "aws"
|
||||
cfg.TargetOS = target
|
||||
err := validateProviderTarget(cfg)
|
||||
if err == nil || !strings.Contains(err.Error(), "currently supports target=linux only") {
|
||||
t.Fatalf("target=%s err=%v", target, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateProviderTargetAllowsStaticNonLinux(t *testing.T) {
|
||||
for _, target := range []string{targetMacOS, targetWindows} {
|
||||
cfg := baseConfig()
|
||||
cfg.Provider = staticProvider
|
||||
cfg.TargetOS = target
|
||||
if err := validateProviderTarget(cfg); err != nil {
|
||||
t.Fatalf("target=%s err=%v", target, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -18,8 +18,6 @@ func (a App) vnc(ctx context.Context, args []string) error {
|
||||
localPort := fs.String("local-port", "", "local VNC tunnel port")
|
||||
openClient := fs.Bool("open", false, "open the VNC client locally")
|
||||
hostManaged := fs.Bool("host-managed", false, "allow opening host-managed static VNC")
|
||||
managedLogin := fs.Bool("managed-login", defaults.Static.ManagedLogin, "create or reuse a Crabbox-managed static VNC login when supported")
|
||||
managedUser := fs.String("managed-user", firstNonEmpty(defaults.Static.ManagedUser, "crabbox"), "static managed VNC login user")
|
||||
targetFlags := registerTargetFlags(fs, defaults)
|
||||
if err := parseFlags(fs, args); err != nil {
|
||||
return err
|
||||
@ -33,8 +31,6 @@ func (a App) vnc(ctx context.Context, args []string) error {
|
||||
}
|
||||
cfg.Provider = *provider
|
||||
cfg.Desktop = true
|
||||
cfg.Static.ManagedLogin = *managedLogin
|
||||
cfg.Static.ManagedUser = *managedUser
|
||||
if err := applyTargetFlagOverrides(&cfg, fs, targetFlags); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -44,7 +40,7 @@ func (a App) vnc(ctx context.Context, args []string) error {
|
||||
if *id == "" && !isStaticProvider(cfg.Provider) {
|
||||
return exit(2, "usage: crabbox vnc --id <lease-id-or-slug>")
|
||||
}
|
||||
if *openClient && isStaticProvider(cfg.Provider) && !*hostManaged && !cfg.Static.ManagedLogin {
|
||||
if *openClient && isStaticProvider(cfg.Provider) && !*hostManaged {
|
||||
return exit(2, "static %s VNC is an existing host, not a Crabbox-created box; rerun with --host-managed only if you want to open that host's OS login prompt", cfg.TargetOS)
|
||||
}
|
||||
server, target, leaseID, err := a.resolveLeaseTarget(ctx, cfg, *id)
|
||||
@ -62,10 +58,6 @@ func (a App) vnc(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
a.touchActiveLeaseBestEffort(ctx, cfg, server, leaseID)
|
||||
login, err := ensureStaticManagedVNCLogin(ctx, cfg, target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
endpoint, err := resolveVNCEndpoint(ctx, cfg, target)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -75,9 +67,7 @@ func (a App) vnc(ctx context.Context, args []string) error {
|
||||
}
|
||||
password := ""
|
||||
if endpoint.Managed {
|
||||
if login.User != "" {
|
||||
password = login.Password
|
||||
} else if target.TargetOS == targetLinux {
|
||||
if target.TargetOS == targetLinux {
|
||||
password, _ = runSSHOutput(ctx, target, "cat "+shellQuote(vncPasswordPath))
|
||||
}
|
||||
}
|
||||
@ -103,11 +93,7 @@ func (a App) vnc(ctx context.Context, args []string) error {
|
||||
if endpoint.Direct {
|
||||
fmt.Fprintln(a.Stdout, "direct vnc:")
|
||||
fmt.Fprintf(a.Stdout, " %s:%s\n", endpoint.Host, endpoint.Port)
|
||||
directURL := fmt.Sprintf("vnc://%s:%s", endpoint.Host, endpoint.Port)
|
||||
if login.User != "" {
|
||||
directURL = fmt.Sprintf("vnc://%s@%s:%s", login.User, endpoint.Host, endpoint.Port)
|
||||
}
|
||||
fmt.Fprintf(a.Stdout, " %s\n", directURL)
|
||||
fmt.Fprintf(a.Stdout, " vnc://%s:%s\n", endpoint.Host, endpoint.Port)
|
||||
} else {
|
||||
fmt.Fprintln(a.Stdout, "ssh tunnel:")
|
||||
fmt.Fprintf(a.Stdout, " %s\n", tunnel)
|
||||
@ -119,12 +105,6 @@ func (a App) vnc(ctx context.Context, args []string) error {
|
||||
fmt.Fprintf(a.Stdout, " localhost:%s\n", *localPort)
|
||||
}
|
||||
if strings.TrimSpace(password) != "" {
|
||||
if login.User != "" {
|
||||
fmt.Fprintf(a.Stdout, "username: %s\n", login.User)
|
||||
if login.PasswordPath != "" {
|
||||
fmt.Fprintf(a.Stdout, "password_file: %s\n", login.PasswordPath)
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(a.Stdout, "password: %s\n", strings.TrimSpace(password))
|
||||
} else if staticHostVNC {
|
||||
fmt.Fprintln(a.Stdout, "credentials: host-managed")
|
||||
@ -140,9 +120,6 @@ func (a App) vnc(ctx context.Context, args []string) error {
|
||||
fmt.Fprintln(a.Stdout, "opening existing host VNC; expect that host's OS credential prompt")
|
||||
}
|
||||
url := fmt.Sprintf("vnc://%s:%s", endpoint.Host, endpoint.Port)
|
||||
if login.User != "" {
|
||||
url = fmt.Sprintf("vnc://%s@%s:%s", login.User, endpoint.Host, endpoint.Port)
|
||||
}
|
||||
if !endpoint.Direct {
|
||||
pid, err := startVNCTunnel(ctx, target, *localPort, endpoint.Host, endpoint.Port)
|
||||
if err != nil {
|
||||
@ -154,9 +131,6 @@ func (a App) vnc(ctx context.Context, args []string) error {
|
||||
fmt.Fprintln(a.Stdout, "tunnel: started in background")
|
||||
}
|
||||
url = fmt.Sprintf("vnc://localhost:%s", *localPort)
|
||||
if login.User != "" {
|
||||
url = fmt.Sprintf("vnc://%s@localhost:%s", login.User, *localPort)
|
||||
}
|
||||
}
|
||||
if err := openLocalURL(url); err != nil {
|
||||
return err
|
||||
|
||||
Loading…
Reference in New Issue
Block a user