diff --git a/internal/database/postgres/migrations/000125_lock_default_user_groups.down.sql b/internal/database/postgres/migrations/000125_lock_default_user_groups.down.sql new file mode 100644 index 00000000..ca42c1f5 --- /dev/null +++ b/internal/database/postgres/migrations/000125_lock_default_user_groups.down.sql @@ -0,0 +1,6 @@ +UPDATE groups +SET system_managed = FALSE +WHERE id IN ( + '00000000-0000-5000-8000-000000000005', + '00000000-0000-5000-8000-000000000006' +); diff --git a/internal/database/postgres/migrations/000125_lock_default_user_groups.up.sql b/internal/database/postgres/migrations/000125_lock_default_user_groups.up.sql new file mode 100644 index 00000000..4faf9643 --- /dev/null +++ b/internal/database/postgres/migrations/000125_lock_default_user_groups.up.sql @@ -0,0 +1,12 @@ +-- Lock the seeded Standard Users and Read-Only Users groups (issue #168). +-- +-- Owner decision 2026-10-16: these default groups are system-managed, so the +-- #1737 guard (ErrSystemManagedGroup) refuses API edits and deletes, like +-- Administrators, Purchaser and RI Exchanger. Locking is by id, so a group an +-- admin already renamed or edited is locked as-is; permissions are untouched. +UPDATE groups +SET system_managed = TRUE +WHERE id IN ( + '00000000-0000-5000-8000-000000000005', -- Standard Users (000057) + '00000000-0000-5000-8000-000000000006' -- Read-Only Users (000057) +); diff --git a/internal/database/postgres/migrations/000125_lock_default_user_groups_test.go b/internal/database/postgres/migrations/000125_lock_default_user_groups_test.go new file mode 100644 index 00000000..f64fc280 --- /dev/null +++ b/internal/database/postgres/migrations/000125_lock_default_user_groups_test.go @@ -0,0 +1,56 @@ +//go:build integration + +package migrations_test + +import ( + "context" + "testing" + + "github.com/LeanerCloud/cloud-commitments-platform/internal/database/postgres/migrations" + "github.com/LeanerCloud/cloud-commitments-platform/internal/database/postgres/testhelpers" + "github.com/stretchr/testify/require" +) + +func TestMigration_LockDefaultUserGroups(t *testing.T) { + ctx := context.Background() + container := testhelpers.RequirePostgresContainer(ctx, t) + t.Cleanup(func() { require.NoError(t, container.Cleanup(context.Background())) }) + pool, path := container.DB.Pool(), getMigrationsPath() + ids := []string{ + "00000000-0000-5000-8000-000000000005", // Standard Users + "00000000-0000-5000-8000-000000000006", // Read-Only Users + } + const otherID = "00000000-0000-5000-8000-000000000007" // Purchaser, already locked by its own seed + + locked := func(id string) bool { + var v bool + require.NoError(t, pool.QueryRow(ctx, `SELECT system_managed FROM groups WHERE id=$1`, id).Scan(&v)) + return v + } + + require.NoError(t, migrations.MigrateToVersion(ctx, pool, path, 124)) + for _, id := range ids { + require.False(t, locked(id), "before 000125 %s must be editable", id) + } + // An admin-renamed group is still locked by id, with its permissions kept. + _, err := pool.Exec(ctx, `UPDATE groups SET name='Renamed Standard' WHERE id=$1`, ids[0]) + require.NoError(t, err) + var permsBefore string + require.NoError(t, pool.QueryRow(ctx, `SELECT permissions::text FROM groups WHERE id=$1`, ids[0]).Scan(&permsBefore)) + + require.NoError(t, migrations.MigrateToVersion(ctx, pool, path, 125)) + for _, id := range ids { + require.True(t, locked(id), "after 000125 %s must be system-managed", id) + } + var name, permsAfter string + require.NoError(t, pool.QueryRow(ctx, `SELECT name, permissions::text FROM groups WHERE id=$1`, ids[0]).Scan(&name, &permsAfter)) + require.Equal(t, "Renamed Standard", name) + require.Equal(t, permsBefore, permsAfter) + require.True(t, locked(otherID), "Purchaser must stay locked") + + require.NoError(t, migrations.MigrateToVersion(ctx, pool, path, 124)) + for _, id := range ids { + require.False(t, locked(id), "down must restore %s", id) + } + require.True(t, locked(otherID), "down must not unlock groups it did not lock") +}