feat: add resilient updates and access controls
Container-Image veröffentlichen / container (push) Has been cancelled

This commit is contained in:
admin committed 2026-08-28 14:02:07 +02:00
1 parent a000152423
commit f02ec9977c
41 files changed
+3277 -151

No files matched your search

+128
View File
@@ -38,6 +38,26 @@ func TestOpenHardensDataDirectoryAndDatabasePermissions(t *testing.T) {
}
}
func TestCreateAdminOnlyAllowsTheAtomicFirstSetup(t *testing.T) {
t.Parallel()
ctx := context.Background()
database, err := Open(ctx, filepath.Join(t.TempDir(), "app.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
if err := database.CreateAdmin(ctx, "admin", "Administrator", "password-hash"); err != nil {
t.Fatal(err)
}
if err := database.CreateAdmin(ctx, "second", "Second admin", "other-password-hash"); !errors.Is(err, ErrAdminAlreadyConfigured) {
t.Fatalf("second first-run administrator was not rejected: %v", err)
}
if _, _, err := database.GetUserByUsername(ctx, "second"); err == nil {
t.Fatal("rejected second administrator was persisted")
}
}
func TestUserAccessCanBeScopedToTargetOrDomain(t *testing.T) {
t.Parallel()
ctx := context.Background()
@@ -168,6 +188,100 @@ func TestInactiveUserSessionIsRejected(t *testing.T) {
}
}
func TestAdministratorAccessIsRoleBasedAndImmutable(t *testing.T) {
t.Parallel()
ctx := context.Background()
database, err := Open(ctx, filepath.Join(t.TempDir(), "app.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
hash, err := secure.HashPassword("a secure administrator password")
if err != nil {
t.Fatal(err)
}
if err := database.CreateAdmin(ctx, "admin", "Administrator", hash); err != nil {
t.Fatal(err)
}
admin, _, err := database.GetUserByUsername(ctx, "admin")
if err != nil {
t.Fatal(err)
}
if err := database.UpdateUserState(ctx, admin.ID, admin.DisplayName, "member", false); !errors.Is(err, ErrLastActiveAdmin) {
t.Fatalf("last active administrator could be removed: %v", err)
}
if err := database.ReplaceAccessGrants(ctx, admin.ID, AccessSelection{}); !errors.Is(err, ErrAdminAccessImmutable) {
t.Fatalf("administrator accepted editable individual grants: %v", err)
}
member, err := database.CreateUser(ctx, "alice", "Alice", hash, "member")
if err != nil {
t.Fatal(err)
}
credential, err := database.CreateCredential(ctx, "Hetzner", []byte("ciphertext"), []byte("nonce"), "admin-access-test")
if err != nil {
t.Fatal(err)
}
zone, err := database.CreateZone(ctx, credential.ID, "role-test", "role.example")
if err != nil {
t.Fatal(err)
}
if err := database.ReplaceAccessGrants(ctx, member.ID, AccessSelection{ZoneIDs: []int64{zone.ID}}); err != nil {
t.Fatal(err)
}
memberSession, err := database.CreateSession(ctx, model.Session{UserID: member.ID, TokenHash: "member-role-session", CSRFToken: "csrf", ExpiresAt: time.Now().Add(time.Hour)})
if err != nil {
t.Fatal(err)
}
if err := database.UpdateUserState(ctx, member.ID, member.DisplayName, "admin", true); err != nil {
t.Fatal(err)
}
if session, err := database.GetSession(ctx, memberSession.TokenHash); err != nil || session != nil {
t.Fatalf("role change did not invalidate existing sessions: %#v, %v", session, err)
}
grants, err := database.ListAccessGrants(ctx, member.ID)
if err != nil {
t.Fatal(err)
}
if len(grants) != 0 {
t.Fatalf("promoted administrator retained hidden member grants: %#v", grants)
}
if err := database.ReplaceAccessGrants(ctx, member.ID, AccessSelection{ZoneIDs: []int64{zone.ID}}); !errors.Is(err, ErrAdminAccessImmutable) {
t.Fatalf("promoted administrator accepted individual grants: %v", err)
}
}
func TestSoftwareUpdateStateRetainsKnownReleaseOnCheckFailure(t *testing.T) {
t.Parallel()
ctx := context.Background()
database, err := Open(ctx, filepath.Join(t.TempDir(), "app.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
if err := database.SaveSoftwareUpdateCredential(ctx, "registry-user", []byte("encrypted-token"), []byte("nonce")); err != nil {
t.Fatal(err)
}
checkedAt := time.Now().UTC().Truncate(time.Second)
if err := database.RecordSoftwareUpdateSuccess(ctx, "v1.2.3", "https://git.example/releases/v1.2.3", checkedAt); err != nil {
t.Fatal(err)
}
if err := database.SetDesiredSoftwareVersion(ctx, "v1.2.3"); err != nil {
t.Fatal(err)
}
if err := database.RecordSoftwareUpdateFailure(ctx, "temporarily unavailable", checkedAt.Add(time.Minute)); err != nil {
t.Fatal(err)
}
settings, err := database.GetSoftwareUpdateSettings(ctx)
if err != nil {
t.Fatal(err)
}
if settings.RegistryUsername != "registry-user" || string(settings.TokenCiphertext) != "encrypted-token" || settings.LatestVersion != "v1.2.3" || settings.DesiredVersion != "v1.2.3" || settings.LastError != "temporarily unavailable" {
t.Fatalf("software update state was not persisted safely: %#v", settings)
}
}
func TestPiHoleIntegrationsPersistEncryptedCredentialsAndStatus(t *testing.T) {
t.Parallel()
ctx := context.Background()
@@ -268,6 +382,9 @@ func TestIPv6TargetsCanBeDisabledAndTargetCanBeDeleted(t *testing.T) {
if err != nil || len(targets) != 1 || targets[0].ID != target.ID {
t.Fatalf("IPv6 target was not listed: %#v err=%v", targets, err)
}
if err := database.UpdateRunState(ctx, "198.51.100.10", "2001:db8::20", "ok"); err != nil {
t.Fatal(err)
}
if err := database.DisableIPv6ForTargets(ctx); err != nil {
t.Fatal(err)
}
@@ -275,6 +392,10 @@ func TestIPv6TargetsCanBeDisabledAndTargetCanBeDeleted(t *testing.T) {
if err != nil || target.UpdateIPv6 || target.StaticIPv6 != "" {
t.Fatalf("IPv6 target was not disabled cleanly: %#v err=%v", target, err)
}
settings, err := database.GetSettings(ctx)
if err != nil || settings.LastIPv6 != "" {
t.Fatalf("disabled IPv6 kept a stale detected address: %#v err=%v", settings, err)
}
if err := database.DeleteTarget(ctx, target.ID); err != nil {
t.Fatal(err)
}
@@ -335,6 +456,13 @@ func TestUpdaterSettingsAndErrorsCanBeReset(t *testing.T) {
if err := database.UpdateRunState(ctx, "198.51.100.1", "", "partial"); err != nil {
t.Fatal(err)
}
if err := database.UpdateRunState(ctx, "", "", "error"); err != nil {
t.Fatal(err)
}
settings, err = database.GetSettings(ctx)
if err != nil || settings.LastIPv4 != "198.51.100.1" {
t.Fatalf("transient resolver failure discarded the last known IPv4: %#v err=%v", settings, err)
}
if count, err := database.CountErrors(ctx); err != nil || count != 3 {
t.Fatalf("unexpected error count: count=%d err=%v", count, err)
}