fix: harden APT overlay precedence
This commit is contained in:
+94
-13
@@ -16,14 +16,15 @@ import (
|
||||
|
||||
func TestAvailabilityRequiresDebianMarkerAPTToolAndConfigurationDirectory(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
marker, tool, dir bool
|
||||
want bool
|
||||
name string
|
||||
marker, tool, dir, dumpWorks bool
|
||||
want bool
|
||||
}{
|
||||
{"detected", true, true, true, true},
|
||||
{"not Debian", false, true, true, false},
|
||||
{"apt-config absent", true, false, true, false},
|
||||
{"configuration absent", true, true, false, false},
|
||||
{"detected", true, true, true, true, true},
|
||||
{"not Debian", false, true, true, true, false},
|
||||
{"apt-config absent", true, false, true, true, false},
|
||||
{"configuration absent", true, true, false, true, false},
|
||||
{"apt-config inspection fails", true, true, true, false, false},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
@@ -35,7 +36,7 @@ func TestAvailabilityRequiresDebianMarkerAPTToolAndConfigurationDirectory(t *tes
|
||||
mustMkdir(t, filepath.Dir(filepath.Join(root, "etc", "debian_version")))
|
||||
mustWrite(t, filepath.Join(root, "etc", "debian_version"), "12\n")
|
||||
}
|
||||
runner := &fakeRunner{available: tc.tool}
|
||||
runner := &fakeRunner{available: tc.tool, outputError: !tc.dumpWorks}
|
||||
a := apt.New(apt.Configuration{DebianVersionFile: filepath.Join(root, "etc", "debian_version"), ConfigDirectory: configDir, OverlayFile: filepath.Join(configDir, "99proxyctl"), TrustedRoot: root}, nil, runner)
|
||||
got := a.Availability(context.Background())
|
||||
if got.Available != tc.want || got.Evidence == "" {
|
||||
@@ -92,6 +93,62 @@ func TestAPTAdapterRunsReusableTargetContract(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPTConfigurationKeysAndHostOverridesAreCaseInsensitive(t *testing.T) {
|
||||
a, runner := newAdapter(t, nil)
|
||||
runner.external = "aCqUiRe::HtTp::pRoXy \"http://prior.example:3128\";\n" +
|
||||
"ACQUIRE::HTTP::PROXY::SPECIAL.EXAMPLE \"http://special.example:3128\";"
|
||||
if got := a.Inspect(context.Background(), compatibleProfile()); got.State != target.External {
|
||||
t.Fatalf("mixed-case external inspection = %#v", got)
|
||||
}
|
||||
if got := a.SetDesired(context.Background(), true, compatibleProfile()); got.Err != nil || got.Inspection.State != target.Enabled {
|
||||
t.Fatalf("enable = %#v", got)
|
||||
}
|
||||
managed := readFile(t, a.OverlayPath())
|
||||
if !strings.Contains(managed, `Acquire::http::Proxy::special.example "http://alice:secret@proxy.example:8080";`) {
|
||||
t.Fatalf("mixed-case host override was not masked case-insensitively:\n%s", managed)
|
||||
}
|
||||
|
||||
mustWrite(t, a.OverlayPath(), strings.ReplaceAll(managed, "Acquire::http::Proxy", "ACQUIRE::HTTP::PROXY"))
|
||||
if got := a.Inspect(context.Background(), compatibleProfile()); got.State != target.Enabled {
|
||||
t.Fatalf("mixed-case managed inspection = %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissingProtocolWritesDIRECTAndDisableRestoresUnderlyingConfiguration(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name, configuredKey, configuredURL, missingKey, priorURL string
|
||||
}{
|
||||
{"missing HTTPS", "Acquire::http::Proxy", "http://proxy.example:8080", "Acquire::https::Proxy", "https://prior.example:8443"},
|
||||
{"missing HTTP", "Acquire::https::Proxy", "https://proxy.example:8443", "Acquire::http::Proxy", "http://prior.example:8080"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
a, runner := newAdapter(t, nil)
|
||||
runner.external = tc.missingKey + ` "` + tc.priorURL + `";`
|
||||
p := &profile.Profile{Name: "partial", Mode: profile.AdvancedMode}
|
||||
if strings.Contains(tc.configuredKey, "http::") {
|
||||
p.HTTPURL = tc.configuredURL
|
||||
} else {
|
||||
p.HTTPSURL = tc.configuredURL
|
||||
}
|
||||
|
||||
if got := a.SetDesired(context.Background(), true, p); got.Err != nil || got.Inspection.State != target.Enabled {
|
||||
t.Fatalf("enable = %#v", got)
|
||||
}
|
||||
managed := readFile(t, a.OverlayPath())
|
||||
if !strings.Contains(managed, tc.configuredKey+` "`+tc.configuredURL+`";`) ||
|
||||
!strings.Contains(managed, tc.missingKey+` "DIRECT";`) {
|
||||
t.Fatalf("overlay does not explicitly mask missing protocol:\n%s", managed)
|
||||
}
|
||||
if got := a.SetDesired(context.Background(), false, nil); got.Err != nil || got.Inspection.State != target.External {
|
||||
t.Fatalf("disable = %#v", got)
|
||||
}
|
||||
if !strings.Contains(runner.external, tc.priorURL) {
|
||||
t.Fatalf("underlying configuration changed: %q", runner.external)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnedOverlayWinsAndDisableRevealsPriorEffectiveConfiguration(t *testing.T) {
|
||||
a, runner := newAdapter(t, nil)
|
||||
runner.external = "Acquire::http::Proxy \"http://prior.example:3128\";\n" +
|
||||
@@ -144,6 +201,22 @@ func TestOverlayNameCollisionNeverRewritesOrRemovesUnownedConfiguration(t *testi
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisablePermissionFailureReportsActualManagedState(t *testing.T) {
|
||||
files := &failingFS{}
|
||||
a, _ := newFailingAdapter(t, files)
|
||||
if got := a.SetDesired(context.Background(), true, compatibleProfile()); got.Err != nil {
|
||||
t.Fatalf("enable = %#v", got)
|
||||
}
|
||||
files.removePermission = true
|
||||
got := a.SetDesired(context.Background(), false, nil)
|
||||
if got.Err == nil || !errors.Is(got.Err, os.ErrPermission) || got.Inspection.State != target.Drifted {
|
||||
t.Fatalf("permission-denied disable = %#v", got)
|
||||
}
|
||||
if _, err := os.Stat(a.OverlayPath()); err != nil {
|
||||
t.Fatalf("managed overlay should remain after denied removal: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnableReturnsFailureWhenManagedValuesDoNotBecomeEffective(t *testing.T) {
|
||||
a, runner := newAdapter(t, nil)
|
||||
runner.override = `Acquire::http::Proxy "http://later.example:8080";`
|
||||
@@ -274,7 +347,7 @@ func newFailingAdapter(t *testing.T, files *failingFS) (*apt.Adapter, *fakeRunne
|
||||
}
|
||||
|
||||
type fakeRunner struct {
|
||||
available bool
|
||||
available, outputError bool
|
||||
overlay, external, override, failDumpAfterWrite string
|
||||
}
|
||||
|
||||
@@ -285,6 +358,9 @@ func (r *fakeRunner) LookPath(string) (string, error) {
|
||||
return "/usr/bin/apt-config", nil
|
||||
}
|
||||
func (r *fakeRunner) Output(context.Context, string, ...string) ([]byte, error) {
|
||||
if r.outputError {
|
||||
return nil, errors.New("apt-config dump failed")
|
||||
}
|
||||
if r.failDumpAfterWrite != "" {
|
||||
if _, err := os.Stat(r.failDumpAfterWrite); err == nil {
|
||||
return nil, errors.New("apt-config dump failed")
|
||||
@@ -302,9 +378,9 @@ func (r *fakeRunner) Output(context.Context, string, ...string) ([]byte, error)
|
||||
}
|
||||
|
||||
type failingFS struct {
|
||||
delegate apt.FileSystem
|
||||
permission, failOnce bool
|
||||
writeError error
|
||||
delegate apt.FileSystem
|
||||
permission, failOnce, removePermission bool
|
||||
writeError error
|
||||
}
|
||||
|
||||
func (f *failingFS) ReadFile(path string) ([]byte, error) { return f.delegate.ReadFile(path) }
|
||||
@@ -321,7 +397,12 @@ func (f *failingFS) AtomicWrite(path string, data []byte, mode os.FileMode) erro
|
||||
}
|
||||
return f.delegate.AtomicWrite(path, data, mode)
|
||||
}
|
||||
func (f *failingFS) Remove(path string) error { return f.delegate.Remove(path) }
|
||||
func (f *failingFS) Remove(path string) error {
|
||||
if f.removePermission {
|
||||
return os.ErrPermission
|
||||
}
|
||||
return f.delegate.Remove(path)
|
||||
}
|
||||
func mustMkdir(t *testing.T, path string) {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(path, 0o755); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user