SAAD: DDM Asset table migration (#48866)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #48566 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [ ] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. Coming in bigger backend story. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. - [x] Timeouts are implemented and retries are limited to avoid infinite loops - [x] If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added support for tracking Apple declaration assets, including a new asset record and a link table for associating assets with declarations. * Added a new timestamp on declarations to reflect the latest asset update time. * **Bug Fixes** * Strengthened database constraints to prevent duplicate asset entries and enforce valid asset/declaration references. * Improved delete behavior so referenced declarations clean up related links automatically. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func init() {
|
||||
MigrationClient.AddMigration(Up_20260707140752, Down_20260707140752)
|
||||
}
|
||||
|
||||
func Up_20260707140752(tx *sql.Tx) error {
|
||||
_, err := tx.Exec(`
|
||||
CREATE TABLE mdm_apple_declaration_assets (
|
||||
asset_uuid varchar(37) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
team_id int unsigned NOT NULL,
|
||||
identifier varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
name varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
raw_json mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
secrets_updated_at datetime(6) NULL DEFAULT NULL,
|
||||
-- generated token drives DDM ServerToken/sync; mirrors mdm_apple_declarations.token
|
||||
token binary(16) GENERATED ALWAYS AS (UNHEX(MD5(CONCAT(raw_json, IFNULL(secrets_updated_at, ''))))) STORED,
|
||||
created_at timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
uploaded_at timestamp(6) NULL DEFAULT NULL,
|
||||
PRIMARY KEY (asset_uuid),
|
||||
UNIQUE KEY idx_mdm_apple_decl_asset_team_identifier (team_id, identifier),
|
||||
UNIQUE KEY idx_mdm_apple_decl_asset_team_name (team_id, name)
|
||||
);
|
||||
`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating mdm_apple_declaration_assets table: %w", err)
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`
|
||||
CREATE TABLE mdm_apple_declaration_asset_references (
|
||||
declaration_uuid varchar(37) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
asset_uuid varchar(37) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
PRIMARY KEY (declaration_uuid, asset_uuid),
|
||||
|
||||
-- deleting the referencing config drops the edge; the asset FK is RESTRICT (default)
|
||||
CONSTRAINT FOREIGN KEY (declaration_uuid) REFERENCES mdm_apple_declarations (declaration_uuid) ON DELETE CASCADE,
|
||||
CONSTRAINT FOREIGN KEY (asset_uuid) REFERENCES mdm_apple_declaration_assets (asset_uuid)
|
||||
);
|
||||
`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating mdm_apple_declaration_asset_references table: %w", err)
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`
|
||||
-- ties a referencing config's per-host token to its assets (mirrors variables_updated_at); the reconciler
|
||||
-- sets it to max(referenced assets' uploaded_at) so an asset edit changes the config token and re-syncs the host
|
||||
ALTER TABLE host_mdm_apple_declarations
|
||||
ADD COLUMN assets_updated_at datetime(6) NULL DEFAULT NULL;
|
||||
`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("adding assets_updated_at column to host_mdm_apple_declarations table: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Down_20260707140752(tx *sql.Tx) error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUp_20260707140752(t *testing.T) {
|
||||
db := applyUpToPrev(t)
|
||||
|
||||
// Apply current migration.
|
||||
applyNext(t, db)
|
||||
|
||||
// Insert a row into mdm_apple_declaration_assets and check for unique constraints
|
||||
_, err := db.ExecContext(t.Context(), "INSERT INTO mdm_apple_declaration_assets (asset_uuid, team_id, identifier, name, raw_json) VALUES ('uuid1', 1, 'identifier1', 'name1', '{}')")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Attempt to insert a duplicate row with the same team_id and identifier
|
||||
_, err = db.ExecContext(t.Context(), "INSERT INTO mdm_apple_declaration_assets (asset_uuid, team_id, identifier, name, raw_json) VALUES ('uuid2', 1, 'identifier1', 'name2', '{}')")
|
||||
require.Error(t, err)
|
||||
|
||||
// Attempt to insert a duplicate row with the same team_id and name
|
||||
_, err = db.ExecContext(t.Context(), "INSERT INTO mdm_apple_declaration_assets (asset_uuid, team_id, identifier, name, raw_json) VALUES ('uuid3', 1, 'identifier3', 'name1', '{}')")
|
||||
require.Error(t, err)
|
||||
|
||||
// Same identifier and name but different team_id should succeed
|
||||
_, err = db.ExecContext(t.Context(), "INSERT INTO mdm_apple_declaration_assets (asset_uuid, team_id, identifier, name, raw_json) VALUES ('uuid4', 2, 'identifier1', 'name1', '{}')")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Insert declaration
|
||||
_, err = db.ExecContext(t.Context(), "INSERT INTO mdm_apple_declarations (declaration_uuid, identifier, name, team_id, raw_json) VALUES ('decl_uuid1', 'identifier1', 'name1', 1, '{}')")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Insert a reference to the asset
|
||||
_, err = db.ExecContext(t.Context(), "INSERT INTO mdm_apple_declaration_asset_references (declaration_uuid, asset_uuid) VALUES ('decl_uuid1', 'uuid1')")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify that mdm_apple_declaration_asset_references table has the correct foreign key constraints
|
||||
_, err = db.ExecContext(t.Context(), "INSERT INTO mdm_apple_declaration_asset_references (declaration_uuid, asset_uuid) VALUES ('decl_uuid2', 'uuid1')")
|
||||
require.Error(t, err) // Should fail because 'decl_uuid2' does not exist in mdm_apple_declarations
|
||||
_, err = db.ExecContext(t.Context(), "INSERT INTO mdm_apple_declaration_asset_references (declaration_uuid, asset_uuid) VALUES ('decl_uuid1', 'uuid-none')")
|
||||
require.Error(t, err) // Should fail because 'uuid-none' does not exist in mdm_apple_declaration_assets
|
||||
|
||||
// Verify deleting asset is not allowed
|
||||
_, err = db.ExecContext(t.Context(), "DELETE FROM mdm_apple_declaration_assets WHERE asset_uuid = 'uuid1'")
|
||||
require.Error(t, err) // Should fail due to foreign key constraint
|
||||
|
||||
// Verify deleting declaration cascades to references
|
||||
_, err = db.ExecContext(t.Context(), "DELETE FROM mdm_apple_declarations WHERE declaration_uuid = 'decl_uuid1'")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify that the reference has been deleted
|
||||
var count int
|
||||
err = db.QueryRowContext(t.Context(), "SELECT COUNT(*) FROM mdm_apple_declaration_asset_references WHERE declaration_uuid = 'decl_uuid1'").Scan(&count)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, count)
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user