Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package tables

import (
"database/sql"
"fmt"
)

func init() {
MigrationClient.AddMigration(Up_20240314101544, Down_20240314101544)
}

func Up_20240314101544(tx *sql.Tx) error {
// create a new table to store user information that's persisted after
// users are deleted.
_, err := tx.Exec(`
CREATE TABLE IF NOT EXISTS user_persistent_info (
-- id is an unique identifier for the row, independent from whatever is stored in 'users'
id int(10) unsigned NOT NULL AUTO_INCREMENT,

-- user_id is a nullable FK reference to the users table
user_id int(10) unsigned DEFAULT NULL,

-- user_name mirrors the users.name value
user_name varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',

-- user_email mirrors the users.email value
user_email varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,

-- timestamps
created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

PRIMARY KEY (id),
UNIQUE INDEX idx_unique_user_id (user_id),
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL
)
`)
if err != nil {
return fmt.Errorf("failed to add user_persistent_info table: %w", err)
}

// migrate existing data.
_, err = tx.Exec(`
INSERT INTO user_persistent_info (user_id, user_name, user_email)
SELECT id, name, email
FROM users
`)
if err != nil {
return fmt.Errorf("failed to add user information into the user_persistent_info table: %w", err)
}

tables := []string{
"nano_commands", "windows_mdm_commands",
"mdm_apple_configuration_profiles", "mdm_windows_configuration_profiles",
}

for _, t := range tables {
_, err := tx.Exec(fmt.Sprintf(`
ALTER TABLE`+" `%s` "+`
-- user_persistent_info_id references the user that created the entity.
-- it's NULL for rows created prior to this migration,
-- and also for entities that don't have an user
-- associated with it (eg: Fleet initiated actions)
ADD COLUMN user_persistent_info_id int(10) unsigned DEFAULT NULL,

-- fleet_owned indicates if the entity is managed by Fleet.
ADD COLUMN fleet_owned tinyint(1) DEFAULT NULL
`, t))
if err != nil {
return fmt.Errorf("failed to add user_persistent_info_id and fleet_owned to %s: %w", t, err)
}

_, err = tx.Exec(fmt.Sprintf(`
ALTER TABLE`+" `%s` "+`
ADD CONSTRAINT`+" `fk_%s_user_info` "+`
FOREIGN KEY (user_persistent_info_id) REFERENCES user_persistent_info(id)
ON DELETE RESTRICT`, t, t))
if err != nil {
return fmt.Errorf("failed to add user_persistent_info_id foreign key to %s: %w", t, err)
}
}

return nil
}

