feat: add resilient updates and access controls
Container-Image veröffentlichen / container (push) Has been cancelled
Container-Image veröffentlichen / container (push) Has been cancelled
This commit is contained in:
1 parent
a000152423
commit
f02ec9977c
41 files changed
+3277
-151
No files matched your search
@@ -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)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user