Skip to content

Commit 44d98a1

Browse files
itcmsgrclaude
andcommitted
installer: scope DirectAdmin adapter to control-plane only (Path A)
Clarifies PR26.3 scope after auditor disposition: - Top-level doc-comment: "control-plane only"; full DirectAdmin service-port surface (16+ public-facing ports) is NOT validated here — that lands in PR26.4 via internal/ports/panel_loader.LoadPanelConfig("directadmin") and /etc/nftban/conf.d/panels/directadmin/main.conf. - PR26.4 follow-up note inline at the top of directadmin.go. - ValidateReachability error message: explicitly says "control-plane port N not in LISTEN state — control-plane unreachable" so the user-facing AssertionResult.Detail (and log lines) cannot be misread as a full panel-survival claim. - New test: ErrorMentionsControlPlane (positive: "control-plane" is in the message; negative: no affirmative full-surface claim). - New test: Reason_DoesNotImplyFullPortSurvival at the framework integration layer. - Tightened existing TestFrameworkIntegration_DA_Detected_NotReachable_Blocks to assert the surfaced Reason mentions "control-plane". No internal/ports import in this PR (deliberately deferred to PR26.4). No new mutation surface; adapter remains read-only. No cPanel/Plesk/classifier/restore code added. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0b575a8 commit 44d98a1

2 files changed

Lines changed: 111 additions & 5 deletions

File tree

internal/installer/panelfw/adapters/directadmin/directadmin.go

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,33 @@
2020
// First and only adapter shipped in this PR. cPanel/Plesk/etc. live in
2121
// future PRs under the same framework.
2222
//
23+
// SCOPE — CONTROL PLANE ONLY (PR26.3)
24+
// -----------------------------------
25+
// This adapter validates DirectAdmin **control-plane reachability**
26+
// only. It detects DirectAdmin and validates the DirectAdmin control
27+
// port (TCP 2222 by default, per-config override via directadmin.conf
28+
// `port=N`).
29+
//
30+
// It does NOT yet validate the full DirectAdmin service-port surface
31+
// (the 16+ public-facing ports DirectAdmin manages on behalf of
32+
// hosted accounts: SMTP/SMTPS, IMAP/IMAPS, POP3/POP3S, FTP/FTPS, SSH,
33+
// HTTP/HTTPS, DNS, etc.). Those are tracked separately by the
34+
// canonical /etc/nftban/conf.d/panels/directadmin/main.conf and are
35+
// loaded by internal/ports/panel_loader.LoadPanelConfig — neither of
36+
// which this adapter consumes.
37+
//
38+
// PR26.4 follow-up:
39+
// Full port-surface validation MUST reuse
40+
// internal/ports/panel_loader.LoadPanelConfig("directadmin") and the
41+
// canonical conf.d panel config. PR26.3 deliberately does not import
42+
// internal/ports — that import lands in PR26.4 so the control-plane
43+
// assertion ships narrowly first and full-surface validation is a
44+
// separate, separately-reviewed change.
45+
//
2346
// Read-only by interface contract:
2447
// - Detect: filesystem stat + service-active query + ss listener parse
2548
// - RequiredPorts: config-file read (best-effort); falls back to default 2222
26-
// - ValidateReachability: ss -lnt output parse for the required port
49+
// - ValidateReachability: ss -lnt output parse for the control port
2750
//
2851
// No mutation surface: no nft, no service writes, no file writes, no
2952
// shell out beyond the read-only `systemctl is-active` and `ss -lnt`
@@ -148,15 +171,22 @@ func (a *adapter) RequiredPorts(ctx context.Context, exec executor.Executor) ([]
148171

149172
// ValidateReachability implements panelfw.PanelAdapter. Read-only.
150173
//
151-
// Confirms the required TCP port is in LISTEN state. Returns nil on
152-
// success; a structured error otherwise. Does NOT mutate ports,
153-
// services, or rules.
174+
// Confirms the DirectAdmin **control-plane** TCP port is in LISTEN
175+
// state. Returns nil on success; a structured error otherwise. Does
176+
// NOT mutate ports, services, or rules.
177+
//
178+
// Scope is the control plane (default 2222) only. The full DirectAdmin
179+
// service-port surface (mail/web/SSH/etc.) is NOT validated here —
180+
// see the file-level "PR26.4 follow-up" comment.
154181
func (a *adapter) ValidateReachability(ctx context.Context, exec executor.Executor) error {
155182
port := readConfiguredPort(exec)
156183
if portInListenState(exec, port) {
157184
return nil
158185
}
159-
return fmt.Errorf("DirectAdmin TCP port %d not in LISTEN state — panel control surface unreachable", port)
186+
return fmt.Errorf(
187+
"DirectAdmin control-plane port %d not in LISTEN state — control-plane unreachable "+
188+
"(note: this assertion validates the control plane only; full DirectAdmin port surface validated in PR26.4)",
189+
port)
160190
}
161191

162192
// readConfiguredPort returns the configured control port from

internal/installer/panelfw/adapters/directadmin/directadmin_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,42 @@ func TestValidateReachability_NotListening_ReturnsError(t *testing.T) {
245245
}
246246
}
247247

