147 lines
5.1 KiB
Go
147 lines
5.1 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.pcux.cn/xuans/Scert/backend/internal/config"
|
|
)
|
|
|
|
func TestCompleterPersistsAndUsesCustomCertificateRoot(t *testing.T) {
|
|
fixture := newCompletionFixture(t, "admin.example.test")
|
|
request := fixture.request("admin.example.test")
|
|
request.CertificateRoot = filepath.Join(t.TempDir(), "custom-certificates")
|
|
result, err := fixture.completer(nil).Complete(context.Background(), request)
|
|
if err != nil || !result.Completed {
|
|
t.Fatalf("completion failed: %v", err)
|
|
}
|
|
stored, err := config.Load(fixture.configPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if stored.Paths.Certificate != request.CertificateRoot || !pathWithinCertificateRoot(request.CertificateRoot, stored.Server.TLSCert) {
|
|
t.Fatalf("custom directory not used: root=%s certificate=%s", stored.Paths.Certificate, stored.Server.TLSCert)
|
|
}
|
|
if _, err := os.Stat(stored.Server.TLSCert); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestCertificateRootDefaultsAndRemovesOnlyItsProbe(t *testing.T) {
|
|
fixture := newCompletionFixture(t, "admin.example.test")
|
|
root := fixture.base.Paths.Certificate
|
|
if err := os.MkdirAll(root, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
marker := filepath.Join(root, "existing-file")
|
|
if err := os.WriteFile(marker, []byte("keep"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
resolved, err := prepareCertificateRoot(fixture.base.Paths, "")
|
|
if err != nil || resolved != root {
|
|
t.Fatalf("default directory failed: %s %v", resolved, err)
|
|
}
|
|
entries, err := os.ReadDir(root)
|
|
if err != nil || len(entries) != 1 || entries[0].Name() != "existing-file" {
|
|
t.Fatalf("probe leaked or existing data changed: %v %v", entries, err)
|
|
}
|
|
data, err := os.ReadFile(marker)
|
|
if err != nil || string(data) != "keep" {
|
|
t.Fatal("existing data changed")
|
|
}
|
|
}
|
|
|
|
func TestCertificateRootRejectsUnsafePaths(t *testing.T) {
|
|
fixture := newCompletionFixture(t, "admin.example.test")
|
|
paths := fixture.base.Paths
|
|
root := filepath.VolumeName(paths.Install) + string(filepath.Separator)
|
|
for _, path := range []string{
|
|
"relative/certificates", root, paths.Install, filepath.Dir(paths.Install),
|
|
paths.Config, paths.Data, paths.Log, paths.Backup,
|
|
filepath.Join(paths.Config, "certificates"), filepath.Join(paths.Data, "certificates"),
|
|
paths.Certificate + "\ninvalid", paths.Certificate + "\x00invalid",
|
|
} {
|
|
t.Run(path, func(t *testing.T) {
|
|
_, err := prepareCertificateRoot(paths, path)
|
|
var failure *CompleteError
|
|
if !errors.As(err, &failure) || failure.Kind != CompleteErrorCertificatePath {
|
|
t.Fatalf("unsafe path accepted: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCertificateRootRejectsFilesAndSymlinks(t *testing.T) {
|
|
fixture := newCompletionFixture(t, "admin.example.test")
|
|
root := t.TempDir()
|
|
file := filepath.Join(root, "file")
|
|
if err := os.WriteFile(file, []byte("keep"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := prepareCertificateRoot(fixture.base.Paths, file); err == nil {
|
|
t.Fatal("file accepted as certificate directory")
|
|
}
|
|
link := filepath.Join(root, "link")
|
|
if err := os.Symlink(t.TempDir(), link); err != nil {
|
|
t.Skipf("symbolic links unavailable: %v", err)
|
|
}
|
|
for _, path := range []string{link, filepath.Join(link, "nested")} {
|
|
if _, err := prepareCertificateRoot(fixture.base.Paths, path); err == nil {
|
|
t.Fatal("symbolic link accepted")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCertificateRootRejectsUnwritableDirectory(t *testing.T) {
|
|
if runtime.GOOS == "windows" || os.Geteuid() == 0 {
|
|
t.Skip("requires Unix file permissions without root privileges")
|
|
}
|
|
fixture := newCompletionFixture(t, "admin.example.test")
|
|
root := t.TempDir()
|
|
if err := os.Chmod(root, 0o500); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = os.Chmod(root, 0o700) })
|
|
_, err := prepareCertificateRoot(fixture.base.Paths, root)
|
|
var failure *CompleteError
|
|
if !errors.As(err, &failure) || failure.Kind != CompleteErrorCertificateAccess {
|
|
t.Fatalf("unwritable directory accepted: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCompleterRejectsCertificateRootBeforeIssuing(t *testing.T) {
|
|
fixture := newCompletionFixture(t, "admin.example.test")
|
|
request := fixture.request("admin.example.test")
|
|
request.CertificateRoot = fixture.base.Paths.Config
|
|
_, err := fixture.completer(nil).Complete(context.Background(), request)
|
|
if err == nil || fixture.issuer.calls != 0 {
|
|
t.Fatalf("unsafe directory reached certificate issuer: %v", err)
|
|
}
|
|
if _, err := os.Stat(fixture.configPath); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatal("unsafe directory committed configuration")
|
|
}
|
|
}
|
|
|
|
func TestCertificateRootErrorsDoNotExposeUnderlyingDetails(t *testing.T) {
|
|
for _, item := range []struct {
|
|
kind CompleteErrorKind
|
|
status int
|
|
}{
|
|
{CompleteErrorCertificatePath, http.StatusBadRequest},
|
|
{CompleteErrorCertificateAccess, http.StatusUnprocessableEntity},
|
|
} {
|
|
response := httptest.NewRecorder()
|
|
writeBootstrapCompletionError(response, completeError(item.kind, "secret-value", errors.New("secret-value")))
|
|
if response.Code != item.status || strings.Contains(response.Body.String(), "secret-value") {
|
|
t.Fatalf("unsafe response: %d %s", response.Code, response.Body.String())
|
|
}
|
|
}
|
|
}
|