func Down_20240314101544(tx *sql.Tx) error {
return nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package tables

import (
"fmt"
"strings"
"testing"
"time"

"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/stretchr/testify/require"
)

func TestUp_20240314101544(t *testing.T) {
db := applyUpToPrev(t)

dataStmts := `
INSERT INTO users VALUES
(1,'2023-07-21','2023-07-21',_binary '$2a$12$n6hwsD7OU2bAXX94551DQOBcNNhfsEPS3Y6JEuLDjsLNvry3lgJjy','0fF81xRQIriYzm5fdXouk3V3tRwsZJhV','admin','admin@email.com',0,'','',0,'admin',0),
(2,'2023-07-21','2023-07-21',_binary '$2a$12$YxPPOd5TOmYhDlH5CfGIfuxBe4GJ78gbwvtxoBHTTw.symxpVcEZS','JPDLcBcv4j1QwIU+rHoRWBt3HVJC8hnf','User 1','user1@email.com',0,'','',0,NULL,0),
(3,'2023-07-21','2023-07-21',_binary '$2a$12$u3kuHl44jMojsols1NayLu0pPBwZvnWH6j6ZuDk6HsN4r0jgg7BRu','MoWlTEHH9zR7blcJ0l7/1c4EMnkh/dxq','User 2','user2@email.com',0,'','',0,NULL,0);

INSERT INTO nano_commands
(command_uuid, request_type, command, created_at, updated_at)
VALUES
('nano-command-uuid-1', 'nano', '<?xml', '2023-07-21', '2023-07-21'),
('nano-command-uuid-2', 'nano', '<?xml', '2023-07-21', '2023-07-21');

INSERT INTO windows_mdm_commands
(command_uuid, raw_command, target_loc_uri, created_at, updated_at)
VALUES
('win-command-uuid-1', '<?xml', '', '2023-07-21', '2023-07-21'),
('win-command-uuid-2', '<?xml', '', '2023-07-21', '2023-07-21');

INSERT INTO
mdm_apple_configuration_profiles
(profile_uuid, team_id, identifier, name, mobileconfig, checksum, created_at, uploaded_at)
VALUES
('a1', 0, 'TestPayloadIdentifier', 'TestPayloadName', "<?xml version='1.0'", 'foo', '2023-07-21', '2023-07-21'),
('a2', 0, 'TestPayloadIdentifier2', 'TestPayloadName2', "<?xml version='1.0'", 'foo', '2023-07-21', '2023-07-21');

INSERT INTO
mdm_windows_configuration_profiles
(profile_uuid, team_id, name, syncml, created_at, uploaded_at)
VALUES
('w1', 0, 'TestName', "<?xml version='1.0'", '2023-07-21', '2023-07-21'),
('w2', 0, 'TestName2', "<?xml version='1.0'", '2023-07-21', '2023-07-21');
`

_, err := db.Exec(dataStmts)
require.NoError(t, err)

applyNext(t, db)

// check the newly created user_info tables
type userInfo struct {
ID uint `db:"id"`
UserID *uint `db:"user_id"`
UserName string `db:"user_name"`
UserEmail string `db:"user_email"`
}
var userInfos []userInfo
err = db.Select(
&userInfos,
`SELECT user_id, user_name, id, user_email FROM user_persistent_info`,
)
require.NoError(t, err)
require.ElementsMatch(t, []userInfo{
{ID: uint(1), UserID: ptr.Uint(1), UserEmail: "admin@email.com", UserName: "admin"},
{ID: uint(2), UserID: ptr.Uint(2), UserEmail: "user1@email.com", UserName: "User 1"},
{ID: uint(3), UserID: ptr.Uint(3), UserEmail: "user2@email.com", UserName: "User 2"},
}, userInfos)

// deleting an user doesn't delete the user info
_, err = db.Exec(`DELETE FROM users WHERE name = "User 1"`)
require.NoError(t, err)

var info userInfo
err = db.Get(
&info,
`SELECT user_id, user_name, id FROM user_persistent_info WHERE user_name = "User 1"`,
)
require.NoError(t, err)
require.Nil(t, info.UserID)
require.Equal(t, "User 1", info.UserName)

// check the other tables for timestamps and references
expectedDate, err := time.Parse("2006-01-02", "2023-07-21")
require.NoError(t, err)

tables := []string{
"nano_commands", "windows_mdm_commands",
"mdm_apple_configuration_profiles", "mdm_windows_configuration_profiles",
}

type entity struct {
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
UploadedAt time.Time `db:"uploaded_at"`
UserPersistentInfoID *uint `db:"user_persistent_info_id"`
FleetOwned *bool `db:"fleet_owned"`
}

for _, table := range tables {
updatedTimestamp := "updated_at"
wantUploaded, wantUpdated := time.Time{}, expectedDate
if strings.Contains(table, "configuration_profile") {
updatedTimestamp = "uploaded_at"
wantUploaded, wantUpdated = expectedDate, time.Time{}
}

fetchEntities := func() []entity {
var entities []entity
err = db.Select(
&entities,
fmt.Sprintf(`
SELECT user_persistent_info_id, fleet_owned, created_at, %s
FROM %s`, updatedTimestamp, table),
)
return entities
}

entities := fetchEntities()
require.NoError(t, err)
require.Len(t, entities, 2)

// timestamps are not modified, and columns have the
// expected default values.
for _, entity := range entities {
require.EqualValues(t, expectedDate, entity.CreatedAt)
require.EqualValues(t, wantUpdated, entity.UpdatedAt)
require.EqualValues(t, wantUploaded, entity.UploadedAt)
require.Nil(t, entity.UserPersistentInfoID)
require.Nil(t, entity.FleetOwned)
}

_, err = db.Exec(fmt.Sprintf("UPDATE %s SET fleet_owned = 1", table))
require.NoError(t, err)
entities = fetchEntities()
for _, entity := range entities {
require.Nil(t, entity.UserPersistentInfoID)
require.True(t, *entity.FleetOwned)
}

_, err = db.Exec(fmt.Sprintf("UPDATE %s SET fleet_owned = 0, user_persistent_info_id = 1", table))
require.NoError(t, err)
entities = fetchEntities()
for _, entity := range entities {
require.Equal(t, uint(1), *entity.UserPersistentInfoID)
require.False(t, *entity.FleetOwned)
}

_, err = db.Exec(fmt.Sprintf("UPDATE %s SET user_persistent_info_id = 9", table))
require.ErrorContains(t, err, "foreign key constraint fails")
}
}
Loading