248+
// PR26.3 Path A: the user-facing error must explicitly identify the
249+
// scope as control-plane, not "panel survival" or "full panel". This
250+
// keeps operators from mistakenly believing the assertion has
251+
// validated the full DirectAdmin port surface.
252+
func TestValidateReachability_NotListening_ErrorMentionsControlPlane(t *testing.T) {
253+
a := New()
254+
mock := executor.NewMockExecutor()
255+
mock.RunResults["ss:-lnt"] = executor.Result{ExitCode: 0, Stdout: ssOutput(80)}
256+
257+
err := a.ValidateReachability(context.Background(), mock)
258+
if err == nil {
259+
t.Fatalf("expected error when control port not listening")
260+
}
261+
msg := err.Error()
262+
if !strings.Contains(msg, "control-plane") {
263+
t.Errorf("error must explicitly say 'control-plane'; got %q", msg)
264+
}
265+
// Negative: the message must NOT make affirmative claims of full
266+
// survival or full surface validation. We allow the explanatory
267+
// negation form ("full DirectAdmin port surface validated in
268+
// PR26.4") because it reads as scope clarification, not a claim
269+
// the assertion has validated those ports.
270+
for _, forbidden := range []string{
271+
"full panel survival validated",
272+
"full panel survived",
273+
"all DirectAdmin ports validated",
274+
"all DirectAdmin ports listening",
275+
"all panel ports listening",
276+
"all panel ports validated",
277+
} {
278+
if strings.Contains(msg, forbidden) {
279+
t.Errorf("error must NOT claim %q (overstates scope); got %q", forbidden, msg)
280+
}
281+
}
282+
}
283+
248284
// Defensive: ":22222" must not match expected ":2222".
249285
func TestValidateReachability_PortPrefixCollision(t *testing.T) {
250286
a := New()
@@ -332,6 +368,46 @@ func TestFrameworkIntegration_DA_Detected_NotReachable_Blocks(t *testing.T) {
332368
if !strings.Contains(res.Reason, "directadmin") {
333369
t.Errorf("Reason should mention directadmin: %q", res.Reason)
334370
}
371+
// PR26.3 Path A: the surfaced Reason must say control-plane.
372+
if !strings.Contains(res.Reason, "control-plane") {
373+
t.Errorf("Reason must say 'control-plane'; got %q", res.Reason)
374+
}
375+
}
376+
377+
// PR26.3 Path A: the surfaced Reason on a failing DA host must NOT
378+
// claim that the full panel survival was checked — the assertion
379+
// covers only the control plane in PR26.3. Full port-surface
380+
// validation lands in PR26.4.
381+
func TestFrameworkIntegration_DA_Reason_DoesNotImplyFullPortSurvival(t *testing.T) {
382+
a := New()
383+
mock := executor.NewMockExecutor()
384+
mock.Dirs[installDir] = true
385+
mock.Files[binaryPath] = []byte("ELF")
386+
mock.Services[systemdUnit] = true
387+
mock.RunResults["ss:-lnt"] = executor.Result{ExitCode: 0, Stdout: ssOutput(80)}
388+
389+
res := panelfw.EvaluateAdapters(context.Background(), mock, newTestLogger(),
390+
[]panelfw.PanelAdapter{a}, panelfw.DefaultPolicy())
391+
392+
if !res.Fatal {
393+
t.Fatalf("expected Fatal=true; got %#v", res)
394+
}
395+
// These phrases would imply the assertion validated more than the
396+
// control plane. The error MAY mention "full ... port surface" in
397+
// a NEGATION (e.g., "...full DirectAdmin port surface validated in
398+
// PR26.4"), so we look for affirmative-claim verbs instead.
399+
for _, forbidden := range []string{
400+
"full panel survival validated",
401+
"full panel survived",
402+
"all DirectAdmin ports validated",
403+
"all DirectAdmin ports listening",
404+
"all panel ports listening",
405+
"all panel ports validated",
406+
} {
407+
if strings.Contains(res.Reason, forbidden) {
408+
t.Errorf("Reason must NOT claim %q (overstates PR26.3 scope); got %q", forbidden, res.Reason)
409+
}
410+
}
335411
}
336412

337413
func TestFrameworkIntegration_DA_Absent_Passes(t *testing.T) {

0 commit comments

Comments
 (0)