You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
165 lines
6.0 KiB
165 lines
6.0 KiB
|
2 months ago
|
package mock
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"errors"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"strings"
|
||
|
|
"tera-clone/services/tera-api/internal/domain"
|
||
|
|
"tera-clone/services/tera-api/internal/supportbundle"
|
||
|
|
"tera-clone/services/tera-api/internal/tlscert"
|
||
|
|
"testing"
|
||
|
|
)
|
||
|
|
|
||
|
|
type resetRebooter struct {
|
||
|
|
calls int
|
||
|
|
check func()
|
||
|
|
}
|
||
|
|
|
||
|
|
func (r *resetRebooter) Reboot(context.Context) error {
|
||
|
|
if r.check != nil {
|
||
|
|
r.check()
|
||
|
|
}
|
||
|
|
r.calls++
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestFactoryResetErasesPersistedDataAndRestoresOnlyTerauser(t *testing.T) {
|
||
|
|
root := t.TempDir()
|
||
|
|
certOptions := tlscert.Options{Root: filepath.Join(root, "tls"), Clock: fixedClock}
|
||
|
|
certs, err := tlscert.Open(certOptions)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
bundles, err := supportbundle.NewManager(supportbundle.Options{Directory: filepath.Join(root, "bundles")})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
reboot := &resetRebooter{}
|
||
|
|
options := StoreOptions{ConfigPath: filepath.Join(root, "config.json"), AuthPath: filepath.Join(root, "auth.json"), SessionPath: filepath.Join(root, "sessions.json"), LoginFailurePath: filepath.Join(root, "failures.json"), SecretPath: filepath.Join(root, "secrets.json"), OperationPath: filepath.Join(root, "operations.json"), AuditPath: filepath.Join(root, "audit.jsonl"), SystemLogPath: filepath.Join(root, "system.jsonl"), PortLogPath: filepath.Join(root, "port.jsonl"), LinkLogPath: filepath.Join(root, "link.jsonl"), TLSCertificateStore: certs, SupportBundleService: bundles, SystemController: reboot}
|
||
|
|
store, err := NewStoreWithOptions(fixedClock, options)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
defer store.Close()
|
||
|
|
oldSession, err := store.Login(context.Background(), domain.AuthLoginRequest{Username: "admin", Password: "mock-admin-secret"})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if err := store.secrets.Set("customer/secret", []byte("customer-secret-marker")); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
store.systemLogEvents = []domain.SystemLogEvent{{Message: "old-system-marker"}}
|
||
|
|
store.portLogEvents = []domain.PortLogEvent{{Message: "old-port-marker"}}
|
||
|
|
store.linkLogEvents = []domain.LinkLogEvent{{Message: "old-link-marker"}}
|
||
|
|
store.authGroupOverrides["customer"] = authGroupRecord{Name: "Customer", Role: "port_operator"}
|
||
|
|
store.addressFilters = domain.AddressFilterSettings{Enabled: true, Mode: "whitelist"}
|
||
|
|
store.loginFailures["customer"] = loginFailureRecord{}
|
||
|
|
if _, err := certs.Generate("customer", tlscert.Subject{CommonName: "customer.example"}); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if _, err := bundles.Create(context.Background(), supportbundle.CreateRequest{BundleID: "support_bundle_20260908_000001", GeneratedAt: fixedClock(), Sections: []supportbundle.Section{{Name: "config.json", Value: "old-bundle-marker"}}}); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
store.runningManagement.CaptchaEnabled = false
|
||
|
|
store.persistentManagement.CaptchaEnabled = false
|
||
|
|
reboot.check = func() {
|
||
|
|
content, err := os.ReadFile(options.AuthPath)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if strings.Contains(string(content), `"admin"`) || !strings.Contains(string(content), `"terauser"`) {
|
||
|
|
t.Fatal("reboot requested before account reset was persisted")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if _, err := store.FactoryReset(context.Background()); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if !store.runningManagement.CaptchaEnabled || !store.persistentManagement.CaptchaEnabled {
|
||
|
|
t.Fatal("factory reset disabled CAPTCHA")
|
||
|
|
}
|
||
|
|
if reboot.calls != 1 {
|
||
|
|
t.Fatal("reset did not request reboot")
|
||
|
|
}
|
||
|
|
if _, err := store.ActorBySession(context.Background(), oldSession.SessionToken); err == nil {
|
||
|
|
t.Fatal("old session survived")
|
||
|
|
}
|
||
|
|
if len(store.authUsers) != 1 || len(store.authGroupOverrides) != 0 || len(store.authTokens) != 0 || len(store.loginFailures) != 0 || store.addressFilters.Enabled {
|
||
|
|
t.Fatal("security state survived reset")
|
||
|
|
}
|
||
|
|
if store.secrets.Has("customer/secret") {
|
||
|
|
t.Fatal("stored credential survived")
|
||
|
|
}
|
||
|
|
if len(certs.List("factory-default")) != 1 {
|
||
|
|
t.Fatal("customer certificate survived")
|
||
|
|
}
|
||
|
|
if _, err := bundles.Read(context.Background(), "support_bundle_20260908_000001"); err == nil {
|
||
|
|
t.Fatal("support bundle survived")
|
||
|
|
}
|
||
|
|
for _, path := range []string{options.SystemLogPath, options.PortLogPath, options.LinkLogPath, options.AuditPath} {
|
||
|
|
content, err := os.ReadFile(path)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if strings.Contains(string(content), "old-") {
|
||
|
|
t.Fatalf("old log survived in %s", path)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
options.RemoveLegacyAuthUsers = true
|
||
|
|
options.TLSCertificateStore, err = tlscert.Open(certOptions)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
reopened, err := NewStoreWithOptions(fixedClock, options)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
defer reopened.Close()
|
||
|
|
if !reopened.runningManagement.CaptchaEnabled {
|
||
|
|
t.Fatal("CAPTCHA disabled after reboot")
|
||
|
|
}
|
||
|
|
if len(reopened.authUsers) != 1 {
|
||
|
|
t.Fatalf("bootstrap accounts resurrected: %d", len(reopened.authUsers))
|
||
|
|
}
|
||
|
|
if _, err := reopened.Login(context.Background(), domain.AuthLoginRequest{Username: "terauser", Password: "terauser"}); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if _, err := reopened.Login(context.Background(), domain.AuthLoginRequest{Username: "admin", Password: "mock-admin-secret"}); err == nil {
|
||
|
|
t.Fatal("old account works after restart")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestFactoryResetCleanupFailureDoesNotRebootOrClearAccounts(t *testing.T) {
|
||
|
|
reboot := &resetRebooter{}
|
||
|
|
store, err := NewStoreWithOptions(fixedClock, StoreOptions{SystemController: reboot, FactoryResetCleanup: func() error { return errors.New("cleanup failed") }})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
defer store.Close()
|
||
|
|
if _, err := store.FactoryReset(context.Background()); err == nil {
|
||
|
|
t.Fatal("reset unexpectedly succeeded")
|
||
|
|
}
|
||
|
|
if reboot.calls != 0 || len(store.authUsers) != 3 {
|
||
|
|
t.Fatal("failed cleanup reported success or changed accounts")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestFactoryResetBlocksFurtherMutations(t *testing.T) {
|
||
|
|
s := NewStore(fixedClock)
|
||
|
|
defer s.Close()
|
||
|
|
release, ok := s.BeginMutation()
|
||
|
|
if !ok {
|
||
|
|
t.Fatal("new store rejected writes")
|
||
|
|
}
|
||
|
|
release()
|
||
|
|
if _, err := s.FactoryReset(context.Background()); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
release, ok = s.BeginMutation()
|
||
|
|
defer release()
|
||
|
|
if ok {
|
||
|
|
t.Fatal("writes allowed after reset before reboot")
|
||
|
|
}
|
||
|
|
}
|