diff --git a/internal/users/db/db_test.go b/internal/users/db/db_test.go index 7dcf6bdf30..0774627cbb 100644 --- a/internal/users/db/db_test.go +++ b/internal/users/db/db_test.go @@ -919,6 +919,55 @@ func TestDeleteUser(t *testing.T) { } } +// TestBackwardCompatibilityAndMigrations covers loading legacy schemas (e.g., v2 with INT ugid) +// and migrating older schemas (e.g., v1 without 'locked' column) to the latest schema. +func TestBackwardCompatibilityAndMigrations(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + dump string + }{ + "SchemaV2_IntUGID": {dump: filepath.Join("testdata", "TestLoadSchemaV2WithIntUGID", "one_user_and_group_v2.sql")}, + "SchemaV1_NoLockedColumn": {dump: filepath.Join("testdata", "TestMigrationAddLockedColumnToUsersTable", "one_user_and_group_without_locked_column.sql")}, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + tempDir := t.TempDir() + + err := db.Z_ForTests_CreateDBFromDump(tc.dump, tempDir) + require.NoError(t, err, "Setup: could not create database from dump") + + // Open using current manager, it'll trigger a migration depending on schema_version. + m, err := db.New(tempDir) + require.NoError(t, err, "Setup: could not open manager for database") + t.Cleanup(func() { _ = m.Close() }) + + // Validate user can be read and that locked is false (either set or default). + u, err := m.UserByID(1111) + require.NoError(t, err, "Should read user from DB") + require.Equal(t, "user1", u.Name) + require.EqualValues(t, 11111, u.GID) + require.False(t, u.Locked, "locked should be false in both old and migrated schemas") + + // Validate group and members. ugid should read as string regardless of underlying type. + g, err := m.GroupWithMembersByID(11111) + require.NoError(t, err, "Should read group from DB") + require.Equal(t, "group1", g.Name) + require.Equal(t, "12345678", g.UGID) + require.Len(t, g.Users, 1) + require.Equal(t, "user1", g.Users[0]) + + // Also ensure lookup by UGID works with string input. + gByUGID, err := m.GroupByUGID("12345678") + require.NoError(t, err) + require.EqualValues(t, 11111, gByUGID.GID) + }) + } +} + // initDB returns a new database ready to be used alongside its database directory. func initDB(t *testing.T, dbFile string) *db.Manager { t.Helper() diff --git a/internal/users/db/sql/create_schema.sql b/internal/users/db/sql/create_schema.sql index f08c89e399..668fcc6b90 100644 --- a/internal/users/db/sql/create_schema.sql +++ b/internal/users/db/sql/create_schema.sql @@ -6,24 +6,24 @@ CREATE TABLE IF NOT EXISTS users ( dir TEXT DEFAULT "", shell TEXT DEFAULT "/bin/bash", broker_id TEXT DEFAULT "", - locked BOOLEAN DEFAULT FALSE + locked BOOLEAN DEFAULT FALSE ); CREATE UNIQUE INDEX "idx_user_name" ON users ("name"); -CREATE TABLE IF NOT EXISTS GROUPS ( +CREATE TABLE IF NOT EXISTS groups ( name TEXT NOT NULL, -- Uniqueness is enforced by the index below gid INT PRIMARY KEY, -- Uniqueness and not NULL is enforced by PRIMARY KEY - ugid INT NOT NULL -- Uniqueness is enforced by the index below + ugid TEXT NOT NULL -- Uniqueness is enforced by the index below ); -CREATE UNIQUE INDEX "idx_group_name" ON GROUPS ("name"); -CREATE UNIQUE INDEX "idx_group_ugid" ON GROUPS ("ugid"); +CREATE UNIQUE INDEX "idx_group_name" ON groups ("name"); +CREATE UNIQUE INDEX "idx_group_ugid" ON groups ("ugid"); CREATE TABLE IF NOT EXISTS users_to_groups ( uid INT NOT NULL, gid INT NOT NULL, PRIMARY KEY (uid, gid), FOREIGN KEY (uid) REFERENCES users (uid) ON DELETE CASCADE, - FOREIGN KEY (gid) REFERENCES GROUPS (gid) ON DELETE CASCADE + FOREIGN KEY (gid) REFERENCES groups (gid) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS users_to_local_groups ( diff --git a/internal/users/db/testdata/TestLoadSchemaV2WithIntUGID/one_user_and_group_v2.sql b/internal/users/db/testdata/TestLoadSchemaV2WithIntUGID/one_user_and_group_v2.sql new file mode 100644 index 0000000000..9fcd97b672 --- /dev/null +++ b/internal/users/db/testdata/TestLoadSchemaV2WithIntUGID/one_user_and_group_v2.sql @@ -0,0 +1,54 @@ +PRAGMA foreign_keys=OFF; +BEGIN TRANSACTION; +CREATE TABLE IF NOT EXISTS users ( + name TEXT NOT NULL, + uid INT PRIMARY KEY, + gid INT NOT NULL, + gecos TEXT DEFAULT "", + dir TEXT DEFAULT "", + shell TEXT DEFAULT "/bin/bash", + broker_id TEXT DEFAULT "", + locked BOOLEAN DEFAULT FALSE +); +CREATE UNIQUE INDEX "idx_user_name" ON users ("name"); + +CREATE TABLE IF NOT EXISTS GROUPS ( + name TEXT NOT NULL, + gid INT PRIMARY KEY, + ugid INT NOT NULL +); +CREATE UNIQUE INDEX "idx_group_name" ON GROUPS ("name"); +CREATE UNIQUE INDEX "idx_group_ugid" ON GROUPS ("ugid"); + +CREATE TABLE IF NOT EXISTS users_to_groups ( + uid INT NOT NULL, + gid INT NOT NULL, + PRIMARY KEY (uid, gid), + FOREIGN KEY (uid) REFERENCES users (uid) ON DELETE CASCADE, + FOREIGN KEY (gid) REFERENCES GROUPS (gid) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS users_to_local_groups ( + uid INT NOT NULL, + group_name TEXT NOT NULL, + PRIMARY KEY (uid, group_name), + FOREIGN KEY (uid) REFERENCES users (uid) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS schema_version ( + version INT PRIMARY KEY +); + +-- Seed data using the old schema (ugid as INT) +INSERT INTO users (name, uid, gid, gecos, dir, shell, broker_id, locked) +VALUES ('user1', 1111, 11111, 'User1 gecos', '/home/user1', '/bin/bash', 'broker-id', FALSE); + +INSERT INTO GROUPS (name, gid, ugid) +VALUES ('group1', 11111, 12345678); + +INSERT INTO users_to_groups (uid, gid) +VALUES (1111, 11111); + +-- Old schema v2 +INSERT INTO schema_version VALUES (2); +COMMIT; \ No newline at end of file