Scert/backend/internal/admin/installer_test.go

243 lines
8.1 KiB
Go

package admin
import (
"bufio"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"testing"
)
func TestLinuxInstallerStoppedUnitStates(t *testing.T) {
bash, err := exec.LookPath("bash")
if err != nil {
t.Skip(err)
}
data, err := os.ReadFile(filepath.Join("..", "..", "..", "deploy", "install-linux.sh"))
if err != nil {
t.Fatal(err)
}
content := string(data)
start := strings.Index(content, "ensure_services_stopped() {")
if start < 0 {
t.Fatal("ensure_services_stopped")
}
end := strings.Index(content[start:], "\n}\n")
if end < 0 {
t.Fatal("ensure_services_stopped")
}
guard := content[start : start+end+3]
stopped := "LoadState=loaded\nActiveState=inactive\nSubState=dead\nControlPID=0\nJob=\n"
for _, tc := range []struct {
name string
unit string
properties string
allowed bool
}{
{"socket-missing-main-pid", "Scert-storage.socket", stopped, true},
{"socket-zero-main-pid", "Scert-storage.socket", stopped + "MainPID=0\n", true},
{"socket-active", "Scert-storage.socket", strings.ReplaceAll(strings.ReplaceAll(stopped, "inactive", "active"), "dead", "listening"), false},
{"socket-control-process", "Scert-storage.socket", strings.ReplaceAll(stopped, "ControlPID=0", "ControlPID=52"), false},
{"socket-missing-control-pid", "Scert-storage.socket", strings.ReplaceAll(stopped, "ControlPID=0\n", ""), false},
{"socket-pending-job", "Scert-storage.socket", strings.ReplaceAll(stopped, "Job=", "Job=7"), false},
{"socket-missing-job", "Scert-storage.socket", strings.ReplaceAll(stopped, "Job=\n", ""), false},
{"socket-nonzero-main-pid", "Scert-storage.socket", stopped + "MainPID=51\n", false},
{"service-missing-main-pid", "Scert.service", stopped, false},
{"admin-missing-main-pid", "Scert-admin.service", stopped, false},
{"storage-missing-main-pid", "Scert-storage.service", stopped, false},
{"service-stopped", "Scert.service", stopped + "MainPID=0\n", true},
} {
t.Run(tc.name, func(t *testing.T) {
script := `set -Eeuo pipefail
fail() { printf '%s\n' "$1" >&2; exit 1; }
systemctl() {
[[ "$1" == show ]] || exit 97
if [[ "$2" == "$TEST_UNIT" ]]; then
printf '%s' "$TEST_PROPERTIES"
else
printf '%s' "$TEST_STOPPED"
[[ "$2" == *.socket ]] || printf 'MainPID=0\n'
fi
return 0
}
` + guard + "\nensure_services_stopped\n"
command := exec.Command(bash, "--noprofile", "--norc", "-c", script)
command.Env = append(os.Environ(), "TEST_UNIT="+tc.unit, "TEST_PROPERTIES="+tc.properties, "TEST_STOPPED="+stopped)
output, err := command.CombinedOutput()
if (err == nil) != tc.allowed {
t.Fatalf("allowed=%t, err=%v, output=%s", tc.allowed, err, output)
}
})
}
}
func TestLinuxInstallerCertificateOwnership(t *testing.T) {
bash, err := exec.LookPath("bash")
if err != nil {
t.Skip(err)
}
data, err := os.ReadFile(filepath.Join("..", "..", "..", "deploy", "install-linux.sh"))
if err != nil {
t.Fatal(err)
}
content := string(data)
start := strings.Index(content, "validate_existing_certificate_dir() {")
if start < 0 {
t.Fatal("validate_existing_certificate_dir")
}
end := strings.Index(content[start:], "\n}\n")
if end < 0 {
t.Fatal("validate_existing_certificate_dir")
}
guard := content[start : start+end+3]
base := t.TempDir()
file := filepath.Join(base, "keep.pem")
if err := os.WriteFile(file, []byte("preserve"), 0600); err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
name string
path string
owner string
allowed bool
}{
{"new-directory", filepath.Join(base, "new"), "root:root", true},
{"owned-directory", base, "Scert:Scert", true},
{"shared-directory", base, "root:root", false},
{"other-service", base, "nginx:nginx", false},
{"wrong-group", base, "Scert:root", false},
{"missing-owner", base, "", false},
{"existing-file", file, "Scert:Scert", false},
} {
t.Run(tc.name, func(t *testing.T) {
script := `set -Eeuo pipefail
SERVICE_USER=Scert
SERVICE_GROUP=Scert
certificate_dir="$TEST_PATH"
fail() { printf '%s\n' "$1" >&2; exit 1; }
stat() { printf '%s\n' "$TEST_OWNER"; }
` + guard + "\nvalidate_existing_certificate_dir\n"
command := exec.Command(bash, "--noprofile", "--norc", "-c", script)
command.Env = append(os.Environ(), "TEST_PATH="+filepath.ToSlash(tc.path), "TEST_OWNER="+tc.owner)
output, err := command.CombinedOutput()
if (err == nil) != tc.allowed {
t.Fatalf("allowed=%t, err=%v, output=%s", tc.allowed, err, output)
}
preserved, err := os.ReadFile(file)
if err != nil || string(preserved) != "preserve" {
t.Fatalf("preserved=%q, err=%v", preserved, err)
}
})
}
}
func TestLinuxInstallerPolicy(t *testing.T) {
installerPath := filepath.Join("..", "..", "..", "deploy", "install-linux.sh")
data, err := os.ReadFile(installerPath)
if err != nil {
t.Fatalf("读取 Linux 安装器失败:%v", err)
}
content := string(data)
required := []string{
`readonly INSTALL_DIR="/scert"`,
`readonly DEFAULT_CERTIFICATE_DIR="/ssl/scert"`,
`validate_existing_certificate_dir`,
`readonly SERVICE_USER="Scert"`,
`readonly INSTALL_METADATA_FILE="$INSTALL_DIR/conf/installation.json"`,
`useradd --system`,
`--badname`,
`--shell "$nologin_shell"`,
`systemctl daemon-reload`,
`--certificate-dir`,
`readlink -m -- "$certificate_dir"`,
`reject_symlink_path "$certificate_dir" "证书目录"`,
`/etc|/etc/*`,
`/scert|/scert/*`,
`chown -R -h "$SERVICE_USER:$SERVICE_GROUP"`,
`write_install_metadata`,
`begin_install_transaction`,
`trap finish_install_transaction EXIT`,
`mv -fT -- "$main_temporary" "$INSTALL_DIR/scert"`,
`mv -T -- "$INSTALL_DIR/web" "$transaction_dir/web-replaced"`,
`load_existing_certificate_dir`,
`certificate_dir_explicit=true`,
`file -b -- "$source_dir/$artifact"`,
`for unit in Scert.service Scert-admin.service`,
`systemctl show "$unit" -p LoadState -p ActiveState -p SubState -p MainPID -p ControlPID -p Job`,
`loaded:inactive:dead|loaded:failed:failed|not-found:inactive:dead`,
`"$main_pid" == "0" && "$control_pid" == "0" && -z "$job"`,
`pwd -P`,
`10-certificate-path.conf`,
`"$INSTALL_DIR/conf"`,
`"$INSTALL_DIR/data"`,
`"$INSTALL_DIR/log"`,
`"$INSTALL_DIR/backups"`,
`"$certificate_dir"`,
}
for _, item := range required {
if !strings.Contains(content, item) {
t.Errorf("Linux 安装器缺少必要策略:%s", item)
}
}
forbiddenCommands := []string{
"systemctl enable Scert-admin",
"systemctl start Scert-admin",
"systemctl restart Scert-admin",
"systemctl enable --now Scert-admin",
}
for _, command := range forbiddenCommands {
if containsExecutableLine(content, command) || containsSystemctlAction(content, strings.Fields(command)[1], "Scert-admin") {
t.Errorf("Linux 安装器不得执行命令:%s", command)
}
}
for _, command := range []string{"ufw ", "firewall-cmd ", "New-NetFirewallRule"} {
if strings.Contains(content, command) {
t.Errorf("Linux 文件安装器不得直接配置防火墙:%s", command)
}
}
if countExecutableLine(content, "systemctl daemon-reload") != 1 {
t.Errorf("Linux 安装器必须且只能执行一次 systemctl daemon-reload")
}
}
func TestSystemctlPolicyMatcherCoversWrappedCommands(t *testing.T) {
for _, content := range []string{
"systemctl --no-block start Scert-admin",
`command systemctl restart "Scert-admin"`,
"sudo systemctl reload Scert-admin",
} {
if !containsSystemctlAction(content, strings.Fields(content)[2], "Scert-admin") {
t.Fatalf("未识别受禁止的 systemctl 调用:%s", content)
}
}
}
func containsExecutableLine(content, command string) bool {
return countExecutableLine(content, command) > 0
}
func containsSystemctlAction(content, action, unit string) bool {
pattern := `(?m)^\s*(?:command\s+|sudo\s+)?systemctl\b[^\n]*\b` + regexp.QuoteMeta(action) + `\b[^\n]*\b` + regexp.QuoteMeta(unit) + `\b`
return regexp.MustCompile(pattern).MatchString(content)
}
func countExecutableLine(content, command string) int {
count := 0
scanner := bufio.NewScanner(strings.NewReader(content))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
if line == command {
count++
}
}
return count
}