From 5331019735e552e29b032ffa4646ecae92101c80 Mon Sep 17 00:00:00 2001 From: Jahziel Villasana-Espinoza Date: Wed, 30 Oct 2024 21:58:34 -0400 Subject: [PATCH 1/7] feat: migration for new col (#23391) > Related issue: #22578 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] 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/Committing-Changes.md#changes-files) for more information. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [x] Added/updated tests - [x] If database migrations are included, checked table schema to confirm autoupdate - For database migrations: - [x] Checked schema for all modified table for columns that will auto-update timestamps during migration. - [x] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - [x] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`). - [x] Manual QA for all new/changed functionality --- changes/22578-db-schema | 1 + ...30102721_AddAllLabelsToMDMProfileLabels.go | 41 +++++++++++ ...721_AddAllLabelsToMDMProfileLabels_test.go | 70 +++++++++++++++++++ server/datastore/mysql/schema.sql | 6 +- 4 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 changes/22578-db-schema create mode 100644 server/datastore/mysql/migrations/tables/20241030102721_AddAllLabelsToMDMProfileLabels.go create mode 100644 server/datastore/mysql/migrations/tables/20241030102721_AddAllLabelsToMDMProfileLabels_test.go diff --git a/changes/22578-db-schema b/changes/22578-db-schema new file mode 100644 index 0000000000..281c14a6b9 --- /dev/null +++ b/changes/22578-db-schema @@ -0,0 +1 @@ +- Adds DB support for "include any" label profile deployment \ No newline at end of file diff --git a/server/datastore/mysql/migrations/tables/20241030102721_AddAllLabelsToMDMProfileLabels.go b/server/datastore/mysql/migrations/tables/20241030102721_AddAllLabelsToMDMProfileLabels.go new file mode 100644 index 0000000000..ada5cb8f64 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20241030102721_AddAllLabelsToMDMProfileLabels.go @@ -0,0 +1,41 @@ +package tables + +import ( + "database/sql" + "fmt" +) + +func init() { + MigrationClient.AddMigration(Up_20241030102721, Down_20241030102721) +} + +func Up_20241030102721(tx *sql.Tx) error { + // Add columns + _, err := tx.Exec(`ALTER TABLE mdm_configuration_profile_labels ADD COLUMN require_all BOOL NOT NULL DEFAULT false`) + if err != nil { + return fmt.Errorf("failed to add require_all to mdm_configuration_profile_labels: %w", err) + } + + _, err = tx.Exec(`ALTER TABLE mdm_declaration_labels ADD COLUMN require_all BOOL NOT NULL DEFAULT false`) + if err != nil { + return fmt.Errorf("failed to add require_all to mdm_declaration_labels: %w", err) + } + + // Set require_all to true if exclude was false (this means that it represents an "include all" + // label filter + _, err = tx.Exec(`UPDATE mdm_configuration_profile_labels SET require_all = true WHERE exclude = false`) + if err != nil { + return fmt.Errorf("failed to migrate include all records in mdm_configuration_profile_labels: %w", err) + } + + _, err = tx.Exec(`UPDATE mdm_declaration_labels SET require_all = true WHERE exclude = false`) + if err != nil { + return fmt.Errorf("failed to migrate include all records in mdm_declaration_labels: %w", err) + } + + return nil +} + +func Down_20241030102721(tx *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/migrations/tables/20241030102721_AddAllLabelsToMDMProfileLabels_test.go b/server/datastore/mysql/migrations/tables/20241030102721_AddAllLabelsToMDMProfileLabels_test.go new file mode 100644 index 0000000000..4a0a9b97fc --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20241030102721_AddAllLabelsToMDMProfileLabels_test.go @@ -0,0 +1,70 @@ +package tables + +import ( + "context" + "testing" + + "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/require" +) + +func TestUp_20241030102721(t *testing.T) { + db := applyUpToPrev(t) + + // insert 2 profiles and 2 declarations + execNoErr(t, db, `INSERT INTO mdm_apple_configuration_profiles (team_id, identifier, name, mobileconfig, checksum, profile_uuid) VALUES (0, 'A', 'nameA', '', '', 'A')`) + execNoErr(t, db, `INSERT INTO mdm_apple_configuration_profiles (team_id, identifier, name, mobileconfig, checksum, profile_uuid) VALUES (0, 'B', 'nameB', '', '', 'B')`) + + execNoErr(t, db, `INSERT INTO mdm_apple_declarations (declaration_uuid, identifier, name, raw_json, checksum, team_id) VALUES ('C', 'C', 'nameC', '{"foo": "bar"}', '', 0)`) + execNoErr(t, db, `INSERT INTO mdm_apple_declarations (declaration_uuid, identifier, name, raw_json, checksum, team_id) VALUES ('D', 'D', 'nameD', '{"foo": "bar"}', '', 0)`) + + // insert 2 profile labels associations: 1 that's exclude any and 1 that's include all + cfgExcludeAnyID := execNoErrLastID(t, db, `INSERT INTO mdm_configuration_profile_labels (apple_profile_uuid, label_name, exclude) VALUES ('A', 'foo', true)`) + cfgIncludeAllID := execNoErrLastID(t, db, `INSERT INTO mdm_configuration_profile_labels (apple_profile_uuid, label_name, exclude) VALUES ('B', 'bar', false)`) + + declExcludeAnyID := execNoErrLastID(t, db, `INSERT INTO mdm_declaration_labels (apple_declaration_uuid, label_name, exclude) VALUES ('C', 'baz', true)`) + declIncludeAllID := execNoErrLastID(t, db, `INSERT INTO mdm_declaration_labels (apple_declaration_uuid, label_name, exclude) VALUES ('C', 'boo', false)`) + + // Apply current migration. + applyNext(t, db) + + var cps []struct { + ID int64 `db:"id"` + Exclude bool `db:"exclude"` + AllLabels bool `db:"require_all"` + } + + err := sqlx.SelectContext(context.Background(), db, &cps, `SELECT id, exclude, require_all FROM mdm_configuration_profile_labels`) + require.NoError(t, err) + + for _, c := range cps { + // the exclude any should be unchanged + if c.ID == cfgExcludeAnyID { + require.True(t, c.Exclude) + require.False(t, c.AllLabels) + } + + // the include all should have require_all = true + if c.ID == cfgIncludeAllID { + require.False(t, c.Exclude) + require.True(t, c.AllLabels) + } + } + + err = sqlx.SelectContext(context.Background(), db, &cps, `SELECT id, exclude, require_all FROM mdm_declaration_labels`) + require.NoError(t, err) + + for _, c := range cps { + // the exclude any should be unchanged + if c.ID == declExcludeAnyID { + require.True(t, c.Exclude) + require.False(t, c.AllLabels) + } + + // the include all should have require_all = true + if c.ID == declIncludeAllID { + require.False(t, c.Exclude) + require.True(t, c.AllLabels) + } + } +} diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index 3fa9bfa650..eccc0976e7 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -996,6 +996,7 @@ CREATE TABLE `mdm_configuration_profile_labels` ( `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `exclude` tinyint(1) NOT NULL DEFAULT '0', + `require_all` tinyint(1) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `idx_mdm_configuration_profile_labels_apple_label_name` (`apple_profile_uuid`,`label_name`), UNIQUE KEY `idx_mdm_configuration_profile_labels_windows_label_name` (`windows_profile_uuid`,`label_name`), @@ -1016,6 +1017,7 @@ CREATE TABLE `mdm_declaration_labels` ( `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `uploaded_at` timestamp NULL DEFAULT NULL, `exclude` tinyint(1) NOT NULL DEFAULT '0', + `require_all` tinyint(1) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `idx_mdm_declaration_labels_label_name` (`apple_declaration_uuid`,`label_name`), KEY `label_id` (`label_id`), @@ -1099,9 +1101,9 @@ CREATE TABLE `migration_status_tables` ( `tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `id` (`id`) -) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=327 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=328 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,20241021224359,1,'2020-01-01 01:01:01'),(323,20241022140321,1,'2020-01-01 01:01:01'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'); +INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,20241021224359,1,'2020-01-01 01:01:01'),(323,20241022140321,1,'2020-01-01 01:01:01'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'),(327,20241030102721,1,'2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `mobile_device_management_solutions` ( From ccbdf46119c57fd6abd7d0c518fe270937f88941 Mon Sep 17 00:00:00 2001 From: Gabriel Hernandez Date: Thu, 31 Oct 2024 15:10:49 +0000 Subject: [PATCH 2/7] add UI to add the include any option for custom profile custom label target (#23390) relates to #22575 Add the UI for allowing custom profiles to be uploaded to hosts that have any of specified labels. I also spent some time cleaning up the custom settings card and its components. I was able to remove a lot of unused styles. ![image](https://github.com/user-attachments/assets/167a575c-4520-496b-8f73-ed390e079e44) - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. - [ ] Added/updated tests - [x] Manual QA for all new/changed functionality --- changes/22575-ui-for-include-any-labels | 2 + frontend/interfaces/mdm.ts | 1 + .../cards/CustomSettings/CustomSettings.tsx | 23 +-- .../cards/CustomSettings/_styles.scss | 149 ------------------ .../ProfileLabelsModal/ProfileLabelsModal.tsx | 93 +++++------ .../ProfileLabelsModal/_styles.scss | 32 ++++ .../ProfileListItem/ProfileListItem.tsx | 3 +- .../{ => AddProfileCard}/AddProfileCard.tsx | 16 +- .../components/AddProfileCard/_styles.scss | 21 +++ .../components/AddProfileCard/index.ts | 1 + .../AddProfileModal/AddProfileModal.tsx | 17 +- .../components/AddProfileModal/helpers.tsx | 31 +++- frontend/services/entities/mdm.ts | 19 ++- 13 files changed, 158 insertions(+), 250 deletions(-) create mode 100644 changes/22575-ui-for-include-any-labels create mode 100644 frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileLabelsModal/_styles.scss rename frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/{ => AddProfileCard}/AddProfileCard.tsx (69%) create mode 100644 frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileCard/_styles.scss create mode 100644 frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileCard/index.ts diff --git a/changes/22575-ui-for-include-any-labels b/changes/22575-ui-for-include-any-labels new file mode 100644 index 0000000000..5f66f8396b --- /dev/null +++ b/changes/22575-ui-for-include-any-labels @@ -0,0 +1,2 @@ +- add UI for allowing users to install custom profiles on hosts that include any of the defined +labels diff --git a/frontend/interfaces/mdm.ts b/frontend/interfaces/mdm.ts index 2f822eb968..07fe2cffb9 100644 --- a/frontend/interfaces/mdm.ts +++ b/frontend/interfaces/mdm.ts @@ -111,6 +111,7 @@ export interface IMdmProfile { updated_at: string; checksum: string | null; // null for windows profiles labels_include_all?: IProfileLabel[]; + labels_include_any?: IProfileLabel[]; labels_exclude_any?: IProfileLabel[]; } diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/CustomSettings.tsx b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/CustomSettings.tsx index 89887e9b83..00cc7e199b 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/CustomSettings.tsx +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/CustomSettings.tsx @@ -20,7 +20,7 @@ import Pagination from "pages/ManageControlsPage/components/Pagination"; import UploadList from "../../../components/UploadList"; import AddProfileCard from "./components/ProfileUploader/components/AddProfileCard"; -import AddProfileModal from "./components/ProfileUploader/components/AddProfileModal/AddProfileModal"; +import AddProfileModal from "./components/ProfileUploader/components/AddProfileModal"; import DeleteProfileModal from "./components/DeleteProfileModal/DeleteProfileModal"; import ProfileLabelsModal from "./components/ProfileLabelsModal/ProfileLabelsModal"; import ProfileListItem from "./components/ProfileListItem"; @@ -136,7 +136,7 @@ const CustomSettings = ({ } if (!profiles?.length) { - return null; + return ; } return ( @@ -144,11 +144,11 @@ const CustomSettings = ({ - ProfileListHeading({ - onClickAddProfile: () => setShowAddProfileModal(true), - }) - } + HeadingComponent={() => ( + setShowAddProfileModal(true)} + /> + )} ListItemComponent={({ listItem }) => (

- {renderProfileList()} - {!isLoadingProfiles && !isErrorProfiles && !profiles?.length && ( - - )} + <>{renderProfileList()} {showAddProfileModal && ( diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/_styles.scss b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/_styles.scss index 30a35b38fb..b758f3c777 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/_styles.scss +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/_styles.scss @@ -1,69 +1,15 @@ .custom-settings { - .section-header { - margin: 0; - padding: 0 0 12px 0; - - h2 { - padding-bottom: 0; - border-bottom: none; - margin: 0; - } - } - &__description { font-size: $x-small; margin: $pad-large 0; } - &__profiles-header { - padding: $pad-medium $pad-large; - display: flex; - justify-content: space-between; - font-size: $x-small; - font-weight: $bold; - border-bottom: 1px solid $ui-fleet-black-10; - } - - &__profile-list { - list-style: none; - padding: 0; - margin: 0; - } - &__pagination-controls { display: flex; justify-content: flex-end; margin: $pad-large 0; } - &__file-uploader { - margin-top: $pad-xxlarge; - } - - &__labels-list { - border-radius: 6px; - border: 1px solid $ui-fleet-black-10; - - &--label { - display: flex; - height: 41px; - padding: 0 $pad-large; - align-items: center; - justify-content: space-between; - border-bottom: 1px solid $ui-fleet-black-10; - - .warning { - display: flex; - padding: 0; - gap: $pad-small; - } - - &:last-of-type { - border-bottom: none; - } - } - } - .upload-list { &__list { .list-item__label-count { @@ -84,99 +30,4 @@ } } } - - .add-profile { - &__card--content-wrap { - display: flex; - flex-direction: column; - align-items: center; - gap: $pad-medium; - padding: 28.5px 0; - } - - &__profile-graphic { - display: flex; - flex-direction: column; - align-items: center; - gap: $pad-small; - - &--message { - text-align: center; - line-height: 20px; - } - } - - &__button-wrap { - display: flex; - justify-content: flex-end; - padding-top: $pad-medium; - } - - &__target { - margin: $pad-large 0 $pad-small 0; - } - - &__description { - margin: $pad-medium 0; - } - - &__no-labels { - display: flex; - height: 187px; - flex-direction: column; - align-items: center; - gap: $pad-small; - justify-content: center; - - span { - color: $ui-fleet-black-75; - } - } - - &__checkboxes { - display: flex; - max-height: 187px; - flex-direction: column; - border-radius: $border-radius; - border: 1px solid $ui-fleet-black-10; - overflow-y: auto; - - .loading-spinner { - margin: 69.5px auto; - } - } - - &__label { - width: 100%; - padding: $pad-small $pad-medium; - box-sizing: border-box; - display: flex; - align-items: center; - - &:not(:last-child) { - border-bottom: 1px solid $ui-fleet-black-10; - } - - .form-field--checkbox { - width: auto; - } - } - - &__label-name { - padding-left: $pad-large; - } - - .fleet-checkbox { - height: 20px; - display: flex; - align-items: center; - - &__label { - width: 490px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - } - } } diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileLabelsModal/ProfileLabelsModal.tsx b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileLabelsModal/ProfileLabelsModal.tsx index 4128a5e826..f5590c83af 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileLabelsModal/ProfileLabelsModal.tsx +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileLabelsModal/ProfileLabelsModal.tsx @@ -6,35 +6,7 @@ import InfoBanner from "components/InfoBanner"; import TooltipWrapper from "components/TooltipWrapper"; import Icon from "components/Icon"; -interface IModalDescriptionProps { - baseClass: string; - profileName: string; - targetType: "includeAll" | "excludeAny"; -} - -const ModalDescription = ({ - baseClass, - profileName, - targetType, -}: IModalDescriptionProps) => { - const targetTypeText = - targetType === "includeAll" ? ( - <> - have all - - ) : ( - <> - don't have any - - ); - - return ( -
- {profileName} profile only applies to hosts that {targetTypeText}{" "} - of these labels: -
- ); -}; +const baseClass = "profile-labels-modal"; const BrokenLabelWarning = () => ( @@ -51,16 +23,10 @@ const BrokenLabelWarning = () => ( ); -const LabelsList = ({ - baseClass, - labels, -}: { - baseClass: string; - labels: IProfileLabel[]; -}) => ( -
+const LabelsList = ({ labels }: { labels: IProfileLabel[] }) => ( +
    {labels.map((label) => ( -
    +
  • {label.name} {label.broken && ( @@ -68,19 +34,17 @@ const LabelsList = ({ Label deleted )} -
  • + ))} -
+ ); interface IProfileLabelsModalProps { - baseClass: string; profile: IMdmProfile | null; setModalData: React.Dispatch>; } const ProfileLabelsModal = ({ - baseClass, profile, setModalData, }: IProfileLabelsModalProps) => { @@ -88,30 +52,53 @@ const ProfileLabelsModal = ({ return null; } - const { name, labels_include_all, labels_exclude_any } = profile; - const labels = labels_include_all || labels_exclude_any; + const { + name, + labels_include_all, + labels_include_any, + labels_exclude_any, + } = profile; + const labels = labels_include_all || labels_include_any || labels_exclude_any; if (!labels?.length) { // caller ensures this never happens return null; } + const renderlabelDescription = () => { + let targetTypeText = <>; + if (labels_include_all) { + targetTypeText = have all; + } else if (labels_include_any) { + targetTypeText = have all; + } else { + targetTypeText = don't have any; + } + + return ( +

+ {name} profile only applies to hosts that {targetTypeText} of + these labels: +

+ ); + }; + return ( - setModalData(null)}> -
+ setModalData(null)} + > + <> {labels.some((label) => label.broken) && } - - + <>{renderlabelDescription()} +
-
+
); }; diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileLabelsModal/_styles.scss b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileLabelsModal/_styles.scss new file mode 100644 index 0000000000..1bac831a22 --- /dev/null +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileLabelsModal/_styles.scss @@ -0,0 +1,32 @@ +.profile-labels-modal { + &__description { + font-size: $x-small; + margin: $pad-large 0 $pad-medium; + } + + &__labels-list { + border-radius: 6px; + border: 1px solid $ui-fleet-black-10; + padding: 0; + margin: 0; + + &--label { + display: flex; + height: 41px; + padding: 0 $pad-large; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid $ui-fleet-black-10; + + .warning { + display: flex; + padding: 0; + gap: $pad-small; + } + + &:last-of-type { + border-bottom: none; + } + } + } +} diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileListItem/ProfileListItem.tsx b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileListItem/ProfileListItem.tsx index 9fb0039d28..b62437d8d8 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileListItem/ProfileListItem.tsx +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileListItem/ProfileListItem.tsx @@ -87,6 +87,7 @@ const ProfileListItem = ({ const { created_at, labels_include_all, + labels_include_any, labels_exclude_any, name, platform, @@ -102,7 +103,7 @@ const ProfileListItem = ({ FileSaver.saveAs(file); }; - const labels = labels_include_all || labels_exclude_any; + const labels = labels_include_all || labels_include_any || labels_exclude_any; const renderLabelInfo = () => { if (!isPremium || labels === undefined || labels.length === 0) { diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileCard.tsx b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileCard/AddProfileCard.tsx similarity index 69% rename from frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileCard.tsx rename to frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileCard/AddProfileCard.tsx index c3ab102ce7..d9d09f081f 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileCard.tsx +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileCard/AddProfileCard.tsx @@ -2,16 +2,16 @@ import React from "react"; import Card from "components/Card"; import Button from "components/buttons/Button"; -import ProfileGraphic from "./AddProfileGraphic"; +import ProfileGraphic from "../AddProfileGraphic"; -const AddProfileCard = ({ - baseClass, - setShowModal, -}: { - baseClass: string; +const baseClass = "add-profile-card"; + +interface IAddProfileCardProps { setShowModal: React.Dispatch>; -}) => ( - +} + +const AddProfileCard = ({ setShowModal }: IAddProfileCardProps) => ( +
); diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileModal/helpers.tsx b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileModal/helpers.tsx index d6d61c5628..a63c121023 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileModal/helpers.tsx +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileModal/helpers.tsx @@ -5,11 +5,22 @@ import { IDropdownOption } from "interfaces/dropdownOption"; export const CUSTOM_TARGET_OPTIONS: IDropdownOption[] = [ { value: "labelsIncludeAll", - label: "Include all ", + label: "Include all", helpText: ( <> - Profile will only be applied to hosts that have all of these - labels{" "} + Profile will only be applied to hosts that have all of these + labels. + + ), + disabled: false, + }, + { + value: "labelsIncludeAny", + label: "Include any", + helpText: ( + <> + Profile will only be applied to hosts that have any of these + labels. ), disabled: false, @@ -19,8 +30,8 @@ export const CUSTOM_TARGET_OPTIONS: IDropdownOption[] = [ label: "Exclude any", helpText: ( <> - Profile will be applied to hosts that don't have any of - these labels{" "} + Profile will only be applied to hosts that don't have any of + these labels. ), disabled: false, @@ -36,7 +47,10 @@ export const listNamesFromSelectedLabels = (dict: Record) => { }, [] as string[]); }; -export type CustomTargetOption = "labelsIncludeAll" | "labelsExcludeAny"; +export type CustomTargetOption = + | "labelsIncludeAll" + | "labelsIncldeAny" + | "labelsExcludeAny"; export const generateLabelKey = ( target: string, @@ -51,3 +65,8 @@ export const generateLabelKey = ( [customTargetOption]: listNamesFromSelectedLabels(selectedLabels), }; }; + +export const getDescriptionText = (value: string) => { + return CUSTOM_TARGET_OPTIONS.find((option) => option.value === value) + ?.helpText; +}; diff --git a/frontend/services/entities/mdm.ts b/frontend/services/entities/mdm.ts index 285973d7ff..50ed3bd428 100644 --- a/frontend/services/entities/mdm.ts +++ b/frontend/services/entities/mdm.ts @@ -49,6 +49,7 @@ export interface IUploadProfileApiParams { file: File; teamId?: number; labelsIncludeAll?: string[]; + labelsIncludeAny?: string[]; labelsExcludeAny?: string[]; } @@ -132,6 +133,7 @@ const mdmService = { file, teamId, labelsIncludeAll, + labelsIncludeAny, labelsExcludeAny, }: IUploadProfileApiParams) => { const { MDM_PROFILES } = endpoints; @@ -143,11 +145,18 @@ const mdmService = { formData.append("team_id", teamId.toString()); } - if (labelsIncludeAll || labelsExcludeAny) { - const labels = labelsIncludeAll || labelsExcludeAny; - const labelKey = labelsIncludeAll - ? "labels_include_all" - : "labels_exclude_any"; + if (labelsIncludeAll || labelsIncludeAny || labelsExcludeAny) { + const labels = labelsIncludeAll || labelsIncludeAny || labelsExcludeAny; + + let labelKey = ""; + if (labelsIncludeAll) { + labelKey = "labels_include_all"; + } else if (labelsIncludeAny) { + labelKey = "labels_include_any"; + } else { + labelKey = "labels_exclude_any"; + } + labels?.forEach((label) => { formData.append(labelKey, label); }); From d050ddbeb12a745328e6bb2149d1a512b9de991e Mon Sep 17 00:00:00 2001 From: Sarah Gillespie <73313222+gillespi314@users.noreply.github.com> Date: Tue, 5 Nov 2024 12:12:22 -0600 Subject: [PATCH 3/7] Update API for MDM profiles with "include any" labels option (#23456) --- server/datastore/mysql/apple_mdm.go | 36 ++++++++++--- server/datastore/mysql/mdm.go | 14 ++--- server/datastore/mysql/microsoft_mdm.go | 11 +++- server/fleet/apple_mdm.go | 5 +- server/fleet/apple_mdm_test.go | 5 +- server/fleet/mdm.go | 16 ++++-- server/fleet/service.go | 6 +-- server/fleet/windows_mdm.go | 1 + server/service/apple_mdm.go | 25 ++++++--- server/service/apple_mdm_test.go | 37 ++++++++------ .../service/integration_mdm_profiles_test.go | 6 +-- server/service/mdm.go | 51 ++++++++++++------- server/service/mdm_test.go | 12 +++-- 13 files changed, 152 insertions(+), 73 deletions(-) diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go index e96238645f..8574f40c61 100644 --- a/server/datastore/mysql/apple_mdm.go +++ b/server/datastore/mysql/apple_mdm.go @@ -79,14 +79,23 @@ INSERT INTO // filled in. profileID, _ = res.LastInsertId() - labels := make([]fleet.ConfigurationProfileLabel, 0, len(cp.LabelsIncludeAll)+len(cp.LabelsExcludeAny)) + labels := make([]fleet.ConfigurationProfileLabel, 0, len(cp.LabelsIncludeAll)+len(cp.LabelsIncludeAny)+len(cp.LabelsExcludeAny)) for i := range cp.LabelsIncludeAll { cp.LabelsIncludeAll[i].ProfileUUID = profUUID + cp.LabelsIncludeAll[i].Exclude = false + cp.LabelsIncludeAll[i].RequireAll = true labels = append(labels, cp.LabelsIncludeAll[i]) } + for i := range cp.LabelsIncludeAny { + cp.LabelsIncludeAny[i].ProfileUUID = profUUID + cp.LabelsIncludeAny[i].Exclude = false + cp.LabelsIncludeAny[i].RequireAll = false + labels = append(labels, cp.LabelsIncludeAny[i]) + } for i := range cp.LabelsExcludeAny { cp.LabelsExcludeAny[i].ProfileUUID = profUUID cp.LabelsExcludeAny[i].Exclude = true + cp.LabelsExcludeAny[i].RequireAll = false labels = append(labels, cp.LabelsExcludeAny[i]) } if _, err := batchSetProfileLabelAssociationsDB(ctx, tx, labels, "darwin"); err != nil { @@ -3808,7 +3817,8 @@ func (ds *Datastore) updateHostDEPAssignProfileResponses(ctx context.Context, pa } func updateHostDEPAssignProfileResponses(ctx context.Context, tx sqlx.ExtContext, logger log.Logger, profileUUID string, serials []string, - status string, abmTokenID *uint) error { + status string, abmTokenID *uint, +) error { if len(serials) == 0 { return nil } @@ -4320,14 +4330,23 @@ func (ds *Datastore) insertOrUpsertMDMAppleDeclaration(ctx context.Context, insO } labels := make([]fleet.ConfigurationProfileLabel, 0, - len(declaration.LabelsIncludeAll)+len(declaration.LabelsExcludeAny)) + len(declaration.LabelsIncludeAll)+len(declaration.LabelsIncludeAny)+len(declaration.LabelsExcludeAny)) for i := range declaration.LabelsIncludeAll { declaration.LabelsIncludeAll[i].ProfileUUID = declUUID + declaration.LabelsIncludeAll[i].Exclude = false + declaration.LabelsIncludeAll[i].RequireAll = true labels = append(labels, declaration.LabelsIncludeAll[i]) } + for i := range declaration.LabelsIncludeAny { + declaration.LabelsIncludeAny[i].ProfileUUID = declUUID + declaration.LabelsIncludeAny[i].Exclude = false + declaration.LabelsIncludeAny[i].RequireAll = false + labels = append(labels, declaration.LabelsIncludeAny[i]) + } for i := range declaration.LabelsExcludeAny { declaration.LabelsExcludeAny[i].ProfileUUID = declUUID declaration.LabelsExcludeAny[i].Exclude = true + declaration.LabelsExcludeAny[i].RequireAll = false labels = append(labels, declaration.LabelsExcludeAny[i]) } if _, err := batchSetDeclarationLabelAssociationsDB(ctx, tx, labels); err != nil { @@ -4362,16 +4381,17 @@ func batchSetDeclarationLabelAssociationsDB(ctx context.Context, tx sqlx.ExtCont upsertStmt := ` INSERT INTO mdm_declaration_labels - (apple_declaration_uuid, label_id, label_name, exclude) + (apple_declaration_uuid, label_id, label_name, exclude, require_all) VALUES %s ON DUPLICATE KEY UPDATE label_id = VALUES(label_id), - exclude = VALUES(exclude) + exclude = VALUES(exclude), + require_all = VALUES(require_all) ` selectStmt := ` - SELECT apple_declaration_uuid as profile_uuid, label_name, label_id, exclude FROM mdm_declaration_labels + SELECT apple_declaration_uuid as profile_uuid, label_name, label_id, exclude, require_all FROM mdm_declaration_labels WHERE (apple_declaration_uuid, label_name) IN (%s) ` @@ -4391,10 +4411,10 @@ func batchSetDeclarationLabelAssociationsDB(ctx context.Context, tx sqlx.ExtCont insertBuilder.WriteString(",") selectOrDeleteBuilder.WriteString(",") } - insertBuilder.WriteString("(?, ?, ?, ?)") + insertBuilder.WriteString("(?, ?, ?, ?, ?)") selectOrDeleteBuilder.WriteString("(?, ?)") selectParams = append(selectParams, pl.ProfileUUID, pl.LabelName) - insertParams = append(insertParams, pl.ProfileUUID, pl.LabelID, pl.LabelName, pl.Exclude) + insertParams = append(insertParams, pl.ProfileUUID, pl.LabelID, pl.LabelName, pl.Exclude, pl.RequireAll) deleteParams = append(deleteParams, pl.ProfileUUID, pl.LabelID) setProfileUUIDs[pl.ProfileUUID] = struct{}{} diff --git a/server/datastore/mysql/mdm.go b/server/datastore/mysql/mdm.go index 5a68667bf3..d01bad72bd 100644 --- a/server/datastore/mysql/mdm.go +++ b/server/datastore/mysql/mdm.go @@ -124,7 +124,8 @@ func (ds *Datastore) getMDMCommand(ctx context.Context, q sqlx.QueryerContext, c func (ds *Datastore) BatchSetMDMProfiles(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDeclarations []*fleet.MDMAppleDeclaration) (updates fleet.MDMProfilesUpdates, - err error) { + err error, +) { err = ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error { var err error if updates.WindowsConfigProfile, err = ds.batchSetMDMWindowsProfilesDB(ctx, tx, tmID, winProfiles); err != nil { @@ -1025,16 +1026,17 @@ func batchSetProfileLabelAssociationsDB( upsertStmt := ` INSERT INTO mdm_configuration_profile_labels - (%s_profile_uuid, label_id, label_name, exclude) + (%s_profile_uuid, label_id, label_name, exclude, require_all) VALUES %s ON DUPLICATE KEY UPDATE label_id = VALUES(label_id), - exclude = VALUES(exclude) + exclude = VALUES(exclude), + require_all = VALUES(require_all) ` selectStmt := ` - SELECT %s_profile_uuid as profile_uuid, label_id, label_name, exclude FROM mdm_configuration_profile_labels + SELECT %s_profile_uuid as profile_uuid, label_id, label_name, exclude, require_all FROM mdm_configuration_profile_labels WHERE (%s_profile_uuid, label_name) IN (%s) ` @@ -1054,10 +1056,10 @@ func batchSetProfileLabelAssociationsDB( insertBuilder.WriteString(",") selectOrDeleteBuilder.WriteString(",") } - insertBuilder.WriteString("(?, ?, ?, ?)") + insertBuilder.WriteString("(?, ?, ?, ?, ?)") selectOrDeleteBuilder.WriteString("(?, ?)") selectParams = append(selectParams, pl.ProfileUUID, pl.LabelName) - insertParams = append(insertParams, pl.ProfileUUID, pl.LabelID, pl.LabelName, pl.Exclude) + insertParams = append(insertParams, pl.ProfileUUID, pl.LabelID, pl.LabelName, pl.Exclude, pl.RequireAll) deleteParams = append(deleteParams, pl.ProfileUUID, pl.LabelID) setProfileUUIDs[pl.ProfileUUID] = struct{}{} diff --git a/server/datastore/mysql/microsoft_mdm.go b/server/datastore/mysql/microsoft_mdm.go index 112d60b868..38563d6baa 100644 --- a/server/datastore/mysql/microsoft_mdm.go +++ b/server/datastore/mysql/microsoft_mdm.go @@ -1571,13 +1571,22 @@ INSERT INTO } } - labels := make([]fleet.ConfigurationProfileLabel, 0, len(cp.LabelsIncludeAll)+len(cp.LabelsExcludeAny)) + labels := make([]fleet.ConfigurationProfileLabel, 0, len(cp.LabelsIncludeAll)+len(cp.LabelsIncludeAny)+len(cp.LabelsExcludeAny)) for i := range cp.LabelsIncludeAll { cp.LabelsIncludeAll[i].ProfileUUID = profileUUID + cp.LabelsIncludeAll[i].RequireAll = true + cp.LabelsIncludeAll[i].Exclude = false + labels = append(labels, cp.LabelsIncludeAll[i]) + } + for i := range cp.LabelsIncludeAny { + cp.LabelsIncludeAny[i].ProfileUUID = profileUUID + cp.LabelsIncludeAny[i].RequireAll = false + cp.LabelsIncludeAny[i].Exclude = false labels = append(labels, cp.LabelsIncludeAll[i]) } for i := range cp.LabelsExcludeAny { cp.LabelsExcludeAny[i].ProfileUUID = profileUUID + cp.LabelsExcludeAny[i].RequireAll = false cp.LabelsExcludeAny[i].Exclude = true labels = append(labels, cp.LabelsExcludeAny[i]) } diff --git a/server/fleet/apple_mdm.go b/server/fleet/apple_mdm.go index 84ab0f7636..7c40b3df7e 100644 --- a/server/fleet/apple_mdm.go +++ b/server/fleet/apple_mdm.go @@ -201,6 +201,7 @@ type MDMAppleConfigProfile struct { // Checksum is an MD5 hash of the Mobileconfig bytes Checksum []byte `db:"checksum" json:"checksum,omitempty"` LabelsIncludeAll []ConfigurationProfileLabel `db:"-" json:"labels_include_all,omitempty"` + LabelsIncludeAny []ConfigurationProfileLabel `db:"-" json:"labels_include_any,omitempty"` LabelsExcludeAny []ConfigurationProfileLabel `db:"-" json:"labels_exclude_any,omitempty"` CreatedAt time.Time `db:"created_at" json:"created_at"` UploadedAt time.Time `db:"uploaded_at" json:"updated_at"` // NOTE: JSON field is still `updated_at` for historical reasons, would be an API breaking change @@ -223,7 +224,8 @@ type ConfigurationProfileLabel struct { LabelName string `db:"label_name" json:"name"` LabelID uint `db:"label_id" json:"id,omitempty"` // omitted if 0 (which is impossible if the label is not broken) Broken bool `db:"broken" json:"broken,omitempty"` // omitted (not rendered to JSON) if false - Exclude bool `db:"exclude" json:"-"` // not rendered in JSON, used to store the profile in LabelsIncludeAll or LabelsExcludeAny on the parent profile + Exclude bool `db:"exclude" json:"-"` // not rendered in JSON, used to store the profile in LabelsIncludeAll, LabelsIncludeAny, or LabelsExcludeAny on the parent profile + RequireAll bool `db:"require_all" json:"-"` // not rendered in JSON, used to store the profile in LabelsIncludeAll, LabelsIncludeAny, or LabelsIncludeAny on the parent profile } func NewMDMAppleConfigProfile(raw []byte, teamID *uint) (*MDMAppleConfigProfile, error) { @@ -599,6 +601,7 @@ type MDMAppleDeclaration struct { // labels associated with this Declaration LabelsIncludeAll []ConfigurationProfileLabel `db:"-" json:"labels_include_all,omitempty"` + LabelsIncludeAny []ConfigurationProfileLabel `db:"-" json:"labels_include_any,omitempty"` LabelsExcludeAny []ConfigurationProfileLabel `db:"-" json:"labels_exclude_any,omitempty"` CreatedAt time.Time `db:"created_at" json:"created_at"` diff --git a/server/fleet/apple_mdm_test.go b/server/fleet/apple_mdm_test.go index 240c37c9d7..924e51aa87 100644 --- a/server/fleet/apple_mdm_test.go +++ b/server/fleet/apple_mdm_test.go @@ -479,7 +479,6 @@ func TestMDMAppleHostDeclarationEqual(t *testing.T) { items[0].Status = nil items[1].Status = nil assert.True(t, items[0].Equal(items[1])) - } func TestMDMAppleProfilePayloadEqual(t *testing.T) { @@ -559,7 +558,6 @@ func TestMDMAppleProfilePayloadEqual(t *testing.T) { items[0].Checksum = nil items[1].Checksum = nil assert.True(t, items[0].Equal(items[1])) - } func TestConfigurationProfileLabelEqual(t *testing.T) { @@ -608,9 +606,10 @@ func TestConfigurationProfileLabelEqual(t *testing.T) { fieldsInEqualMethod++ items[1].Exclude = items[0].Exclude fieldsInEqualMethod++ + items[1].RequireAll = items[0].RequireAll + fieldsInEqualMethod++ assert.Equal(t, fieldsInEqualMethod, numberOfFields, "Does cmp.Equal for ConfigurationProfileLabel needs to be updated for new/updated field(s)?") assert.True(t, cmp.Equal(items[0], items[1])) - } diff --git a/server/fleet/mdm.go b/server/fleet/mdm.go index d6c2a2d000..79503b81ba 100644 --- a/server/fleet/mdm.go +++ b/server/fleet/mdm.go @@ -641,6 +641,14 @@ func MDMProfileSpecsMatch(a, b []MDMProfileSpec) bool { return len(pathLabelIncludeCounts) == 0 && len(pathLabelExcludeCounts) == 0 } +type MDMLabelsMode string + +const ( + LabelsIncludeAll MDMLabelsMode = "labels_include_all" + LabelsIncludeAny MDMLabelsMode = "labels_include_any" + LabelsExcludeAny MDMLabelsMode = "labels_exclude_any" +) + type MDMAssetName string const ( @@ -741,9 +749,11 @@ func FilterMacOSOnlyProfilesFromIOSIPadOS(profiles []*MDMAppleProfilePayload) [] } // RefetchBaseCommandUUIDPrefix and below command prefixes are the prefixes used for MDM commands used to refetch information from iOS/iPadOS devices. -const RefetchBaseCommandUUIDPrefix = "REFETCH-" -const RefetchDeviceCommandUUIDPrefix = RefetchBaseCommandUUIDPrefix + "DEVICE-" -const RefetchAppsCommandUUIDPrefix = RefetchBaseCommandUUIDPrefix + "APPS-" +const ( + RefetchBaseCommandUUIDPrefix = "REFETCH-" + RefetchDeviceCommandUUIDPrefix = RefetchBaseCommandUUIDPrefix + "DEVICE-" + RefetchAppsCommandUUIDPrefix = RefetchBaseCommandUUIDPrefix + "APPS-" +) // VPPTokenInfo is the representation of the VPP token that we send out via API. type VPPTokenInfo struct { diff --git a/server/fleet/service.go b/server/fleet/service.go index e7f70e8072..04ad58c874 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -758,9 +758,9 @@ type Service interface { GetHostDEPAssignment(ctx context.Context, host *Host) (*HostDEPAssignment, error) // NewMDMAppleConfigProfile creates a new configuration profile for the specified team. - NewMDMAppleConfigProfile(ctx context.Context, teamID uint, r io.Reader, labels []string, labelsExcludeMode bool) (*MDMAppleConfigProfile, error) + NewMDMAppleConfigProfile(ctx context.Context, teamID uint, r io.Reader, labels []string, labelsMembershipMode MDMLabelsMode) (*MDMAppleConfigProfile, error) // NewMDMAppleConfigProfileWithPayload creates a new declaration for the specified team. - NewMDMAppleDeclaration(ctx context.Context, teamID uint, r io.Reader, labels []string, name string, labelsExcludeMode bool) (*MDMAppleDeclaration, error) + NewMDMAppleDeclaration(ctx context.Context, teamID uint, r io.Reader, labels []string, name string, labelsMembershipMode MDMLabelsMode) (*MDMAppleDeclaration, error) // GetMDMAppleConfigProfileByDeprecatedID retrieves the specified Apple // configuration profile via its numeric ID. This method is deprecated and @@ -1032,7 +1032,7 @@ type Service interface { // NewMDMWindowsConfigProfile creates a new Windows configuration profile for // the specified team. - NewMDMWindowsConfigProfile(ctx context.Context, teamID uint, profileName string, r io.Reader, labels []string, labelsExcludeMode bool) (*MDMWindowsConfigProfile, error) + NewMDMWindowsConfigProfile(ctx context.Context, teamID uint, profileName string, r io.Reader, labels []string, labelsMembershipMode MDMLabelsMode) (*MDMWindowsConfigProfile, error) // NewMDMUnsupportedConfigProfile is called when a profile with an // unsupported extension is uploaded. diff --git a/server/fleet/windows_mdm.go b/server/fleet/windows_mdm.go index acc90b3a51..98198c4084 100644 --- a/server/fleet/windows_mdm.go +++ b/server/fleet/windows_mdm.go @@ -37,6 +37,7 @@ type MDMWindowsConfigProfile struct { Name string `db:"name" json:"name"` SyncML []byte `db:"syncml" json:"-"` LabelsIncludeAll []ConfigurationProfileLabel `db:"-" json:"labels_include_all,omitempty"` + LabelsIncludeAny []ConfigurationProfileLabel `db:"-" json:"labels_include_any,omitempty"` LabelsExcludeAny []ConfigurationProfileLabel `db:"-" json:"labels_exclude_any,omitempty"` CreatedAt time.Time `db:"created_at" json:"created_at"` UploadedAt time.Time `db:"uploaded_at" json:"updated_at"` // NOTE: JSON field is still `updated_at` for historical reasons, would be an API breaking change diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index 03636bf91f..b815fbf815 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -342,7 +342,7 @@ func newMDMAppleConfigProfileEndpoint(ctx context.Context, request interface{}, } defer ff.Close() // providing an empty set of labels since this endpoint is only maintained for backwards compat - cp, err := svc.NewMDMAppleConfigProfile(ctx, req.TeamID, ff, nil, false) + cp, err := svc.NewMDMAppleConfigProfile(ctx, req.TeamID, ff, nil, fleet.LabelsIncludeAll) if err != nil { return &newMDMAppleConfigProfileResponse{Err: err}, nil } @@ -351,7 +351,7 @@ func newMDMAppleConfigProfileEndpoint(ctx context.Context, request interface{}, }, nil } -func (svc *Service) NewMDMAppleConfigProfile(ctx context.Context, teamID uint, r io.Reader, labels []string, labelsExcludeMode bool) (*fleet.MDMAppleConfigProfile, error) { +func (svc *Service) NewMDMAppleConfigProfile(ctx context.Context, teamID uint, r io.Reader, labels []string, labelsMembershipMode fleet.MDMLabelsMode) (*fleet.MDMAppleConfigProfile, error) { if err := svc.authz.Authorize(ctx, &fleet.MDMConfigProfileAuthz{TeamID: &teamID}, fleet.ActionWrite); err != nil { return nil, ctxerr.Wrap(ctx, err) } @@ -395,10 +395,15 @@ func (svc *Service) NewMDMAppleConfigProfile(ctx context.Context, teamID uint, r if err != nil { return nil, ctxerr.Wrap(ctx, err, "validating labels") } - if labelsExcludeMode { - cp.LabelsExcludeAny = labelMap - } else { + switch labelsMembershipMode { + case fleet.LabelsIncludeAll: cp.LabelsIncludeAll = labelMap + case fleet.LabelsIncludeAny: + cp.LabelsIncludeAny = labelMap + case fleet.LabelsExcludeAny: + cp.LabelsExcludeAny = labelMap + default: + // TODO what happens if mode is not set?s } err = validateConfigProfileFleetVariables(string(cp.Mobileconfig)) if err != nil { @@ -456,7 +461,7 @@ func validateConfigProfileFleetVariables(contents string) error { return nil } -func (svc *Service) NewMDMAppleDeclaration(ctx context.Context, teamID uint, r io.Reader, labels []string, name string, labelsExcludeMode bool) (*fleet.MDMAppleDeclaration, error) { +func (svc *Service) NewMDMAppleDeclaration(ctx context.Context, teamID uint, r io.Reader, labels []string, name string, labelsMembershipMode fleet.MDMLabelsMode) (*fleet.MDMAppleDeclaration, error) { if err := svc.authz.Authorize(ctx, &fleet.MDMConfigProfileAuthz{TeamID: &teamID}, fleet.ActionWrite); err != nil { return nil, ctxerr.Wrap(ctx, err) } @@ -514,9 +519,13 @@ func (svc *Service) NewMDMAppleDeclaration(ctx context.Context, teamID uint, r i d := fleet.NewMDMAppleDeclaration(data, tmID, name, rawDecl.Type, rawDecl.Identifier) - if labelsExcludeMode { + switch labelsMembershipMode { + case fleet.LabelsIncludeAny: + d.LabelsIncludeAny = validatedLabels + case fleet.LabelsExcludeAny: d.LabelsExcludeAny = validatedLabels - } else { + default: + // default to include all d.LabelsIncludeAll = validatedLabels } diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go index 8f159535c7..6f9f98db25 100644 --- a/server/service/apple_mdm_test.go +++ b/server/service/apple_mdm_test.go @@ -216,7 +216,8 @@ func setupAppleMDMService(t *testing.T, license *fleet.LicenseInfo) (fleet.Servi certPEM := tokenpki.PEMCertificate(crt.Raw) keyPEM := tokenpki.PEMRSAPrivateKey(key) ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, - _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + _ sqlx.QueryerContext, + ) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { return map[fleet.MDMAssetName]fleet.MDMConfigAsset{ fleet.MDMAssetAPNSCert: {Value: apnsCert}, fleet.MDMAssetAPNSKey: {Value: apnsKey}, @@ -672,11 +673,11 @@ func TestMDMAppleConfigProfileAuthz(t *testing.T) { t.Run(tt.name, func(t *testing.T) { // test authz create new profile (no team) - _, err := svc.NewMDMAppleConfigProfile(ctx, 0, bytes.NewReader(mcBytes), nil, false) + _, err := svc.NewMDMAppleConfigProfile(ctx, 0, bytes.NewReader(mcBytes), nil, fleet.LabelsIncludeAll) checkShouldFail(err, tt.shouldFailGlobal) // test authz create new profile (team 1) - _, err = svc.NewMDMAppleConfigProfile(ctx, 1, bytes.NewReader(mcBytes), nil, false) + _, err = svc.NewMDMAppleConfigProfile(ctx, 1, bytes.NewReader(mcBytes), nil, fleet.LabelsIncludeAll) checkShouldFail(err, tt.shouldFailTeam) // test authz list profiles (no team) @@ -740,7 +741,7 @@ func TestNewMDMAppleConfigProfile(t *testing.T) { return fleet.MDMProfilesUpdates{}, nil } - cp, err := svc.NewMDMAppleConfigProfile(ctx, 0, r, nil, false) + cp, err := svc.NewMDMAppleConfigProfile(ctx, 0, r, nil, fleet.LabelsIncludeAll) require.NoError(t, err) require.Equal(t, "Foo", cp.Name) assert.Equal(t, identifier, cp.Identifier) @@ -749,7 +750,7 @@ func TestNewMDMAppleConfigProfile(t *testing.T) { // Unsupported Fleet variable mcBytes = mcBytesForTest("Foo", identifier, "UUID${FLEET_VAR_BOZO}") r = bytes.NewReader(mcBytes) - _, err = svc.NewMDMAppleConfigProfile(ctx, 0, r, nil, false) + _, err = svc.NewMDMAppleConfigProfile(ctx, 0, r, nil, fleet.LabelsIncludeAll) assert.ErrorContains(t, err, "Fleet variable") } @@ -781,7 +782,7 @@ func TestNewMDMAppleDeclaration(t *testing.T) { // Unsupported Fleet variable b := declBytesForTest("D1", "d1content $FLEET_VAR_BOZO") - _, err := svc.NewMDMAppleDeclaration(ctx, 0, bytes.NewReader(b), nil, "name", false) + _, err := svc.NewMDMAppleDeclaration(ctx, 0, bytes.NewReader(b), nil, "name", fleet.LabelsIncludeAll) assert.ErrorContains(t, err, "Fleet variable") ds.NewMDMAppleDeclarationFunc = func(ctx context.Context, d *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) { @@ -797,7 +798,7 @@ func TestNewMDMAppleDeclaration(t *testing.T) { // Good declaration b = declBytesForTest("D1", "d1content") - d, err := svc.NewMDMAppleDeclaration(ctx, 0, bytes.NewReader(b), nil, "name", false) + d, err := svc.NewMDMAppleDeclaration(ctx, 0, bytes.NewReader(b), nil, "name", fleet.LabelsIncludeAll) require.NoError(t, err) assert.NotNil(t, d) } @@ -2236,7 +2237,8 @@ func TestMDMAppleReconcileAppleProfiles(t *testing.T) { AppleSCEPKey: "./testdata/server.key", } ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, - _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + _ sqlx.QueryerContext, + ) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { _, pemCert, pemKey, err := mdmConfig.AppleSCEP() require.NoError(t, err) return map[fleet.MDMAssetName]fleet.MDMConfigAsset{ @@ -2344,7 +2346,8 @@ func TestMDMAppleReconcileAppleProfiles(t *testing.T) { return false, nil } mdmStorage.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, - _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + _ sqlx.QueryerContext, + ) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { certPEM, err := os.ReadFile("./testdata/server.pem") require.NoError(t, err) keyPEM, err := os.ReadFile("./testdata/server.key") @@ -2860,7 +2863,8 @@ func TestPreprocessProfileContents(t *testing.T) { ndesPassword := "test-password" ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, - assetNames []fleet.MDMAssetName, _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + assetNames []fleet.MDMAssetName, _ sqlx.QueryerContext, + ) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { return map[fleet.MDMAssetName]fleet.MDMConfigAsset{ fleet.MDMAssetNDESPassword: {Value: []byte(ndesPassword)}, }, nil @@ -3033,8 +3037,10 @@ func TestPreprocessProfileContents(t *testing.T) { "p3": []byte("no variables"), } addProfileToInstall := func(hostUUID, profileUUID, profileIdentifier string) { - hostProfilesToInstallMap[hostProfileUUID{HostUUID: hostUUID, - ProfileUUID: profileUUID}] = &fleet.MDMAppleBulkUpsertHostProfilePayload{ + hostProfilesToInstallMap[hostProfileUUID{ + HostUUID: hostUUID, + ProfileUUID: profileUUID, + }] = &fleet.MDMAppleBulkUpsertHostProfilePayload{ ProfileUUID: profileUUID, ProfileIdentifier: profileIdentifier, HostUUID: hostUUID, @@ -3042,7 +3048,6 @@ func TestPreprocessProfileContents(t *testing.T) { Status: &fleet.MDMDeliveryPending, CommandUUID: cmdUUID, } - } addProfileToInstall(hostUUID, "p1", "com.add.profile") addProfileToInstall("host-2", "p1", "com.add.profile") @@ -3594,7 +3599,8 @@ func setupTest(t *testing.T) (context.Context, kitlog.Logger, *mock.Store, *conf _, pemCert, pemKey, err := mdmConfig.AppleSCEP() require.NoError(t, err) ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, - _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + _ sqlx.QueryerContext, + ) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { return map[fleet.MDMAssetName]fleet.MDMConfigAsset{ fleet.MDMAssetCACert: {Value: pemCert}, fleet.MDMAssetCAKey: {Value: pemKey}, @@ -3603,7 +3609,8 @@ func setupTest(t *testing.T) (context.Context, kitlog.Logger, *mock.Store, *conf }, nil } mdmStorage.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, - _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + _ sqlx.QueryerContext, + ) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { return map[fleet.MDMAssetName]fleet.MDMConfigAsset{ fleet.MDMAssetCACert: {Value: pemCert}, fleet.MDMAssetCAKey: {Value: pemKey}, diff --git a/server/service/integration_mdm_profiles_test.go b/server/service/integration_mdm_profiles_test.go index a9a5ad60aa..ea669a0826 100644 --- a/server/service/integration_mdm_profiles_test.go +++ b/server/service/integration_mdm_profiles_test.go @@ -2850,9 +2850,9 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() { assertWindowsProfile("win-profile-with-labels.xml", "./Test", 0, []string{"does-not-exist", "bar"}, http.StatusBadRequest, "some or all the labels provided don't exist") // profiles with invalid mix of labels - assertAppleProfile("apple-invalid-profile-with-labels.mobileconfig", "apple-invalid-profile-with-labels", "ident-with-labels", 0, []string{"foo", "!bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all" or "labels" can be included.`) - assertAppleDeclaration("apple-invalid-decl-with-labels.json", "ident-decl-with-labels", 0, []string{"foo", "-bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all" or "labels" can be included.`) - assertWindowsProfile("win-invalid-profile-with-labels.xml", "./Test", 0, []string{"-foo", "!bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all" or "labels" can be included.`) + assertAppleProfile("apple-invalid-profile-with-labels.mobileconfig", "apple-invalid-profile-with-labels", "ident-with-labels", 0, []string{"foo", "!bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`) + assertAppleDeclaration("apple-invalid-decl-with-labels.json", "ident-decl-with-labels", 0, []string{"foo", "-bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`) + assertWindowsProfile("win-invalid-profile-with-labels.xml", "./Test", 0, []string{"-foo", "!bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`) // profiles with valid labels uuidAppleWithLabel := assertAppleProfile("apple-profile-with-labels.mobileconfig", "apple-profile-with-labels", "ident-with-labels", 0, []string{"!foo"}, http.StatusOK, "") diff --git a/server/service/mdm.go b/server/service/mdm.go index 90da395ceb..d8ceafd47e 100644 --- a/server/service/mdm.go +++ b/server/service/mdm.go @@ -1209,6 +1209,7 @@ type newMDMConfigProfileRequest struct { TeamID uint Profile *multipart.FileHeader LabelsIncludeAll []string + LabelsIncludeAny []string LabelsExcludeAny []string } @@ -1248,21 +1249,22 @@ func (newMDMConfigProfileRequest) DecodeRequest(ctx context.Context, r *http.Req } // add labels - var existsIncl, existsExcl, existsDepr bool + var existsInclAll, existsInclAny, existsExclAny, existsDepr bool var deprecatedLabels []string - decoded.LabelsIncludeAll, existsIncl = r.MultipartForm.Value["labels_include_all"] - decoded.LabelsExcludeAny, existsExcl = r.MultipartForm.Value["labels_exclude_any"] + decoded.LabelsIncludeAll, existsInclAll = r.MultipartForm.Value[string(fleet.LabelsIncludeAll)] + decoded.LabelsIncludeAny, existsInclAny = r.MultipartForm.Value[string(fleet.LabelsIncludeAny)] + decoded.LabelsExcludeAny, existsExclAny = r.MultipartForm.Value[string(fleet.LabelsExcludeAny)] deprecatedLabels, existsDepr = r.MultipartForm.Value["labels"] // validate that only one of the labels type is provided var count int - for _, b := range []bool{existsIncl, existsExcl, existsDepr} { + for _, b := range []bool{existsInclAll, existsInclAny, existsExclAny, existsDepr} { if b { count++ } } if count > 1 { - return nil, &fleet.BadRequestError{Message: `Only one of "labels_exclude_any", "labels_include_all" or "labels" can be included.`} + return nil, &fleet.BadRequestError{Message: `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`} } if existsDepr { decoded.LabelsIncludeAll = deprecatedLabels @@ -1292,17 +1294,25 @@ func newMDMConfigProfileEndpoint(ctx context.Context, request interface{}, svc f isMobileConfig := strings.EqualFold(fileExt, ".mobileconfig") isJSON := strings.EqualFold(fileExt, ".json") - labels := req.LabelsIncludeAll - excludeMode := false - if len(req.LabelsExcludeAny) > 0 { + var labels []string + var labelsMode fleet.MDMLabelsMode + switch { + case len(req.LabelsIncludeAny) > 0: + labels = req.LabelsIncludeAny + labelsMode = fleet.LabelsIncludeAny + case len(req.LabelsExcludeAny) > 0: labels = req.LabelsExcludeAny - excludeMode = true + labelsMode = fleet.LabelsExcludeAny + default: + // TODO: should this be the default? + labels = req.LabelsIncludeAll + labelsMode = fleet.LabelsIncludeAll } if isMobileConfig || isJSON { // Then it's an Apple configuration file if isJSON { - decl, err := svc.NewMDMAppleDeclaration(ctx, req.TeamID, ff, labels, profileName, excludeMode) + decl, err := svc.NewMDMAppleDeclaration(ctx, req.TeamID, ff, labels, profileName, labelsMode) if err != nil { return &newMDMConfigProfileResponse{Err: err}, nil } @@ -1313,7 +1323,7 @@ func newMDMConfigProfileEndpoint(ctx context.Context, request interface{}, svc f } - cp, err := svc.NewMDMAppleConfigProfile(ctx, req.TeamID, ff, labels, excludeMode) + cp, err := svc.NewMDMAppleConfigProfile(ctx, req.TeamID, ff, labels, labelsMode) if err != nil { return &newMDMConfigProfileResponse{Err: err}, nil } @@ -1323,7 +1333,7 @@ func newMDMConfigProfileEndpoint(ctx context.Context, request interface{}, svc f } if isWindows := strings.EqualFold(fileExt, ".xml"); isWindows { - cp, err := svc.NewMDMWindowsConfigProfile(ctx, req.TeamID, profileName, ff, labels, excludeMode) + cp, err := svc.NewMDMWindowsConfigProfile(ctx, req.TeamID, profileName, ff, labels, labelsMode) if err != nil { return &newMDMConfigProfileResponse{Err: err}, nil } @@ -1347,7 +1357,7 @@ func (svc *Service) NewMDMUnsupportedConfigProfile(ctx context.Context, teamID u return &fleet.BadRequestError{Message: "Couldn't add profile. The file should be a .mobileconfig, XML, or JSON file."} } -func (svc *Service) NewMDMWindowsConfigProfile(ctx context.Context, teamID uint, profileName string, r io.Reader, labels []string, labelsExcludeMode bool) (*fleet.MDMWindowsConfigProfile, error) { +func (svc *Service) NewMDMWindowsConfigProfile(ctx context.Context, teamID uint, profileName string, r io.Reader, labels []string, labelsMembershipMode fleet.MDMLabelsMode) (*fleet.MDMWindowsConfigProfile, error) { if err := svc.authz.Authorize(ctx, &fleet.MDMConfigProfileAuthz{TeamID: &teamID}, fleet.ActionWrite); err != nil { return nil, ctxerr.Wrap(ctx, err) } @@ -1396,11 +1406,17 @@ func (svc *Service) NewMDMWindowsConfigProfile(ctx context.Context, teamID uint, if err != nil { return nil, ctxerr.Wrap(ctx, err, "validating labels") } - if labelsExcludeMode { - cp.LabelsExcludeAny = labelMap - } else { + switch labelsMembershipMode { + case fleet.LabelsIncludeAll: cp.LabelsIncludeAll = labelMap + case fleet.LabelsIncludeAny: + cp.LabelsIncludeAny = labelMap + case fleet.LabelsExcludeAny: + cp.LabelsExcludeAny = labelMap + default: + // TODO what happens if mode is not set?s } + err = validateWindowsProfileFleetVariables(string(cp.SyncML)) if err != nil { return nil, ctxerr.Wrap(ctx, err, "validating Windows profile") @@ -1678,7 +1694,8 @@ func (svc *Service) BatchSetMDMProfiles( } func validateFleetVariables(ctx context.Context, appleProfiles []*fleet.MDMAppleConfigProfile, - windowsProfiles []*fleet.MDMWindowsConfigProfile, appleDecls []*fleet.MDMAppleDeclaration) error { + windowsProfiles []*fleet.MDMWindowsConfigProfile, appleDecls []*fleet.MDMAppleDeclaration, +) error { var err error for _, p := range appleProfiles { err = validateConfigProfileFleetVariables(string(p.Mobileconfig)) diff --git a/server/service/mdm_test.go b/server/service/mdm_test.go index add3999bc6..2cf012a481 100644 --- a/server/service/mdm_test.go +++ b/server/service/mdm_test.go @@ -48,7 +48,8 @@ func TestGetMDMApple(t *testing.T) { require.NoError(t, err) ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, - _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + _ sqlx.QueryerContext, + ) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { return map[fleet.MDMAssetName]fleet.MDMConfigAsset{ fleet.MDMAssetAPNSCert: {Name: fleet.MDMAssetAPNSCert, Value: certPEM}, fleet.MDMAssetAPNSKey: {Name: fleet.MDMAssetAPNSKey, Value: keyPEM}, @@ -108,7 +109,8 @@ func TestMDMAppleAuthorization(t *testing.T) { } ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, - _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + _ sqlx.QueryerContext, + ) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { return map[fleet.MDMAssetName]fleet.MDMConfigAsset{}, nil } @@ -1116,11 +1118,11 @@ func TestMDMWindowsConfigProfileAuthz(t *testing.T) { checkShouldFail(t, err, tt.shouldFailTeamRead) // test authz create new profile (no team) - _, err = svc.NewMDMWindowsConfigProfile(ctx, 0, "prof", strings.NewReader(winProfContent), nil, false) + _, err = svc.NewMDMWindowsConfigProfile(ctx, 0, "prof", strings.NewReader(winProfContent), nil, fleet.LabelsIncludeAll) checkShouldFail(t, err, tt.shouldFailGlobalWrite) // test authz create new profile (team 1) - _, err = svc.NewMDMWindowsConfigProfile(ctx, 1, "prof", strings.NewReader(winProfContent), nil, false) + _, err = svc.NewMDMWindowsConfigProfile(ctx, 1, "prof", strings.NewReader(winProfContent), nil, fleet.LabelsIncludeAll) checkShouldFail(t, err, tt.shouldFailTeamWrite) // test authz delete config profile (no team) @@ -1205,7 +1207,7 @@ func TestUploadWindowsMDMConfigProfileValidations(t *testing.T) { }, nil } ctx = test.UserContext(ctx, test.UserAdmin) - _, err := svc.NewMDMWindowsConfigProfile(ctx, c.tmID, "foo", strings.NewReader(c.profile), nil, false) + _, err := svc.NewMDMWindowsConfigProfile(ctx, c.tmID, "foo", strings.NewReader(c.profile), nil, fleet.LabelsIncludeAll) if c.wantErr != "" { require.Error(t, err) require.ErrorContains(t, err, c.wantErr) From aa3fd29c133a33db3623d7c79d789ae7aaa0c073 Mon Sep 17 00:00:00 2001 From: Dante Catalfamo <43040593+dantecatalfamo@users.noreply.github.com> Date: Tue, 5 Nov 2024 15:13:44 -0500 Subject: [PATCH 4/7] Profile Labels Include Any CLI (#23434) #22576 Co-authored-by: Sarah Gillespie <73313222+gillespi314@users.noreply.github.com> --- changes/22576-labels-include-any-gitops | 1 + cmd/fleetctl/gitops_test.go | 6 +- ...ws_custom_settings_invalid_label_mix_2.yml | 96 ++++++++++ ...s_custom_settings_invalid_labels_mix_2.yml | 29 +++ ee/server/service/teams.go | 4 +- server/fleet/apple_mdm.go | 6 +- server/fleet/mdm.go | 36 +++- server/service/appconfig.go | 9 +- server/service/client.go | 2 + .../service/integration_mdm_profiles_test.go | 34 +++- server/service/mdm.go | 62 ++++++- server/service/mdm_test.go | 173 ++++++++++++++++++ .../generated_files/appconfig.txt | 1 + .../generated_files/mdmprofilespec.txt | 1 + .../cloner-check/generated_files/teammdm.txt | 1 + 15 files changed, 441 insertions(+), 20 deletions(-) create mode 100644 changes/22576-labels-include-any-gitops create mode 100644 cmd/fleetctl/testdata/gitops/global_windows_custom_settings_invalid_label_mix_2.yml create mode 100644 cmd/fleetctl/testdata/gitops/team_macos_windows_custom_settings_invalid_labels_mix_2.yml diff --git a/changes/22576-labels-include-any-gitops b/changes/22576-labels-include-any-gitops new file mode 100644 index 0000000000..228171c7d1 --- /dev/null +++ b/changes/22576-labels-include-any-gitops @@ -0,0 +1 @@ +- Add support for labels_include_any to gitops diff --git a/cmd/fleetctl/gitops_test.go b/cmd/fleetctl/gitops_test.go index 3056932205..b2b02feb94 100644 --- a/cmd/fleetctl/gitops_test.go +++ b/cmd/fleetctl/gitops_test.go @@ -2075,11 +2075,13 @@ func TestGitOpsCustomSettings(t *testing.T) { }{ {"testdata/gitops/global_macos_windows_custom_settings_valid.yml", ""}, {"testdata/gitops/global_macos_custom_settings_valid_deprecated.yml", ""}, - {"testdata/gitops/global_windows_custom_settings_invalid_label_mix.yml", `For each profile, only one of "labels_exclude_any", "labels_include_all" or "labels" can be included`}, + {"testdata/gitops/global_windows_custom_settings_invalid_label_mix.yml", `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included`}, + {"testdata/gitops/global_windows_custom_settings_invalid_label_mix_2.yml", `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included`}, {"testdata/gitops/global_windows_custom_settings_unknown_label.yml", `some or all the labels provided don't exist`}, {"testdata/gitops/team_macos_windows_custom_settings_valid.yml", ""}, {"testdata/gitops/team_macos_custom_settings_valid_deprecated.yml", ""}, - {"testdata/gitops/team_macos_windows_custom_settings_invalid_labels_mix.yml", `For each profile, only one of "labels_exclude_any", "labels_include_all" or "labels" can be included.`}, + {"testdata/gitops/team_macos_windows_custom_settings_invalid_labels_mix.yml", `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included`}, + {"testdata/gitops/team_macos_windows_custom_settings_invalid_labels_mix_2.yml", `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included`}, {"testdata/gitops/team_macos_windows_custom_settings_unknown_label.yml", `some or all the labels provided don't exist`}, } for _, c := range cases { diff --git a/cmd/fleetctl/testdata/gitops/global_windows_custom_settings_invalid_label_mix_2.yml b/cmd/fleetctl/testdata/gitops/global_windows_custom_settings_invalid_label_mix_2.yml new file mode 100644 index 0000000000..04fee3d457 --- /dev/null +++ b/cmd/fleetctl/testdata/gitops/global_windows_custom_settings_invalid_label_mix_2.yml @@ -0,0 +1,96 @@ +controls: + windows_settings: + custom_settings: + - path: ./lib/windows-screenlock.xml + labels_include_all: + - B + labels_include_any: + - C + scripts: + enable_disk_encryption: false + macos_migration: + enable: false + mode: "" + webhook_url: "" + macos_setup: + bootstrap_package: null + enable_end_user_authentication: false + macos_setup_assistant: null + macos_updates: + deadline: null + minimum_version: null + windows_enabled_and_configured: true + windows_updates: + deadline_days: null + grace_period_days: null +queries: +policies: +agent_options: + command_line_flags: + distributed_denylist_duration: 0 + config: + options: + disable_distributed: false + distributed_interval: 10 + distributed_plugin: tls + distributed_tls_max_attempts: 3 + logger_tls_endpoint: /api/v1/osquery/log + pack_delimiter: / +org_settings: + server_settings: + deferred_save_host: false + enable_analytics: true + live_query_disabled: false + query_report_cap: 2000 + query_reports_disabled: false + scripts_disabled: false + server_url: $FLEET_SERVER_URL + ai_features_disabled: true + org_info: + contact_url: https://fleetdm.com/company/contact + org_logo_url: "" + org_logo_url_light_background: "" + org_name: $ORG_NAME + smtp_settings: + authentication_method: authmethod_plain + authentication_type: authtype_username_password + configured: false + domain: "" + enable_smtp: false + enable_ssl_tls: true + enable_start_tls: true + password: "" + port: 587 + sender_address: "" + server: "" + user_name: "" + verify_ssl_certs: true + sso_settings: + enable_jit_provisioning: false + enable_jit_role_sync: false + enable_sso: true + enable_sso_idp_login: false + entity_id: https://saml.example.com/entityid + idp_image_url: "" + idp_name: MockSAML + issuer_uri: "" + metadata: "" + metadata_url: https://mocksaml.com/api/saml/metadata + integrations: + mdm: + webhook_settings: + fleet_desktop: + transparency_url: https://fleetdm.com/transparency + host_expiry_settings: + host_expiry_enabled: false + activity_expiry_settings: + activity_expiry_enabled: true + activity_expiry_window: 60 + features: + enable_host_users: true + enable_software_inventory: true + vulnerability_settings: + databases_path: "" + secrets: + - secret: ABC +software: diff --git a/cmd/fleetctl/testdata/gitops/team_macos_windows_custom_settings_invalid_labels_mix_2.yml b/cmd/fleetctl/testdata/gitops/team_macos_windows_custom_settings_invalid_labels_mix_2.yml new file mode 100644 index 0000000000..c38c9aff12 --- /dev/null +++ b/cmd/fleetctl/testdata/gitops/team_macos_windows_custom_settings_invalid_labels_mix_2.yml @@ -0,0 +1,29 @@ +name: "${TEST_TEAM_NAME}" +team_settings: + secrets: + - secret: "ABC" + features: + enable_host_users: true + enable_software_inventory: true + host_expiry_settings: + host_expiry_enabled: true + host_expiry_window: 30 +agent_options: +controls: + macos_settings: + custom_settings: + - path: ./lib/macos-password.mobileconfig + labels_include_any: + - A + labels: + - B + windows_settings: + custom_settings: + - path: ./lib/windows-screenlock.xml + labels_include_any: + - A + labels_exclude_any: + - C +policies: +queries: +software: diff --git a/ee/server/service/teams.go b/ee/server/service/teams.go index 0952b972f3..34d3b1f76c 100644 --- a/ee/server/service/teams.go +++ b/ee/server/service/teams.go @@ -1435,14 +1435,14 @@ func (svc *Service) editTeamFromSpec( func validateTeamCustomSettings(invalid *fleet.InvalidArgumentError, prefix string, customSettings []fleet.MDMProfileSpec) { for i, prof := range customSettings { count := 0 - for _, b := range []bool{len(prof.Labels) > 0, len(prof.LabelsIncludeAll) > 0, len(prof.LabelsExcludeAny) > 0} { + for _, b := range []bool{len(prof.Labels) > 0, len(prof.LabelsIncludeAll) > 0, len(prof.LabelsIncludeAny) > 0, len(prof.LabelsExcludeAny) > 0} { if b { count++ } } if count > 1 { invalid.Append(fmt.Sprintf("%s_settings.custom_settings", prefix), - fmt.Sprintf(`Couldn't edit %s_settings.custom_settings. For each profile, only one of "labels_exclude_any", "labels_include_all" or "labels" can be included.`, prefix)) + fmt.Sprintf(`Couldn't edit %s_settings.custom_settings. For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`, prefix)) } if len(prof.Labels) > 0 { customSettings[i].LabelsIncludeAll = customSettings[i].Labels diff --git a/server/fleet/apple_mdm.go b/server/fleet/apple_mdm.go index 7c40b3df7e..3cf0378e91 100644 --- a/server/fleet/apple_mdm.go +++ b/server/fleet/apple_mdm.go @@ -218,7 +218,11 @@ type MDMProfilesUpdates struct { // profiles and labels. // // NOTE: json representation of the fields is a bit awkward to match the -// required API response, as this struct is returned within profile responses. +// required API response, as this struct is returned within profile +// responses. +// +// NOTE The fields in this struct other than LabelName and LabelID +// MAY NOT BE SET CORRECTLY, dependong on where they're being ingested from. type ConfigurationProfileLabel struct { ProfileUUID string `db:"profile_uuid" json:"-"` LabelName string `db:"label_name" json:"name"` diff --git a/server/fleet/mdm.go b/server/fleet/mdm.go index 79503b81ba..00f98633a7 100644 --- a/server/fleet/mdm.go +++ b/server/fleet/mdm.go @@ -419,6 +419,7 @@ type MDMConfigProfilePayload struct { CreatedAt time.Time `json:"created_at" db:"created_at"` UploadedAt time.Time `json:"updated_at" db:"uploaded_at"` // NOTE: JSON field is still `updated_at` for historical reasons, would be an API breaking change LabelsIncludeAll []ConfigurationProfileLabel `json:"labels_include_all,omitempty" db:"-"` + LabelsIncludeAny []ConfigurationProfileLabel `json:"labels_include_any,omitempty" db:"-"` LabelsExcludeAny []ConfigurationProfileLabel `json:"labels_exclude_any,omitempty" db:"-"` } @@ -432,6 +433,7 @@ type MDMProfileBatchPayload struct { // LabelsIncludeAll. Labels []string `json:"labels,omitempty"` LabelsIncludeAll []string `json:"labels_include_all,omitempty"` + LabelsIncludeAny []string `json:"labels_include_any,omitempty"` LabelsExcludeAny []string `json:"labels_exclude_any,omitempty"` } @@ -448,6 +450,7 @@ func NewMDMConfigProfilePayloadFromWindows(cp *MDMWindowsConfigProfile) *MDMConf CreatedAt: cp.CreatedAt, UploadedAt: cp.UploadedAt, LabelsIncludeAll: cp.LabelsIncludeAll, + LabelsIncludeAny: cp.LabelsIncludeAny, LabelsExcludeAny: cp.LabelsExcludeAny, } } @@ -467,6 +470,7 @@ func NewMDMConfigProfilePayloadFromApple(cp *MDMAppleConfigProfile) *MDMConfigPr CreatedAt: cp.CreatedAt, UploadedAt: cp.UploadedAt, LabelsIncludeAll: cp.LabelsIncludeAll, + LabelsIncludeAny: cp.LabelsIncludeAny, LabelsExcludeAny: cp.LabelsExcludeAny, } } @@ -486,6 +490,7 @@ func NewMDMConfigProfilePayloadFromAppleDDM(decl *MDMAppleDeclaration) *MDMConfi CreatedAt: decl.CreatedAt, UploadedAt: decl.UploadedAt, LabelsIncludeAll: decl.LabelsIncludeAll, + LabelsIncludeAny: decl.LabelsIncludeAny, LabelsExcludeAny: decl.LabelsExcludeAny, } } @@ -504,6 +509,10 @@ type MDMProfileSpec struct { // of in order to receive the profile. It must be a member of all listed // labels. LabelsIncludeAll []string `json:"labels_include_all,omitempty"` + // LabelsIncludeAny is a list of label names that the host must be a member + // of in order to receive the profile. It may be a member of + // any listed labels. + LabelsIncludeAny []string `json:"labels_include_any,omitempty"` // LabelsExcludeAll is a list of label names that the host must not be a // member of in order to receive the profile. It must not be a member of any // of the listed labels. @@ -558,6 +567,10 @@ func (p *MDMProfileSpec) Copy() *MDMProfileSpec { clone.LabelsIncludeAll = make([]string, len(p.LabelsIncludeAll)) copy(clone.LabelsIncludeAll, p.LabelsIncludeAll) } + if len(p.LabelsIncludeAny) > 0 { + clone.LabelsIncludeAny = make([]string, len(p.LabelsIncludeAny)) + copy(clone.LabelsIncludeAny, p.LabelsIncludeAny) + } if len(p.LabelsExcludeAny) > 0 { clone.LabelsExcludeAny = make([]string, len(p.LabelsExcludeAny)) copy(clone.LabelsExcludeAny, p.LabelsExcludeAny) @@ -591,6 +604,10 @@ func MDMProfileSpecsMatch(a, b []MDMProfileSpec) bool { pathLabelIncludeCounts[v.Path] = labelCountMap(v.Labels) } } + pathLabelsIncludeAnyCounts := make(map[string]map[string]int) + for _, v := range a { + pathLabelsIncludeAnyCounts[v.Path] = labelCountMap(v.LabelsIncludeAny) + } pathLabelExcludeCounts := make(map[string]map[string]int) for _, v := range a { pathLabelExcludeCounts[v.Path] = labelCountMap(v.LabelsExcludeAny) @@ -598,8 +615,9 @@ func MDMProfileSpecsMatch(a, b []MDMProfileSpec) bool { for _, v := range b { includeLabels, okIncl := pathLabelIncludeCounts[v.Path] + includeAnyLabels, okInclAny := pathLabelsIncludeAnyCounts[v.Path] excludeLabels, okExcl := pathLabelExcludeCounts[v.Path] - if !okIncl || !okExcl { + if !okIncl || !okExcl || !okInclAny { return false } @@ -621,6 +639,19 @@ func MDMProfileSpecsMatch(a, b []MDMProfileSpec) bool { } } + bLabelIncludeAnyCounts := labelCountMap(v.LabelsIncludeAny) + for label, count := range bLabelIncludeAnyCounts { + if includeAnyLabels[label] != count { + return false + } + includeAnyLabels[label] -= count + } + for _, count := range includeAnyLabels { + if count != 0 { + return false + } + } + bLabelExcludeCounts := labelCountMap(v.LabelsExcludeAny) for label, count := range bLabelExcludeCounts { if excludeLabels[label] != count { @@ -635,10 +666,11 @@ func MDMProfileSpecsMatch(a, b []MDMProfileSpec) bool { } delete(pathLabelIncludeCounts, v.Path) + delete(pathLabelsIncludeAnyCounts, v.Path) delete(pathLabelExcludeCounts, v.Path) } - return len(pathLabelIncludeCounts) == 0 && len(pathLabelExcludeCounts) == 0 + return len(pathLabelIncludeCounts) == 0 && len(pathLabelsIncludeAnyCounts) == 0 && len(pathLabelExcludeCounts) == 0 } type MDMLabelsMode string diff --git a/server/service/appconfig.go b/server/service/appconfig.go index 420a49ddb8..0e2bb43be4 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -956,14 +956,19 @@ func (svc *Service) validateMDM( checkCustomSettings := func(prefix string, customSettings []fleet.MDMProfileSpec) { for i, prof := range customSettings { count := 0 - for _, b := range []bool{len(prof.Labels) > 0, len(prof.LabelsIncludeAll) > 0, len(prof.LabelsExcludeAny) > 0} { + for _, b := range []bool{ + len(prof.Labels) > 0, + len(prof.LabelsIncludeAll) > 0, + len(prof.LabelsIncludeAny) > 0, + len(prof.LabelsExcludeAny) > 0, + } { if b { count++ } } if count > 1 { invalid.Append(fmt.Sprintf("%s_settings.custom_settings", prefix), - fmt.Sprintf(`Couldn't edit %s_settings.custom_settings. For each profile, only one of "labels_exclude_any", "labels_include_all" or "labels" can be included.`, prefix)) + fmt.Sprintf(`Couldn't edit %s_settings.custom_settings. For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`, prefix)) } if len(prof.Labels) > 0 { customSettings[i].LabelsIncludeAll = customSettings[i].Labels diff --git a/server/service/client.go b/server/service/client.go index 1e2c0179bb..e8a7a2defa 100644 --- a/server/service/client.go +++ b/server/service/client.go @@ -381,6 +381,7 @@ func getProfilesContents(baseDir string, macProfiles []fleet.MDMProfileSpec, win Contents: fileContents, Labels: profile.Labels, LabelsIncludeAll: profile.LabelsIncludeAll, + LabelsIncludeAny: profile.LabelsIncludeAny, LabelsExcludeAny: profile.LabelsExcludeAny, }) @@ -1086,6 +1087,7 @@ func extractAppCfgCustomSettings(appCfg interface{}, platformKey string) []fleet // validations are done later on in the Fleet API endpoint. profSpec.Labels = extractLabelField(m, "labels") profSpec.LabelsIncludeAll = extractLabelField(m, "labels_include_all") + profSpec.LabelsIncludeAny = extractLabelField(m, "labels_include_any") profSpec.LabelsExcludeAny = extractLabelField(m, "labels_exclude_any") if profSpec.Path != "" { diff --git a/server/service/integration_mdm_profiles_test.go b/server/service/integration_mdm_profiles_test.go index ea669a0826..6fdb066eba 100644 --- a/server/service/integration_mdm_profiles_test.go +++ b/server/service/integration_mdm_profiles_test.go @@ -1789,7 +1789,7 @@ func (s *integrationMDMTestSuite) TestAppConfigMDMCustomSettings() { } }`), http.StatusUnprocessableEntity) msg := extractServerErrorText(res.Body) - require.Contains(t, msg, `For each profile, only one of "labels_exclude_any", "labels_include_all" or "labels" can be included.`) + require.Contains(t, msg, `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`) res = s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ "mdm": { @@ -1801,7 +1801,31 @@ func (s *integrationMDMTestSuite) TestAppConfigMDMCustomSettings() { } }`), http.StatusUnprocessableEntity) msg = extractServerErrorText(res.Body) - require.Contains(t, msg, `For each profile, only one of "labels_exclude_any", "labels_include_all" or "labels" can be included.`) + require.Contains(t, msg, `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`) + + res = s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { + "windows_settings": { + "custom_settings": [ + {"path": "foo", "labels_include_any": ["a"], "labels_exclude_any": ["b"]} + ] + } + } + }`), http.StatusUnprocessableEntity) + msg = extractServerErrorText(res.Body) + require.Contains(t, msg, `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`) + + res = s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { + "windows_settings": { + "custom_settings": [ + {"path": "foo", "labels": ["a"], "labels_include_any": ["b"]} + ] + } + } + }`), http.StatusUnprocessableEntity) + msg = extractServerErrorText(res.Body) + require.Contains(t, msg, `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`) } func (s *integrationMDMTestSuite) TestApplyTeamsMDMAppleProfiles() { @@ -1920,7 +1944,7 @@ func (s *integrationMDMTestSuite) TestApplyTeamsMDMAppleProfiles() { }}} res = s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusUnprocessableEntity) errMsg = extractServerErrorText(res.Body) - assert.Contains(t, errMsg, `For each profile, only one of "labels_exclude_any", "labels_include_all" or "labels" can be included.`) + assert.Contains(t, errMsg, `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`) } func (s *integrationMDMTestSuite) TestBatchSetMDMAppleProfiles() { @@ -3781,7 +3805,7 @@ func (s *integrationMDMTestSuite) TestApplyTeamsMDMWindowsProfiles() { } `), http.StatusUnprocessableEntity) errMsg := extractServerErrorText(res.Body) - assert.Contains(t, errMsg, `For each profile, only one of "labels_exclude_any", "labels_include_all" or "labels" can be included.`) + assert.Contains(t, errMsg, `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`) } func (s *integrationMDMTestSuite) TestBatchSetMDMProfiles() { @@ -3982,7 +4006,7 @@ func (s *integrationMDMTestSuite) TestBatchSetMDMProfiles() { {Name: "N1", Contents: mobileconfigForTest("N1", "I1"), Labels: []string{lbl1.Name}, LabelsExcludeAny: []string{lbl2.Name}}, }}, http.StatusUnprocessableEntity) msg = extractServerErrorText(res.Body) - require.Contains(t, msg, `For each profile, only one of "labels_exclude_any", "labels_include_all" or "labels" can be included.`) + require.Contains(t, msg, `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`) // successful batch-set s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{ diff --git a/server/service/mdm.go b/server/service/mdm.go index d8ceafd47e..021d67f2c4 100644 --- a/server/service/mdm.go +++ b/server/service/mdm.go @@ -1602,6 +1602,7 @@ func (svc *Service) BatchSetMDMProfiles( profiles[i].Labels = nil } labels = append(labels, profiles[i].LabelsIncludeAll...) + labels = append(labels, profiles[i].LabelsIncludeAny...) labels = append(labels, profiles[i].LabelsExcludeAny...) } labelMap, err := svc.batchValidateProfileLabels(ctx, labels) @@ -1853,12 +1854,22 @@ func getAppleProfiles( mdmDecl := fleet.NewMDMAppleDeclaration(prof.Contents, tmID, prof.Name, rawDecl.Type, rawDecl.Identifier) for _, labelName := range prof.LabelsIncludeAll { + if lbl, ok := labelMap[labelName]; ok { + declLabel := fleet.ConfigurationProfileLabel{ + LabelName: lbl.LabelName, + LabelID: lbl.LabelID, + RequireAll: true, + } + mdmDecl.LabelsIncludeAll = append(mdmDecl.LabelsIncludeAll, declLabel) + } + } + for _, labelName := range prof.LabelsIncludeAny { if lbl, ok := labelMap[labelName]; ok { declLabel := fleet.ConfigurationProfileLabel{ LabelName: lbl.LabelName, LabelID: lbl.LabelID, } - mdmDecl.LabelsIncludeAll = append(mdmDecl.LabelsIncludeAll, declLabel) + mdmDecl.LabelsIncludeAny = append(mdmDecl.LabelsIncludeAny, declLabel) } } for _, labelName := range prof.LabelsExcludeAny { @@ -1905,12 +1916,31 @@ func getAppleProfiles( for _, labelName := range prof.LabelsIncludeAll { if lbl, ok := labelMap[labelName]; ok { - mdmProf.LabelsIncludeAll = append(mdmProf.LabelsIncludeAll, lbl) + mdmLabel := fleet.ConfigurationProfileLabel{ + LabelName: lbl.LabelName, + LabelID: lbl.LabelID, + RequireAll: true, + } + mdmProf.LabelsIncludeAll = append(mdmProf.LabelsIncludeAll, mdmLabel) + } + } + for _, labelName := range prof.LabelsIncludeAny { + if lbl, ok := labelMap[labelName]; ok { + mdmLabel := fleet.ConfigurationProfileLabel{ + LabelName: lbl.LabelName, + LabelID: lbl.LabelID, + } + mdmProf.LabelsIncludeAny = append(mdmProf.LabelsIncludeAny, mdmLabel) } } for _, labelName := range prof.LabelsExcludeAny { if lbl, ok := labelMap[labelName]; ok { - mdmProf.LabelsExcludeAny = append(mdmProf.LabelsExcludeAny, lbl) + mdmLabel := fleet.ConfigurationProfileLabel{ + LabelName: lbl.LabelName, + LabelID: lbl.LabelID, + Exclude: true, + } + mdmProf.LabelsExcludeAny = append(mdmProf.LabelsExcludeAny, mdmLabel) } } @@ -1981,12 +2011,31 @@ func getWindowsProfiles( } for _, labelName := range profile.LabelsIncludeAll { if lbl, ok := labelMap[labelName]; ok { - mdmProf.LabelsIncludeAll = append(mdmProf.LabelsIncludeAll, lbl) + mdmLabel := fleet.ConfigurationProfileLabel{ + LabelName: lbl.LabelName, + LabelID: lbl.LabelID, + RequireAll: true, + } + mdmProf.LabelsIncludeAll = append(mdmProf.LabelsIncludeAll, mdmLabel) + } + } + for _, labelName := range profile.LabelsIncludeAny { + if lbl, ok := labelMap[labelName]; ok { + mdmLabel := fleet.ConfigurationProfileLabel{ + LabelName: lbl.LabelName, + LabelID: lbl.LabelID, + } + mdmProf.LabelsIncludeAny = append(mdmProf.LabelsIncludeAny, mdmLabel) } } for _, labelName := range profile.LabelsExcludeAny { if lbl, ok := labelMap[labelName]; ok { - mdmProf.LabelsExcludeAny = append(mdmProf.LabelsExcludeAny, lbl) + mdmLabel := fleet.ConfigurationProfileLabel{ + LabelName: lbl.LabelName, + LabelID: lbl.LabelID, + Exclude: true, + } + mdmProf.LabelsExcludeAny = append(mdmProf.LabelsExcludeAny, mdmLabel) } } @@ -2020,6 +2069,7 @@ func validateProfiles(profiles []fleet.MDMProfileBatchPayload) error { var count int for _, b := range []bool{ len(profile.LabelsIncludeAll) > 0, + len(profile.LabelsIncludeAny) > 0, len(profile.LabelsExcludeAny) > 0, len(profile.Labels) > 0, } { @@ -2028,7 +2078,7 @@ func validateProfiles(profiles []fleet.MDMProfileBatchPayload) error { } } if count > 1 { - return fleet.NewInvalidArgumentError("mdm", `Couldn't edit custom_settings. For each profile, only one of "labels_exclude_any", "labels_include_all" or "labels" can be included.`) + return fleet.NewInvalidArgumentError("mdm", `Couldn't edit custom_settings. For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`) } if len(profile.Contents) > 1024*1024 { diff --git a/server/service/mdm_test.go b/server/service/mdm_test.go index 2cf012a481..4c66ad6669 100644 --- a/server/service/mdm_test.go +++ b/server/service/mdm_test.go @@ -20,6 +20,7 @@ import ( nanodep_client "github.com/fleetdm/fleet/v4/server/mdm/nanodep/client" nanodep_mock "github.com/fleetdm/fleet/v4/server/mock/nanodep" "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/assert" "github.com/fleetdm/fleet/v4/server/authz" "github.com/fleetdm/fleet/v4/server/config" @@ -1963,3 +1964,175 @@ func TestMDMResendConfigProfileAuthz(t *testing.T) { }) } } + +func TestBatchSetMDMProfilesLabels(t *testing.T) { + ds := new(mock.Store) + // while the config profiles are not premium-only, teams are and we want to test with teams. + license := &fleet.LicenseInfo{Tier: fleet.TierPremium} + svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true}) + _ = ctx + + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return &fleet.AppConfig{ + MDM: fleet.MDM{ + EnabledAndConfigured: true, + WindowsEnabledAndConfigured: true, + }, + }, nil + } + ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) { + return &fleet.Team{ + ID: tid, + Name: "team1", + }, nil + } + + type ProfileLabels struct { + IncludeAll bool + IncludeAny bool + ExcludeAny bool + } + + profileLabels := map[string]*ProfileLabels{} + + ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDeclarations []*fleet.MDMAppleDeclaration) (updates fleet.MDMProfilesUpdates, err error) { + for _, profile := range macProfiles { + profileLabels[profile.Name] = &ProfileLabels{} + if len(profile.LabelsIncludeAll) > 0 { + assert.True(t, profile.LabelsIncludeAll[0].RequireAll, "profile label missing RequireAll: %s", profile.Name) + assert.False(t, profile.LabelsIncludeAll[0].Exclude, "profile label shouldn't have Exclude: %s", profile.Name) + profileLabels[profile.Name].IncludeAll = true + } + if len(profile.LabelsIncludeAny) > 0 { + assert.False(t, profile.LabelsIncludeAny[0].RequireAll, "profile label shouldn't have RequireAll: %s", profile.Name) + assert.False(t, profile.LabelsIncludeAny[0].Exclude, "profile label shouldn't have Exclude: %s", profile.Name) + profileLabels[profile.Name].IncludeAny = true + } + if len(profile.LabelsExcludeAny) > 0 { + assert.False(t, profile.LabelsExcludeAny[0].RequireAll, "profile label shouldn't have RequireAll: %s", profile.Name) + assert.True(t, profile.LabelsExcludeAny[0].Exclude, "profile label should have Exclude: %s", profile.Name) + profileLabels[profile.Name].ExcludeAny = true + } + } + + for _, profile := range winProfiles { + profileLabels[profile.Name] = &ProfileLabels{} + if len(profile.LabelsIncludeAll) > 0 { + assert.True(t, profile.LabelsIncludeAll[0].RequireAll, "profile label missing RequireAll: %s", profile.Name) + assert.False(t, profile.LabelsIncludeAll[0].Exclude, "profile label shouldn't have Exclude: %s", profile.Name) + profileLabels[profile.Name].IncludeAll = true + } + if len(profile.LabelsIncludeAny) > 0 { + assert.False(t, profile.LabelsIncludeAny[0].RequireAll, "profile label shouldn't have RequireAll: %s", profile.Name) + assert.False(t, profile.LabelsIncludeAny[0].Exclude, "profile label shouldn't have Exclude: %s", profile.Name) + profileLabels[profile.Name].IncludeAny = true + } + if len(profile.LabelsExcludeAny) > 0 { + assert.False(t, profile.LabelsExcludeAny[0].RequireAll, "profile label shouldn't have RequireAll: %s", profile.Name) + assert.True(t, profile.LabelsExcludeAny[0].Exclude, "profile label should have Exclude: %s", profile.Name) + profileLabels[profile.Name].ExcludeAny = true + } + } + + for _, profile := range macDeclarations { + profileLabels[profile.Name] = &ProfileLabels{} + if len(profile.LabelsIncludeAll) > 0 { + assert.True(t, profile.LabelsIncludeAll[0].RequireAll, "profile label missing RequireAll: %s", profile.Name) + assert.False(t, profile.LabelsIncludeAll[0].Exclude, "profile label shouldn't have Exclude: %s", profile.Name) + profileLabels[profile.Name].IncludeAll = true + } + if len(profile.LabelsIncludeAny) > 0 { + assert.False(t, profile.LabelsIncludeAny[0].RequireAll, "profile label shouldn't have RequireAll: %s", profile.Name) + assert.False(t, profile.LabelsIncludeAny[0].Exclude, "profile label shouldn't have Exclude: %s", profile.Name) + profileLabels[profile.Name].IncludeAny = true + } + if len(profile.LabelsExcludeAny) > 0 { + assert.False(t, profile.LabelsExcludeAny[0].RequireAll, "profile label shouldn't have RequireAll: %s", profile.Name) + assert.True(t, profile.LabelsExcludeAny[0].Exclude, "profile label should have Exclude: %s", profile.Name) + profileLabels[profile.Name].ExcludeAny = true + } + } + + return fleet.MDMProfilesUpdates{}, nil + } + ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string) (updates fleet.MDMProfilesUpdates, err error) { + return fleet.MDMProfilesUpdates{}, nil + } + var labelID uint + ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) { + m := map[string]uint{} + for _, label := range labels { + labelID++ + m[label] = labelID + } + return m, nil + } + + profiles := []fleet.MDMProfileBatchPayload{ + // macOS + { + Name: "MIncAll", + Contents: mobileconfigForTest("MIncAll", "1"), + LabelsIncludeAll: []string{"a", "b"}, + }, + { + Name: "MIncAny", + Contents: mobileconfigForTest("MIncAny", "2"), + LabelsIncludeAny: []string{"a", "b"}, + }, + { + Name: "MExclAny", + Contents: mobileconfigForTest("MExclAny", "3"), + LabelsExcludeAny: []string{"a", "b"}, + }, + // Windows + { + Name: "WIncAll", + Contents: syncMLForTest("./Foo/Bar"), + LabelsIncludeAll: []string{"a", "b"}, + }, + { + Name: "WIncAny", + Contents: syncMLForTest("./Foo/Barz"), + LabelsIncludeAny: []string{"a", "b"}, + }, + { + Name: "WExclAny", + Contents: syncMLForTest("./Foo/Barf"), + LabelsExcludeAny: []string{"a", "b"}, + }, + // Declarative + { + Name: "DIncAll", + Contents: declarationForTest("DIncAll"), + LabelsIncludeAll: []string{"a", "b"}, + }, + { + Name: "DIncAny", + Contents: declarationForTest("DIncAny"), + LabelsIncludeAny: []string{"a", "b"}, + }, + { + Name: "DExclAny", + Contents: declarationForTest("DExclAny"), + LabelsExcludeAny: []string{"a", "b"}, + }, + } + + authCtx := test.UserContext(ctx, test.UserAdmin) + + err := svc.BatchSetMDMProfiles(authCtx, ptr.Uint(1), nil, profiles, false, false, ptr.Bool(true)) + require.NoError(t, err) + + assert.Equal(t, ProfileLabels{IncludeAll: true}, *profileLabels["MIncAll"]) + assert.Equal(t, ProfileLabels{IncludeAny: true}, *profileLabels["MIncAny"]) + assert.Equal(t, ProfileLabels{ExcludeAny: true}, *profileLabels["MExclAny"]) + + assert.Equal(t, ProfileLabels{IncludeAll: true}, *profileLabels["WIncAll"]) + assert.Equal(t, ProfileLabels{IncludeAny: true}, *profileLabels["WIncAny"]) + assert.Equal(t, ProfileLabels{ExcludeAny: true}, *profileLabels["WExclAny"]) + + assert.Equal(t, ProfileLabels{IncludeAll: true}, *profileLabels["DIncAll"]) + assert.Equal(t, ProfileLabels{IncludeAny: true}, *profileLabels["DIncAny"]) + assert.Equal(t, ProfileLabels{ExcludeAny: true}, *profileLabels["DExclAny"]) +} diff --git a/tools/cloner-check/generated_files/appconfig.txt b/tools/cloner-check/generated_files/appconfig.txt index dee903783d..8c2f6ba36b 100644 --- a/tools/cloner-check/generated_files/appconfig.txt +++ b/tools/cloner-check/generated_files/appconfig.txt @@ -136,6 +136,7 @@ github.com/fleetdm/fleet/v4/server/fleet/MacOSSettings CustomSettings []fleet.MD github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec Path string github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec Labels []string github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec LabelsIncludeAll []string +github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec LabelsIncludeAny []string github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec LabelsExcludeAny []string github.com/fleetdm/fleet/v4/server/fleet/MacOSSettings DeprecatedEnableDiskEncryption *bool github.com/fleetdm/fleet/v4/server/fleet/MDM MacOSSetup fleet.MacOSSetup diff --git a/tools/cloner-check/generated_files/mdmprofilespec.txt b/tools/cloner-check/generated_files/mdmprofilespec.txt index a4deffbbea..3c190ca329 100644 --- a/tools/cloner-check/generated_files/mdmprofilespec.txt +++ b/tools/cloner-check/generated_files/mdmprofilespec.txt @@ -1,4 +1,5 @@ github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec Path string github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec Labels []string github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec LabelsIncludeAll []string +github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec LabelsIncludeAny []string github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec LabelsExcludeAny []string diff --git a/tools/cloner-check/generated_files/teammdm.txt b/tools/cloner-check/generated_files/teammdm.txt index f21250550d..e05da07d94 100644 --- a/tools/cloner-check/generated_files/teammdm.txt +++ b/tools/cloner-check/generated_files/teammdm.txt @@ -18,6 +18,7 @@ github.com/fleetdm/fleet/v4/server/fleet/MacOSSettings CustomSettings []fleet.MD github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec Path string github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec Labels []string github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec LabelsIncludeAll []string +github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec LabelsIncludeAny []string github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec LabelsExcludeAny []string github.com/fleetdm/fleet/v4/server/fleet/MacOSSettings DeprecatedEnableDiskEncryption *bool github.com/fleetdm/fleet/v4/server/fleet/TeamMDM MacOSSetup fleet.MacOSSetup From 0095b3be89b98af92391850a896bdd7bb20974b9 Mon Sep 17 00:00:00 2001 From: Jahziel Villasana-Espinoza Date: Wed, 6 Nov 2024 09:13:37 -0500 Subject: [PATCH 5/7] feat: update profile reconciliation logic to handle include-any label relationships (#23535) > Related issue: #22581 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] 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/Committing-Changes.md#changes-files) for more information. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [x] Added/updated tests - [x] Manual QA for all new/changed functionality --------- Co-authored-by: gillespi314 <73313222+gillespi314@users.noreply.github.com> --- changes/22581-cron-updates | 1 + server/datastore/mysql/apple_mdm.go | 52 +++- server/datastore/mysql/apple_mdm_test.go | 255 +++++++++++++++++-- server/datastore/mysql/microsoft_mdm.go | 42 ++- server/datastore/mysql/microsoft_mdm_test.go | 165 +++++++++++- 5 files changed, 477 insertions(+), 38 deletions(-) create mode 100644 changes/22581-cron-updates diff --git a/changes/22581-cron-updates b/changes/22581-cron-updates new file mode 100644 index 0000000000..f228460a04 --- /dev/null +++ b/changes/22581-cron-updates @@ -0,0 +1 @@ +- Adds support for "include any" label/profile relationships to the profile reconciliation machinery. \ No newline at end of file diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go index 8574f40c61..459705aef4 100644 --- a/server/datastore/mysql/apple_mdm.go +++ b/server/datastore/mysql/apple_mdm.go @@ -1951,7 +1951,7 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB( ( hmap.profile_uuid IS NULL AND hmap.host_uuid IS NULL ) OR -- profiles in A and B but with operation type "remove" ( hmap.host_uuid IS NOT NULL AND ( hmap.operation_type = ? OR hmap.operation_type IS NULL ) ) -`, fmt.Sprintf(appleMDMProfilesDesiredStateQuery, "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)")) +`, fmt.Sprintf(appleMDMProfilesDesiredStateQuery, "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)")) // batches of 10K hosts because h.uuid appears three times in the // query, and the max number of prepared statements is 65K, this was @@ -1973,7 +1973,7 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB( batchUUIDs := uuids[start:end] - stmt, args, err := sqlx.In(toInstallStmt, batchUUIDs, batchUUIDs, batchUUIDs, fleet.MDMOperationTypeRemove) + stmt, args, err := sqlx.In(toInstallStmt, batchUUIDs, batchUUIDs, batchUUIDs, batchUUIDs, fleet.MDMOperationTypeRemove) if err != nil { return false, ctxerr.Wrapf(ctx, err, "building statement to select profiles to install, batch %d of %d", i, selectProfilesTotalBatches) @@ -2020,7 +2020,7 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB( mcpl.apple_profile_uuid = hmap.profile_uuid AND mcpl.label_id IS NULL ) -`, fmt.Sprintf(appleMDMProfilesDesiredStateQuery, "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)")) +`, fmt.Sprintf(appleMDMProfilesDesiredStateQuery, "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)")) var currentProfiles []*fleet.MDMAppleProfilePayload for i := 0; i < selectProfilesTotalBatches; i++ { @@ -2032,7 +2032,7 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB( batchUUIDs := uuids[start:end] - stmt, args, err := sqlx.In(toRemoveStmt, batchUUIDs, batchUUIDs, batchUUIDs, batchUUIDs, fleet.MDMOperationTypeRemove) + stmt, args, err := sqlx.In(toRemoveStmt, batchUUIDs, batchUUIDs, batchUUIDs, batchUUIDs, batchUUIDs, fleet.MDMOperationTypeRemove) if err != nil { return false, ctxerr.Wrap(ctx, err, "building profiles to remove statement") } @@ -2360,7 +2360,7 @@ func generateDesiredStateQuery(entityType string) string { JOIN nano_enrollments ne ON ne.device_id = h.uuid JOIN ${mdmEntityLabelsTable} mel - ON mel.${appleEntityUUIDColumn} = mae.${entityUUIDColumn} AND mel.exclude = 0 + ON mel.${appleEntityUUIDColumn} = mae.${entityUUIDColumn} AND mel.exclude = 0 AND mel.require_all = 1 LEFT OUTER JOIN label_membership lm ON lm.label_id = mel.label_id AND lm.host_id = h.id WHERE @@ -2394,7 +2394,7 @@ func generateDesiredStateQuery(entityType string) string { JOIN nano_enrollments ne ON ne.device_id = h.uuid JOIN ${mdmEntityLabelsTable} mel - ON mel.${appleEntityUUIDColumn} = mae.${entityUUIDColumn} AND mel.exclude = 1 + ON mel.${appleEntityUUIDColumn} = mae.${entityUUIDColumn} AND mel.exclude = 1 AND mel.require_all = 0 LEFT OUTER JOIN label_membership lm ON lm.label_id = mel.label_id AND lm.host_id = h.id WHERE @@ -2407,6 +2407,42 @@ func generateDesiredStateQuery(entityType string) string { HAVING -- considers only the profiles with labels, without any broken label, and with the host not in any label ${countEntityLabelsColumn} > 0 AND ${countEntityLabelsColumn} = count_non_broken_labels AND count_host_labels = 0 + + UNION + + -- label-based entities where the host is a member of any the labels (include-any). + -- by design, "include" labels cannot match if they are broken (the host cannot be + -- a member of a deleted label). + SELECT + mae.${entityUUIDColumn}, + h.uuid as host_uuid, + h.platform as host_platform, + mae.identifier as ${entityIdentifierColumn}, + mae.name as ${entityNameColumn}, + mae.checksum as checksum, + COUNT(*) as ${countEntityLabelsColumn}, + COUNT(mel.label_id) as count_non_broken_labels, + COUNT(lm.label_id) as count_host_labels + FROM + ${mdmAppleEntityTable} mae + JOIN hosts h + ON h.team_id = mae.team_id OR (h.team_id IS NULL AND mae.team_id = 0) + JOIN nano_enrollments ne + ON ne.device_id = h.uuid + JOIN ${mdmEntityLabelsTable} mel + ON mel.${appleEntityUUIDColumn} = mae.${entityUUIDColumn} AND mel.exclude = 0 AND mel.require_all = 0 + LEFT OUTER JOIN label_membership lm + ON lm.label_id = mel.label_id AND lm.host_id = h.id + WHERE + (h.platform = 'darwin' OR h.platform = 'ios' OR h.platform = 'ipados') AND + ne.enabled = 1 AND + ne.type = 'Device' AND + ( %s ) + GROUP BY + mae.${entityUUIDColumn}, h.uuid, h.platform, mae.identifier, mae.name, mae.checksum + HAVING + ${countEntityLabelsColumn} > 0 AND count_host_labels >= 1 + `, func(s string) string { return dynamicNames[s] }) } @@ -2465,7 +2501,7 @@ func generateEntitiesToInstallQuery(entityType string) string { ( hmae.host_uuid IS NOT NULL AND ( hmae.operation_type = ? OR hmae.operation_type IS NULL ) ) OR -- entities in A and B with operation type "install" and NULL status ( hmae.host_uuid IS NOT NULL AND hmae.operation_type = ? AND hmae.status IS NULL ) -`, func(s string) string { return dynamicNames[s] }), fmt.Sprintf(generateDesiredStateQuery(entityType), "TRUE", "TRUE", "TRUE")) +`, func(s string) string { return dynamicNames[s] }), fmt.Sprintf(generateDesiredStateQuery(entityType), "TRUE", "TRUE", "TRUE", "TRUE")) } // generateEntitiesToRemoveQuery is a set difference between: @@ -2518,7 +2554,7 @@ func generateEntitiesToRemoveQuery(entityType string) string { mcpl.${appleEntityUUIDColumn} = hmae.${entityUUIDColumn} AND mcpl.label_id IS NULL ) -`, func(s string) string { return dynamicNames[s] }), fmt.Sprintf(generateDesiredStateQuery(entityType), "TRUE", "TRUE", "TRUE")) +`, func(s string) string { return dynamicNames[s] }), fmt.Sprintf(generateDesiredStateQuery(entityType), "TRUE", "TRUE", "TRUE", "TRUE")) } func (ds *Datastore) ListMDMAppleProfilesToInstall(ctx context.Context) ([]*fleet.MDMAppleProfilePayload, error) { diff --git a/server/datastore/mysql/apple_mdm_test.go b/server/datastore/mysql/apple_mdm_test.go index 42bf507950..179ff962e0 100644 --- a/server/datastore/mysql/apple_mdm_test.go +++ b/server/datastore/mysql/apple_mdm_test.go @@ -90,6 +90,7 @@ func TestMDMApple(t *testing.T) { {"HostMDMCommands", testHostMDMCommands}, {"IngestMDMAppleDeviceFromOTAEnrollment", testIngestMDMAppleDeviceFromOTAEnrollment}, {"MDMManagedCertificates", testMDMManagedCertificates}, + {"TestMDMAppleProfileLabels", testMDMAppleProfileLabels}, } for _, c := range cases { @@ -1287,8 +1288,8 @@ func configProfileBytesForTest(name, identifier, uuid string) []byte { `, name, identifier, uuid)) } -// if the label name starts with "exclude-", the label is considered an "exclude-any", otherwise -// it is an "include-all". +// If the label name starts with "exclude-", the label is considered an "exclude-any". If it starts +// with "include-any", it is considered an "include-any". Otherwise it is an "include-all". func configProfileForTest(t *testing.T, name, identifier, uuid string, labels ...*fleet.Label) *fleet.MDMAppleConfigProfile { prof := configProfileBytesForTest(name, identifier, uuid) cp, err := fleet.NewMDMAppleConfigProfile(prof, nil) @@ -1297,9 +1298,12 @@ func configProfileForTest(t *testing.T, name, identifier, uuid string, labels .. cp.Checksum = sum[:] for _, lbl := range labels { - if strings.HasPrefix(lbl.Name, "exclude-") { + switch { + case strings.HasPrefix(lbl.Name, "exclude-"): cp.LabelsExcludeAny = append(cp.LabelsExcludeAny, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID}) - } else { + case strings.HasPrefix(lbl.Name, "include-any-"): + cp.LabelsIncludeAny = append(cp.LabelsIncludeAny, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID}) + default: cp.LabelsIncludeAll = append(cp.LabelsIncludeAll, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID}) } } @@ -1396,10 +1400,10 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) { `) require.NoError(t, err) - // if there are no hosts, then no profiles need to be installed - profiles, err := ds.ListMDMAppleProfilesToInstall(ctx) + // if there are no hosts, then no profilesToInstall need to be installed + profilesToInstall, err := ds.ListMDMAppleProfilesToInstall(ctx) require.NoError(t, err) - require.Empty(t, profiles) + require.Empty(t, profilesToInstall) host1, err := ds.NewHost(ctx, &fleet.Host{ Hostname: "test-host1-name", @@ -1437,13 +1441,13 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) { require.NoError(t, err) // global profiles to install on the newly added host - profiles, err = ds.ListMDMAppleProfilesToInstall(ctx) + profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx) require.NoError(t, err) matchProfiles([]*fleet.MDMAppleProfilePayload{ {ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"}, {ProfileUUID: globalPfs[1].ProfileUUID, ProfileIdentifier: globalPfs[1].Identifier, ProfileName: globalPfs[1].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"}, {ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"}, - }, profiles) + }, profilesToInstall) // add another host, it belongs to a team team, err := ds.NewTeam(ctx, &fleet.Team{Name: "test team"}) @@ -1459,9 +1463,9 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) { require.NoError(t, err) nanoEnroll(t, ds, host2, false) - // still the same profiles to assign as there are no profiles for team 1 - profiles, err = ds.ListMDMAppleProfilesToInstall(ctx) + profiles, err := ds.ListMDMAppleProfilesToInstall(ctx) require.NoError(t, err) + matchProfiles([]*fleet.MDMAppleProfilePayload{ {ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"}, {ProfileUUID: globalPfs[1].ProfileUUID, ProfileIdentifier: globalPfs[1].Identifier, ProfileName: globalPfs[1].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"}, @@ -1484,7 +1488,7 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) { require.Len(t, teamPfs, 2) // new profiles, this time for the new host belonging to team 1 - profiles, err = ds.ListMDMAppleProfilesToInstall(ctx) + profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx) require.NoError(t, err) matchProfiles([]*fleet.MDMAppleProfilePayload{ {ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"}, @@ -1492,7 +1496,7 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) { {ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"}, {ProfileUUID: teamPfs[0].ProfileUUID, ProfileIdentifier: teamPfs[0].Identifier, ProfileName: teamPfs[0].Name, HostUUID: "test-uuid-2", HostPlatform: "darwin"}, {ProfileUUID: teamPfs[1].ProfileUUID, ProfileIdentifier: teamPfs[1].Identifier, ProfileName: teamPfs[1].Name, HostUUID: "test-uuid-2", HostPlatform: "darwin"}, - }, profiles) + }, profilesToInstall) // add another global host host3, err := ds.NewHost(ctx, &fleet.Host{ @@ -1507,7 +1511,7 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) { nanoEnroll(t, ds, host3, false) // more profiles, this time for both global hosts and the team - profiles, err = ds.ListMDMAppleProfilesToInstall(ctx) + profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx) require.NoError(t, err) matchProfiles([]*fleet.MDMAppleProfilePayload{ {ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"}, @@ -1518,7 +1522,7 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) { {ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-3", HostPlatform: "darwin"}, {ProfileUUID: globalPfs[1].ProfileUUID, ProfileIdentifier: globalPfs[1].Identifier, ProfileName: globalPfs[1].Name, HostUUID: "test-uuid-3", HostPlatform: "darwin"}, {ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-3", HostPlatform: "darwin"}, - }, profiles) + }, profilesToInstall) // cron runs and updates the status err = ds.BulkUpsertMDMAppleHostProfiles( @@ -1608,9 +1612,9 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) { require.NoError(t, err) // no profiles left to install - profiles, err = ds.ListMDMAppleProfilesToInstall(ctx) + profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx) require.NoError(t, err) - require.Empty(t, profiles) + require.Empty(t, profilesToInstall) // no profiles to remove yet toRemove, err := ds.ListMDMAppleProfilesToRemove(ctx) @@ -1627,9 +1631,9 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) { require.NoError(t, apple_mdm.VerifyHostMDMProfiles(ctx, ds, host3, profilesByIdentifier(verified))) // still no profiles to install - profiles, err = ds.ListMDMAppleProfilesToInstall(ctx) + profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx) require.NoError(t, err) - require.Empty(t, profiles) + require.Empty(t, profilesToInstall) // still no profiles to remove toRemove, err = ds.ListMDMAppleProfilesToRemove(ctx) @@ -1641,12 +1645,12 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) { require.NoError(t, err) // profiles to be added for host1 are now related to the team - profiles, err = ds.ListMDMAppleProfilesToInstall(ctx) + profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx) require.NoError(t, err) matchProfiles([]*fleet.MDMAppleProfilePayload{ {ProfileUUID: teamPfs[0].ProfileUUID, ProfileIdentifier: teamPfs[0].Identifier, ProfileName: teamPfs[0].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"}, {ProfileUUID: teamPfs[1].ProfileUUID, ProfileIdentifier: teamPfs[1].Identifier, ProfileName: teamPfs[1].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"}, - }, profiles) + }, profilesToInstall) // profiles to be removed includes host1's old profiles toRemove, err = ds.ListMDMAppleProfilesToRemove(ctx) @@ -7218,3 +7222,212 @@ func testMDMManagedCertificates(t *testing.T, ds *Datastore) { }) require.ErrorIs(t, err, sql.ErrNoRows) } + +func testMDMAppleProfileLabels(t *testing.T, ds *Datastore) { + ctx := context.Background() + + matchProfiles := func(want, got []*fleet.MDMAppleProfilePayload) { + // match only the fields we care about + for _, p := range got { + p.Checksum = nil + } + require.ElementsMatch(t, want, got) + } + + globProf1, err := ds.NewMDMAppleConfigProfile(ctx, *configProfileForTest(t, "N1", "I1", "z")) + require.NoError(t, err) + globProf2, err := ds.NewMDMAppleConfigProfile(ctx, *configProfileForTest(t, "N2", "I2", "x")) + require.NoError(t, err) + + globalPfs, err := ds.ListMDMAppleConfigProfiles(ctx, ptr.Uint(0)) + require.NoError(t, err) + require.Len(t, globalPfs, 2) + + // if there are no hosts, then no profilesToInstall need to be installed + profilesToInstall, err := ds.ListMDMAppleProfilesToInstall(ctx) + require.NoError(t, err) + require.Empty(t, profilesToInstall) + + host1, err := ds.NewHost(ctx, &fleet.Host{ + Hostname: "test-host1-name", + OsqueryHostID: ptr.String("1337"), + NodeKey: ptr.String("1337"), + UUID: "test-uuid-1", + TeamID: nil, + Platform: "darwin", + }) + require.NoError(t, err) + // add a user enrollment for this device, nothing else should be modified + nanoEnroll(t, ds, host1, true) + + // non-macOS hosts shouldn't modify any of the results below + _, err = ds.NewHost(ctx, &fleet.Host{ + Hostname: "test-windows-host", + OsqueryHostID: ptr.String("4824"), + NodeKey: ptr.String("4824"), + UUID: "test-windows-host", + TeamID: nil, + Platform: "windows", + }) + require.NoError(t, err) + + // a macOS host that's not MDM enrolled into Fleet shouldn't + // modify any of the results below + _, err = ds.NewHost(ctx, &fleet.Host{ + Hostname: "test-non-mdm-host", + OsqueryHostID: ptr.String("4825"), + NodeKey: ptr.String("4825"), + UUID: "test-non-mdm-host", + TeamID: nil, + Platform: "darwin", + }) + require.NoError(t, err) + + // global profiles to install on the newly added host + profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx) + require.NoError(t, err) + matchProfiles([]*fleet.MDMAppleProfilePayload{ + {ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"}, + {ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"}, + }, profilesToInstall) + + hostLabel, err := ds.NewHost(ctx, &fleet.Host{ + Hostname: "test-host-name-label", + OsqueryHostID: ptr.String("1337_label"), + NodeKey: ptr.String("1337_label"), + UUID: "test-uuid-1-label", + TeamID: nil, + Platform: "darwin", + }) + require.NoError(t, err) + // add a user enrollment for this device, nothing else should be modified + nanoEnroll(t, ds, hostLabel, true) + + // include-any labels + l1, err := ds.NewLabel(ctx, &fleet.Label{Name: "include-any-1", Query: "select 1"}) + require.NoError(t, err) + + l2, err := ds.NewLabel(ctx, &fleet.Label{Name: "include-any-2", Query: "select 1"}) + require.NoError(t, err) + + l3, err := ds.NewLabel(ctx, &fleet.Label{Name: "include-any-3", Query: "select 1"}) + require.NoError(t, err) + + // include-all labels + l4, err := ds.NewLabel(ctx, &fleet.Label{Name: "include-all-4", Query: "select 1"}) + require.NoError(t, err) + + l5, err := ds.NewLabel(ctx, &fleet.Label{Name: "include-all-5", Query: "select 1"}) + require.NoError(t, err) + + // exclude-any labels + l6, err := ds.NewLabel(ctx, &fleet.Label{Name: "exclude-any-6", Query: "select 1"}) + require.NoError(t, err) + + l7, err := ds.NewLabel(ctx, &fleet.Label{Name: "exclude-any-7", Query: "select 1"}) + require.NoError(t, err) + + profIncludeAny, err := ds.NewMDMAppleConfigProfile(ctx, *configProfileForTest(t, "prof-include-any", "prof-include-any", "prof-include-any", l1, l2, l3)) + require.NoError(t, err) + profIncludeAll, err := ds.NewMDMAppleConfigProfile(ctx, *configProfileForTest(t, "prof-include-all", "prof-include-all", "prof-include-all", l4, l5)) + require.NoError(t, err) + profExcludeAny, err := ds.NewMDMAppleConfigProfile(ctx, *configProfileForTest(t, "prof-exclude-any", "prof-exclude-any", "prof-exclude-any", l6, l7)) + require.NoError(t, err) + + // hostLabel is a member of l1, l4, l5 + err = ds.AsyncBatchInsertLabelMembership(ctx, [][2]uint{{l1.ID, hostLabel.ID}, {l4.ID, hostLabel.ID}, {l5.ID, hostLabel.ID}}) + require.NoError(t, err) + + globalPfs, err = ds.ListMDMAppleConfigProfiles(ctx, ptr.Uint(0)) + require.NoError(t, err) + require.Len(t, globalPfs, 5) + + // still the same profiles to assign (plus the one for hostLabel) as there are no profiles for team 1 + profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx) + + require.NoError(t, err) + matchProfiles([]*fleet.MDMAppleProfilePayload{ + {ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: host1.UUID, HostPlatform: "darwin"}, + {ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: host1.UUID, HostPlatform: "darwin"}, + {ProfileUUID: profExcludeAny.ProfileUUID, ProfileIdentifier: profExcludeAny.Identifier, ProfileName: profExcludeAny.Name, HostUUID: host1.UUID, HostPlatform: "darwin"}, + + {ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"}, + {ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"}, + {ProfileUUID: profIncludeAny.ProfileUUID, ProfileIdentifier: profIncludeAny.Identifier, ProfileName: profIncludeAny.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"}, + {ProfileUUID: profIncludeAll.ProfileUUID, ProfileIdentifier: profIncludeAll.Identifier, ProfileName: profIncludeAll.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"}, + {ProfileUUID: profExcludeAny.ProfileUUID, ProfileIdentifier: profExcludeAny.Identifier, ProfileName: profExcludeAny.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"}, + }, profilesToInstall) + + // Remove the l1<->hostLabel relationship, but add l2<->hostLabel. The profile should still show + // up since it's "include any" + err = ds.AsyncBatchDeleteLabelMembership(ctx, [][2]uint{{l1.ID, hostLabel.ID}}) + require.NoError(t, err) + + err = ds.AsyncBatchInsertLabelMembership(ctx, [][2]uint{{l2.ID, hostLabel.ID}}) + require.NoError(t, err) + + profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx) + + require.NoError(t, err) + matchProfiles([]*fleet.MDMAppleProfilePayload{ + {ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: host1.UUID, HostPlatform: "darwin"}, + {ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: host1.UUID, HostPlatform: "darwin"}, + {ProfileUUID: profExcludeAny.ProfileUUID, ProfileIdentifier: profExcludeAny.Identifier, ProfileName: profExcludeAny.Name, HostUUID: host1.UUID, HostPlatform: "darwin"}, + + {ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"}, + {ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"}, + {ProfileUUID: profIncludeAny.ProfileUUID, ProfileIdentifier: profIncludeAny.Identifier, ProfileName: profIncludeAny.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"}, + {ProfileUUID: profIncludeAll.ProfileUUID, ProfileIdentifier: profIncludeAll.Identifier, ProfileName: profIncludeAll.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"}, + {ProfileUUID: profExcludeAny.ProfileUUID, ProfileIdentifier: profExcludeAny.Identifier, ProfileName: profExcludeAny.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"}, + }, profilesToInstall) + + // Remove the l2<->hostLabel relationship. The profie should no longer show up since it's + // include-any + err = ds.AsyncBatchDeleteLabelMembership(ctx, [][2]uint{{l2.ID, hostLabel.ID}}) + require.NoError(t, err) + profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx) + require.NoError(t, err) + + matchProfiles([]*fleet.MDMAppleProfilePayload{ + {ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: host1.UUID, HostPlatform: "darwin"}, + {ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: host1.UUID, HostPlatform: "darwin"}, + {ProfileUUID: profExcludeAny.ProfileUUID, ProfileIdentifier: profExcludeAny.Identifier, ProfileName: profExcludeAny.Name, HostUUID: host1.UUID, HostPlatform: "darwin"}, + + {ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"}, + {ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"}, + {ProfileUUID: profIncludeAll.ProfileUUID, ProfileIdentifier: profIncludeAll.Identifier, ProfileName: profIncludeAll.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"}, + {ProfileUUID: profExcludeAny.ProfileUUID, ProfileIdentifier: profExcludeAny.Identifier, ProfileName: profExcludeAny.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"}, + }, profilesToInstall) + + // Remove the l4<->hostLabel relationship. Since the profile is "include-all", it should no longer show + // up even though the l5<->hostLabel connection is still there. + err = ds.AsyncBatchDeleteLabelMembership(ctx, [][2]uint{{l4.ID, hostLabel.ID}}) + require.NoError(t, err) + profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx) + require.NoError(t, err) + + matchProfiles([]*fleet.MDMAppleProfilePayload{ + {ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: host1.UUID, HostPlatform: "darwin"}, + {ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: host1.UUID, HostPlatform: "darwin"}, + {ProfileUUID: profExcludeAny.ProfileUUID, ProfileIdentifier: profExcludeAny.Identifier, ProfileName: profExcludeAny.Name, HostUUID: host1.UUID, HostPlatform: "darwin"}, + + {ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"}, + {ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"}, + {ProfileUUID: profExcludeAny.ProfileUUID, ProfileIdentifier: profExcludeAny.Identifier, ProfileName: profExcludeAny.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"}, + }, profilesToInstall) + + // Add a l6<->host relationship. The exclude-any profile should no longer be assigned to hostLabel. + err = ds.AsyncBatchInsertLabelMembership(ctx, [][2]uint{{l6.ID, hostLabel.ID}}) + require.NoError(t, err) + profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx) + require.NoError(t, err) + + matchProfiles([]*fleet.MDMAppleProfilePayload{ + {ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: host1.UUID, HostPlatform: "darwin"}, + {ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: host1.UUID, HostPlatform: "darwin"}, + {ProfileUUID: profExcludeAny.ProfileUUID, ProfileIdentifier: profExcludeAny.Identifier, ProfileName: profExcludeAny.Name, HostUUID: host1.UUID, HostPlatform: "darwin"}, + + {ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"}, + {ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"}, + }, profilesToInstall) +} diff --git a/server/datastore/mysql/microsoft_mdm.go b/server/datastore/mysql/microsoft_mdm.go index 38563d6baa..eb96305280 100644 --- a/server/datastore/mysql/microsoft_mdm.go +++ b/server/datastore/mysql/microsoft_mdm.go @@ -1166,7 +1166,7 @@ const windowsMDMProfilesDesiredStateQuery = ` JOIN mdm_windows_enrollments mwe ON mwe.host_uuid = h.uuid JOIN mdm_configuration_profile_labels mcpl - ON mcpl.windows_profile_uuid = mwcp.profile_uuid AND mcpl.exclude = 0 + ON mcpl.windows_profile_uuid = mwcp.profile_uuid AND mcpl.exclude = 0 AND mcpl.require_all = 1 LEFT OUTER JOIN label_membership lm ON lm.label_id = mcpl.label_id AND lm.host_id = h.id WHERE @@ -1195,7 +1195,7 @@ const windowsMDMProfilesDesiredStateQuery = ` JOIN mdm_windows_enrollments mwe ON mwe.host_uuid = h.uuid JOIN mdm_configuration_profile_labels mcpl - ON mcpl.windows_profile_uuid = mwcp.profile_uuid AND mcpl.exclude = 1 + ON mcpl.windows_profile_uuid = mwcp.profile_uuid AND mcpl.exclude = 1 AND mcpl.require_all = 0 LEFT OUTER JOIN label_membership lm ON lm.label_id = mcpl.label_id AND lm.host_id = h.id WHERE @@ -1206,6 +1206,36 @@ const windowsMDMProfilesDesiredStateQuery = ` HAVING -- considers only the profiles with labels, without any broken label, and with the host not in any label count_profile_labels > 0 AND count_profile_labels = count_non_broken_labels AND count_host_labels = 0 + + UNION + + -- label-based profiles where the host is a member of any of the labels (include-any). + -- by design, "include" labels cannot match if they are broken (the host cannot be + -- a member of a deleted label). + SELECT + mwcp.profile_uuid, + mwcp.name, + h.uuid as host_uuid, + COUNT(*) as count_profile_labels, + COUNT(mcpl.label_id) as count_non_broken_labels, + COUNT(lm.label_id) as count_host_labels + FROM + mdm_windows_configuration_profiles mwcp + JOIN hosts h + ON h.team_id = mwcp.team_id OR (h.team_id IS NULL AND mwcp.team_id = 0) + JOIN mdm_windows_enrollments mwe + ON mwe.host_uuid = h.uuid + JOIN mdm_configuration_profile_labels mcpl + ON mcpl.windows_profile_uuid = mwcp.profile_uuid AND mcpl.exclude = 0 AND mcpl.require_all = 0 + LEFT OUTER JOIN label_membership lm + ON lm.label_id = mcpl.label_id AND lm.host_id = h.id + WHERE + h.platform = 'windows' AND + ( %s ) + GROUP BY + mwcp.profile_uuid, mwcp.name, h.uuid + HAVING + count_profile_labels > 0 AND count_host_labels >= 1 ` func (ds *Datastore) ListMDMWindowsProfilesToInstall(ctx context.Context) ([]*fleet.MDMWindowsProfilePayload, error) { @@ -1272,9 +1302,9 @@ func listMDMWindowsProfilesToInstallDB( var err error args := []any{fleet.MDMOperationTypeInstall} - query = fmt.Sprintf(query, hostFilter, hostFilter, hostFilter) + query = fmt.Sprintf(query, hostFilter, hostFilter, hostFilter, hostFilter) if len(hostUUIDs) > 0 { - query, args, err = sqlx.In(query, hostUUIDs, hostUUIDs, hostUUIDs, fleet.MDMOperationTypeInstall) + query, args, err = sqlx.In(query, hostUUIDs, hostUUIDs, hostUUIDs, hostUUIDs, fleet.MDMOperationTypeInstall) if err != nil { return nil, ctxerr.Wrap(ctx, err, "building sqlx.In") } @@ -1352,7 +1382,7 @@ func listMDMWindowsProfilesToRemoveDB( mcpl.label_id IS NULL ) AND (%s) -`, fmt.Sprintf(windowsMDMProfilesDesiredStateQuery, "TRUE", "TRUE", "TRUE"), hostFilter) +`, fmt.Sprintf(windowsMDMProfilesDesiredStateQuery, "TRUE", "TRUE", "TRUE", "TRUE"), hostFilter) var err error var args []any @@ -1582,7 +1612,7 @@ INSERT INTO cp.LabelsIncludeAny[i].ProfileUUID = profileUUID cp.LabelsIncludeAny[i].RequireAll = false cp.LabelsIncludeAny[i].Exclude = false - labels = append(labels, cp.LabelsIncludeAll[i]) + labels = append(labels, cp.LabelsIncludeAny[i]) } for i := range cp.LabelsExcludeAny { cp.LabelsExcludeAny[i].ProfileUUID = profileUUID diff --git a/server/datastore/mysql/microsoft_mdm_test.go b/server/datastore/mysql/microsoft_mdm_test.go index a4ee1749c0..2f72a1f45e 100644 --- a/server/datastore/mysql/microsoft_mdm_test.go +++ b/server/datastore/mysql/microsoft_mdm_test.go @@ -40,6 +40,7 @@ func TestMDMWindows(t *testing.T) { {"TestMDMWindowsDiskEncryption", testMDMWindowsDiskEncryption}, {"TestMDMWindowsProfilesSummary", testMDMWindowsProfilesSummary}, {"TestBatchSetMDMWindowsProfiles", testBatchSetMDMWindowsProfiles}, + {"TestMDMWindowsProfileLabels", testMDMWindowsProfileLabels}, } for _, c := range cases { @@ -2010,6 +2011,160 @@ func testSetOrReplaceMDMWindowsConfigProfile(t *testing.T, ds *Datastore) { expectWindowsProfiles(t, ds, nil, []*fleet.MDMWindowsConfigProfile{&cp2, &cp6}) } +func testMDMWindowsProfileLabels(t *testing.T, ds *Datastore) { + ctx := context.Background() + // Create a windows host + u := uuid.New().String() + host, err := ds.NewHost(ctx, &fleet.Host{ + DetailUpdatedAt: time.Now(), + LabelUpdatedAt: time.Now(), + PolicyUpdatedAt: time.Now(), + SeenTime: time.Now(), + NodeKey: &u, + UUID: u, + Hostname: u, + Platform: "windows", + }) + require.NoError(t, err) + windowsEnroll(t, ds, host) + + // "include-any" labels + l1, err := ds.NewLabel(ctx, &fleet.Label{ + Name: "include-any-label1", + Description: "desc", + Query: "select 1;", + }) + require.NoError(t, err) + + l2, err := ds.NewLabel(ctx, &fleet.Label{ + Name: "include-any-label2", + Description: "desc", + Query: "select 1;", + }) + require.NoError(t, err) + + l3, err := ds.NewLabel(ctx, &fleet.Label{ + Name: "include-any-label3", + Description: "desc", + Query: "select 1;", + }) + require.NoError(t, err) + + // include-all labels + l4, err := ds.NewLabel(ctx, &fleet.Label{ + Name: "include-all-label4", + Description: "desc", + Query: "select 1;", + }) + require.NoError(t, err) + + l5, err := ds.NewLabel(ctx, &fleet.Label{ + Name: "include-all-label5", + Description: "desc", + Query: "select 1;", + }) + require.NoError(t, err) + + // exclude-all labels + l6, err := ds.NewLabel(ctx, &fleet.Label{ + Name: "exclude-any-label6", + Description: "desc", + Query: "select 1;", + }) + require.NoError(t, err) + + l7, err := ds.NewLabel(ctx, &fleet.Label{ + Name: "exclude-any-label7", + Description: "desc", + Query: "select 1;", + }) + require.NoError(t, err) + + // Create a profile with "include-any" with l1 + includeAnyProf, err := ds.NewMDMWindowsConfigProfile( + ctx, + *windowsConfigProfileForTest(t, "prof-include-any", "./Foo/Bar", l1, l2, l3), + ) + + require.NoError(t, err) + require.NotEmpty(t, includeAnyProf.ProfileUUID) + + // Create a profile with "include-all" with l4 and l5 + includeAllProf, err := ds.NewMDMWindowsConfigProfile( + ctx, + *windowsConfigProfileForTest(t, "prof-include-all", "./Foo/Bar", l4, l5), + ) + require.NoError(t, err) + require.NotEmpty(t, includeAllProf.ProfileUUID) + + // Create a profile with "exclude-all" with l6 and l7 + excludeAllProf, err := ds.NewMDMWindowsConfigProfile( + ctx, + *windowsConfigProfileForTest(t, "prof-exclude-any", "./Foo/Bar", l6, l7), + ) + require.NoError(t, err) + + // Connect the host and l1, l4, l5 + err = ds.AsyncBatchInsertLabelMembership(ctx, [][2]uint{{l1.ID, host.ID}, {l4.ID, host.ID}, {l5.ID, host.ID}}) + require.NoError(t, err) + + // We should see all 3 profiles in the "to install" list + profilesToInstall, err := ds.ListMDMWindowsProfilesToInstall(ctx) + require.NoError(t, err) + require.ElementsMatch(t, []*fleet.MDMWindowsProfilePayload{ + {ProfileUUID: includeAllProf.ProfileUUID, ProfileName: includeAllProf.Name, HostUUID: host.UUID}, + {ProfileUUID: includeAnyProf.ProfileUUID, ProfileName: includeAnyProf.Name, HostUUID: host.UUID}, + {ProfileUUID: excludeAllProf.ProfileUUID, ProfileName: excludeAllProf.Name, HostUUID: host.UUID}, + }, profilesToInstall) + + // Remove the l1<->host relationship, but add l2<->labelHost. The profile should still show + // up since it's "include any" + err = ds.AsyncBatchDeleteLabelMembership(ctx, [][2]uint{{l1.ID, host.ID}}) + require.NoError(t, err) + + err = ds.AsyncBatchInsertLabelMembership(ctx, [][2]uint{{l2.ID, host.ID}}) + require.NoError(t, err) + + profilesToInstall, err = ds.ListMDMWindowsProfilesToInstall(ctx) + require.NoError(t, err) + require.ElementsMatch(t, []*fleet.MDMWindowsProfilePayload{ + {ProfileUUID: includeAllProf.ProfileUUID, ProfileName: includeAllProf.Name, HostUUID: host.UUID}, + {ProfileUUID: includeAnyProf.ProfileUUID, ProfileName: includeAnyProf.Name, HostUUID: host.UUID}, + {ProfileUUID: excludeAllProf.ProfileUUID, ProfileName: excludeAllProf.Name, HostUUID: host.UUID}, + }, profilesToInstall) + + // Remove the l2<->host relationship. Since the profile is "include-any", it should no longer + // show up + err = ds.AsyncBatchDeleteLabelMembership(ctx, [][2]uint{{l2.ID, host.ID}}) + require.NoError(t, err) + + profilesToInstall, err = ds.ListMDMWindowsProfilesToInstall(ctx) + require.NoError(t, err) + require.ElementsMatch(t, []*fleet.MDMWindowsProfilePayload{ + {ProfileUUID: includeAllProf.ProfileUUID, ProfileName: includeAllProf.Name, HostUUID: host.UUID}, + {ProfileUUID: excludeAllProf.ProfileUUID, ProfileName: excludeAllProf.Name, HostUUID: host.UUID}, + }, profilesToInstall) + + // Remove the l4<->host relationship. Since the profile is "include-all", it should no longer show + // up even though the l5<->host connection is still there. + err = ds.AsyncBatchDeleteLabelMembership(ctx, [][2]uint{{l4.ID, host.ID}}) + require.NoError(t, err) + + profilesToInstall, err = ds.ListMDMWindowsProfilesToInstall(ctx) + require.NoError(t, err) + require.ElementsMatch(t, []*fleet.MDMWindowsProfilePayload{ + {ProfileUUID: excludeAllProf.ProfileUUID, ProfileName: excludeAllProf.Name, HostUUID: host.UUID}, + }, profilesToInstall) + + // Add a l6<->host relationship. The exclude-any profile should be gone now. + err = ds.AsyncBatchInsertLabelMembership(ctx, [][2]uint{{l6.ID, host.ID}}) + require.NoError(t, err) + + profilesToInstall, err = ds.ListMDMWindowsProfilesToInstall(ctx) + require.NoError(t, err) + require.Empty(t, profilesToInstall) +} + func expectWindowsProfiles( t *testing.T, ds *Datastore, @@ -2068,7 +2223,8 @@ func testBatchSetMDMWindowsProfiles(t *testing.T, ds *Datastore) { ctx := context.Background() applyAndExpect := func(newSet []*fleet.MDMWindowsConfigProfile, tmID *uint, want []*fleet.MDMWindowsConfigProfile, - wantUpdated bool) map[string]string { + wantUpdated bool, + ) map[string]string { err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error { updatedDB, err := ds.batchSetMDMWindowsProfilesDB(ctx, tx, tmID, newSet) require.NoError(t, err) @@ -2197,9 +2353,12 @@ func windowsConfigProfileForTest(t *testing.T, name, locURI string, labels ...*f } for _, lbl := range labels { - if strings.HasPrefix(lbl.Name, "exclude-") { + switch { + case strings.HasPrefix(lbl.Name, "exclude-"): prof.LabelsExcludeAny = append(prof.LabelsExcludeAny, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID}) - } else { + case strings.HasPrefix(lbl.Name, "include-any-"): + prof.LabelsIncludeAny = append(prof.LabelsIncludeAny, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID}) + default: prof.LabelsIncludeAll = append(prof.LabelsIncludeAll, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID}) } } From 16d0d8d58fd6da28a767fd67ce52a3b3573649ca Mon Sep 17 00:00:00 2001 From: Sarah Gillespie <73313222+gillespi314@users.noreply.github.com> Date: Thu, 14 Nov 2024 08:30:32 -0600 Subject: [PATCH 6/7] Fix unreleased bugs with `labels_include_any` feature (#23734) --- .../cards/CustomSettings/CustomSettings.tsx | 1 + .../ProfileLabelsModal/ProfileLabelsModal.tsx | 2 +- server/datastore/mysql/apple_mdm.go | 49 +++- server/datastore/mysql/mdm.go | 22 +- server/datastore/mysql/mdm_test.go | 16 +- server/datastore/mysql/microsoft_mdm.go | 23 +- server/fleet/app.go | 1 + server/fleet/mdm.go | 16 +- server/fleet/mdm_test.go | 164 ++++++++++--- server/service/appconfig_test.go | 10 +- .../service/integration_mdm_profiles_test.go | 218 +++++++++++++++++- server/service/mdm.go | 9 +- 12 files changed, 454 insertions(+), 77 deletions(-) diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/CustomSettings.tsx b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/CustomSettings.tsx index 00cc7e199b..ede9ac75b1 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/CustomSettings.tsx +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/CustomSettings.tsx @@ -171,6 +171,7 @@ const CustomSettings = ({ const hasLabels = !!profileLabelsModalData?.labels_include_all?.length || + !!profileLabelsModalData?.labels_include_any?.length || !!profileLabelsModalData?.labels_exclude_any?.length; return ( diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileLabelsModal/ProfileLabelsModal.tsx b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileLabelsModal/ProfileLabelsModal.tsx index f5590c83af..28eb07ad96 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileLabelsModal/ProfileLabelsModal.tsx +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileLabelsModal/ProfileLabelsModal.tsx @@ -70,7 +70,7 @@ const ProfileLabelsModal = ({ if (labels_include_all) { targetTypeText = have all; } else if (labels_include_any) { - targetTypeText = have all; + targetTypeText = have any; } else { targetTypeText = don't have any; } diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go index 84fdfbaff7..5188e3e9b1 100644 --- a/server/datastore/mysql/apple_mdm.go +++ b/server/datastore/mysql/apple_mdm.go @@ -247,9 +247,19 @@ WHERE return nil, err } for _, lbl := range labels { - if lbl.Exclude { + switch { + case lbl.Exclude && lbl.RequireAll: + // this should never happen so log it for debugging + level.Debug(ds.logger).Log("msg", "unsupported profile label: cannot be both exclude and require all", + "profile_uuid", lbl.ProfileUUID, + "label_name", lbl.LabelName, + ) + case lbl.Exclude && !lbl.RequireAll: res.LabelsExcludeAny = append(res.LabelsExcludeAny, lbl) - } else { + case !lbl.Exclude && !lbl.RequireAll: + res.LabelsIncludeAny = append(res.LabelsIncludeAny, lbl) + default: + // default include all res.LabelsIncludeAll = append(res.LabelsIncludeAll, lbl) } } @@ -289,9 +299,19 @@ WHERE return nil, err } for _, lbl := range labels { - if lbl.Exclude { + switch { + case lbl.Exclude && lbl.RequireAll: + // this should never happen so log it for debugging + level.Debug(ds.logger).Log("msg", "unsupported profile label: cannot be both exclude and require all", + "profile_uuid", lbl.ProfileUUID, + "label_name", lbl.LabelName, + ) + case lbl.Exclude && !lbl.RequireAll: res.LabelsExcludeAny = append(res.LabelsExcludeAny, lbl) - } else { + case !lbl.Exclude && !lbl.RequireAll: + res.LabelsIncludeAny = append(res.LabelsIncludeAny, lbl) + default: + // default include all res.LabelsIncludeAll = append(res.LabelsIncludeAll, lbl) } } @@ -1828,16 +1848,28 @@ ON DUPLICATE KEY UPDATE for _, label := range incomingProf.LabelsIncludeAll { label.ProfileUUID = newlyInsertedProf.ProfileUUID + label.Exclude = false + label.RequireAll = true + incomingLabels = append(incomingLabels, label) + } + for _, label := range incomingProf.LabelsIncludeAny { + label.ProfileUUID = newlyInsertedProf.ProfileUUID + label.Exclude = false + label.RequireAll = false incomingLabels = append(incomingLabels, label) } for _, label := range incomingProf.LabelsExcludeAny { label.ProfileUUID = newlyInsertedProf.ProfileUUID label.Exclude = true + label.RequireAll = false incomingLabels = append(incomingLabels, label) } } } + // FIXME: At what point are we deleting label associations for existing profiles (e.g. if the user + // removes all labels from a profile in gitops, shouldn't we remove the old associations)? + // insert label associations var updatedLabels bool if updatedLabels, err = batchSetProfileLabelAssociationsDB(ctx, tx, incomingLabels, @@ -4289,11 +4321,20 @@ WHERE for _, label := range incomingDecl.LabelsIncludeAll { label.ProfileUUID = newlyInsertedDecl.DeclarationUUID + label.Exclude = false + label.RequireAll = true + incomingLabels = append(incomingLabels, label) + } + for _, label := range incomingDecl.LabelsIncludeAny { + label.ProfileUUID = newlyInsertedDecl.DeclarationUUID + label.Exclude = false + label.RequireAll = false incomingLabels = append(incomingLabels, label) } for _, label := range incomingDecl.LabelsExcludeAny { label.ProfileUUID = newlyInsertedDecl.DeclarationUUID label.Exclude = true + label.RequireAll = false incomingLabels = append(incomingLabels, label) } } diff --git a/server/datastore/mysql/mdm.go b/server/datastore/mysql/mdm.go index d01bad72bd..66f90c06f6 100644 --- a/server/datastore/mysql/mdm.go +++ b/server/datastore/mysql/mdm.go @@ -274,9 +274,19 @@ FROM ( } for _, label := range labels { if prof, ok := profMap[label.ProfileUUID]; ok { - if label.Exclude { + switch { + case label.Exclude && label.RequireAll: + // this should never happen so log it for debugging + level.Debug(ds.logger).Log("msg", "unsupported profile label: cannot be both exclude and require all", + "profile_uuid", label.ProfileUUID, + "label_name", label.LabelName, + ) + case label.Exclude && !label.RequireAll: prof.LabelsExcludeAny = append(prof.LabelsExcludeAny, label) - } else { + case !label.Exclude && !label.RequireAll: + prof.LabelsIncludeAny = append(prof.LabelsIncludeAny, label) + default: + // default include all prof.LabelsIncludeAll = append(prof.LabelsIncludeAll, label) } } @@ -293,7 +303,8 @@ SELECT label_name, COALESCE(label_id, 0) as label_id, IF(label_id IS NULL, 1, 0) as broken, - exclude + exclude, + require_all FROM mdm_configuration_profile_labels mcpl WHERE @@ -305,7 +316,8 @@ SELECT label_name, COALESCE(label_id, 0) as label_id, IF(label_id IS NULL, 1, 0) as broken, - exclude + exclude, + require_all FROM mdm_declaration_labels mdl WHERE @@ -997,6 +1009,8 @@ func batchSetProfileLabelAssociationsDB( platform string, ) (updatedDB bool, err error) { if len(profileLabels) == 0 { + // FIXME: At what point are we deleting all labels for a profile (e.g., the user might + // remove all labels from an existing profile)? return false, nil } diff --git a/server/datastore/mysql/mdm_test.go b/server/datastore/mysql/mdm_test.go index 43e94d71df..7e41ce03d2 100644 --- a/server/datastore/mysql/mdm_test.go +++ b/server/datastore/mysql/mdm_test.go @@ -699,20 +699,20 @@ func testListMDMConfigProfiles(t *testing.T, ds *Datastore) { require.NoError(t, ds.DeleteLabel(ctx, labels[4].Name)) profLabels := map[string][]fleet.ConfigurationProfileLabel{ "C": { - {LabelName: labels[0].Name, LabelID: labels[0].ID}, - {LabelName: labels[1].Name, LabelID: labels[1].ID}, + {LabelName: labels[0].Name, LabelID: labels[0].ID, RequireAll: true}, + {LabelName: labels[1].Name, LabelID: labels[1].ID, RequireAll: true}, }, "D": { - {LabelName: labels[2].Name, LabelID: labels[2].ID}, - {LabelName: labels[3].Name, LabelID: 0, Broken: true}, + {LabelName: labels[2].Name, LabelID: labels[2].ID, RequireAll: true}, + {LabelName: labels[3].Name, LabelID: 0, Broken: true, RequireAll: true}, }, "E": { - {LabelName: labels[4].Name, LabelID: 0, Broken: true}, - {LabelName: labels[5].Name, LabelID: labels[5].ID}, + {LabelName: labels[4].Name, LabelID: 0, Broken: true, RequireAll: true}, + {LabelName: labels[5].Name, LabelID: labels[5].ID, RequireAll: true}, }, "F": { - {LabelName: labels[6].Name, LabelID: labels[6].ID}, - {LabelName: labels[7].Name, LabelID: labels[7].ID}, + {LabelName: labels[6].Name, LabelID: labels[6].ID, RequireAll: true}, + {LabelName: labels[7].Name, LabelID: labels[7].ID, RequireAll: true}, }, } diff --git a/server/datastore/mysql/microsoft_mdm.go b/server/datastore/mysql/microsoft_mdm.go index 961a6c1355..524888cf8d 100644 --- a/server/datastore/mysql/microsoft_mdm.go +++ b/server/datastore/mysql/microsoft_mdm.go @@ -745,9 +745,19 @@ WHERE return nil, err } for _, lbl := range labels { - if lbl.Exclude { + switch { + case lbl.Exclude && lbl.RequireAll: + // this should never happen so log it for debugging + level.Debug(ds.logger).Log("msg", "unsupported profile label: cannot be both exclude and require all", + "profile_uuid", lbl.ProfileUUID, + "label_name", lbl.LabelName, + ) + case lbl.Exclude && !lbl.RequireAll: res.LabelsExcludeAny = append(res.LabelsExcludeAny, lbl) - } else { + case !lbl.Exclude && !lbl.RequireAll: + res.LabelsIncludeAny = append(res.LabelsIncludeAny, lbl) + default: + // default include all res.LabelsIncludeAll = append(res.LabelsIncludeAll, lbl) } } @@ -1872,11 +1882,20 @@ ON DUPLICATE KEY UPDATE for _, label := range incomingProf.LabelsIncludeAll { label.ProfileUUID = newlyInsertedProf.ProfileUUID + label.Exclude = false + label.RequireAll = true + incomingLabels = append(incomingLabels, label) + } + for _, label := range incomingProf.LabelsIncludeAny { + label.ProfileUUID = newlyInsertedProf.ProfileUUID + label.Exclude = false + label.RequireAll = false incomingLabels = append(incomingLabels, label) } for _, label := range incomingProf.LabelsExcludeAny { label.ProfileUUID = newlyInsertedProf.ProfileUUID label.Exclude = true + label.RequireAll = false incomingLabels = append(incomingLabels, label) } } diff --git a/server/fleet/app.go b/server/fleet/app.go index 59c5ef24f4..3806e79c7d 100644 --- a/server/fleet/app.go +++ b/server/fleet/app.go @@ -385,6 +385,7 @@ func (s *MacOSSettings) FromMap(m map[string]interface{}) (map[string]bool, erro spec.Labels = extractLabelField(m, "labels") spec.LabelsIncludeAll = extractLabelField(m, "labels_include_all") spec.LabelsExcludeAny = extractLabelField(m, "labels_exclude_any") + spec.LabelsIncludeAny = extractLabelField(m, "labels_include_any") csSpecs = append(csSpecs, spec) } else if m, ok := v.(string); ok { // for backwards compatibility with the old way to define profiles diff --git a/server/fleet/mdm.go b/server/fleet/mdm.go index 00f98633a7..7f717eeb14 100644 --- a/server/fleet/mdm.go +++ b/server/fleet/mdm.go @@ -525,26 +525,30 @@ func (p *MDMProfileSpec) UnmarshalJSON(data []byte) error { if len(data) == 0 { return nil } - if lookAhead := bytes.TrimSpace(data); len(lookAhead) > 0 && lookAhead[0] == '"' { var backwardsCompat string if err := json.Unmarshal(data, &backwardsCompat); err != nil { return fmt.Errorf("unmarshal profile spec. Error using old format: %w", err) } p.Path = backwardsCompat + + // FIXME: equivalent of no label condition, should clear all labels slice? + // p.Labels = nil + // p.LabelsIncludeAll = nil + // p.LabelsIncludeAny = nil + // p.LabelsExcludeAny = nil return nil } // use an alias type to avoid recursively calling this function forever. type Alias MDMProfileSpec - aliasData := struct { - *Alias - }{ - Alias: (*Alias)(p), - } + var aliasData Alias if err := json.Unmarshal(data, &aliasData); err != nil { return fmt.Errorf("unmarshal profile spec. Error using new format: %w", err) } + // NOTE: we always want the newly unmarshaled profile spec to completely replace the old one + // (rather than merging the new data into the old one). + *p = MDMProfileSpec(aliasData) return nil } diff --git a/server/fleet/mdm_test.go b/server/fleet/mdm_test.go index 256b65be64..3ea7f0b36a 100644 --- a/server/fleet/mdm_test.go +++ b/server/fleet/mdm_test.go @@ -2,6 +2,7 @@ package fleet_test import ( "context" + "encoding/json" "errors" "net/http" "net/http/httptest" @@ -122,72 +123,106 @@ func TestDEPClient(t *testing.T) { wantToksTermsFlags map[string]bool }{ // use a valid token, appconfig should not be updated (already unflagged) - {token: validToken, orgName: "org1", wantErr: false, readInvoked: true, writeTokInvoked: false, - writeAppCfgInvoked: false, wantAppCfgTermsFlag: false, wantToksTermsFlags: map[string]bool{"org1": false, "org2": false}}, + { + token: validToken, orgName: "org1", wantErr: false, readInvoked: true, writeTokInvoked: false, + writeAppCfgInvoked: false, wantAppCfgTermsFlag: false, wantToksTermsFlags: map[string]bool{"org1": false, "org2": false}, + }, // use a valid token without org, nothing is checked - {token: validToken, orgName: "", wantErr: false, readInvoked: false, writeTokInvoked: false, - writeAppCfgInvoked: false, wantAppCfgTermsFlag: false, wantToksTermsFlags: map[string]bool{"org1": false, "org2": false}}, + { + token: validToken, orgName: "", wantErr: false, readInvoked: false, writeTokInvoked: false, + writeAppCfgInvoked: false, wantAppCfgTermsFlag: false, wantToksTermsFlags: map[string]bool{"org1": false, "org2": false}, + }, // use an invalid token without org, call fails but nothing is checked because this is an unsaved token - {token: invalidToken, orgName: "", wantErr: true, readInvoked: false, writeTokInvoked: false, - writeAppCfgInvoked: false, wantAppCfgTermsFlag: false, wantToksTermsFlags: map[string]bool{"org1": false, "org2": false}}, + { + token: invalidToken, orgName: "", wantErr: true, readInvoked: false, writeTokInvoked: false, + writeAppCfgInvoked: false, wantAppCfgTermsFlag: false, wantToksTermsFlags: map[string]bool{"org1": false, "org2": false}, + }, // use an invalid token, appconfig should not even be read (not a terms error) - {token: invalidToken, orgName: "org1", wantErr: true, readInvoked: false, writeTokInvoked: false, - writeAppCfgInvoked: false, wantAppCfgTermsFlag: false, wantToksTermsFlags: map[string]bool{"org1": false, "org2": false}}, + { + token: invalidToken, orgName: "org1", wantErr: true, readInvoked: false, writeTokInvoked: false, + writeAppCfgInvoked: false, wantAppCfgTermsFlag: false, wantToksTermsFlags: map[string]bool{"org1": false, "org2": false}, + }, // terms changed for org1 during the auth request - {token: termsChangedToken, orgName: "org1", wantErr: true, readInvoked: true, writeTokInvoked: true, - writeAppCfgInvoked: true, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": true, "org2": false}}, + { + token: termsChangedToken, orgName: "org1", wantErr: true, readInvoked: true, writeTokInvoked: true, + writeAppCfgInvoked: true, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": true, "org2": false}, + }, // use of an invalid token does not update the flag - {token: invalidToken, orgName: "org1", wantErr: true, readInvoked: false, writeTokInvoked: false, - writeAppCfgInvoked: false, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": true, "org2": false}}, + { + token: invalidToken, orgName: "org1", wantErr: true, readInvoked: false, writeTokInvoked: false, + writeAppCfgInvoked: false, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": true, "org2": false}, + }, // use of a valid token for org1 resets the flags - {token: validToken, orgName: "org1", wantErr: false, readInvoked: true, writeTokInvoked: true, - writeAppCfgInvoked: true, wantAppCfgTermsFlag: false, wantToksTermsFlags: map[string]bool{"org1": false, "org2": false}}, + { + token: validToken, orgName: "org1", wantErr: false, readInvoked: true, writeTokInvoked: true, + writeAppCfgInvoked: true, wantAppCfgTermsFlag: false, wantToksTermsFlags: map[string]bool{"org1": false, "org2": false}, + }, // use of a valid token again with org2 does not update anything - {token: validToken, orgName: "org2", wantErr: false, readInvoked: true, writeTokInvoked: false, - writeAppCfgInvoked: false, wantAppCfgTermsFlag: false, wantToksTermsFlags: map[string]bool{"org1": false, "org2": false}}, + { + token: validToken, orgName: "org2", wantErr: false, readInvoked: true, writeTokInvoked: false, + writeAppCfgInvoked: false, wantAppCfgTermsFlag: false, wantToksTermsFlags: map[string]bool{"org1": false, "org2": false}, + }, // terms changed for org2 during the actual account request, after auth - {token: termsChangedAfterAuthToken, orgName: "org2", wantErr: true, readInvoked: true, writeTokInvoked: true, - writeAppCfgInvoked: true, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": false, "org2": true}}, + { + token: termsChangedAfterAuthToken, orgName: "org2", wantErr: true, readInvoked: true, writeTokInvoked: true, + writeAppCfgInvoked: true, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": false, "org2": true}, + }, // again terms changed after auth for org2, doesn't update appConfig - {token: termsChangedAfterAuthToken, orgName: "org2", wantErr: true, readInvoked: true, writeTokInvoked: true, - writeAppCfgInvoked: false, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": false, "org2": true}}, + { + token: termsChangedAfterAuthToken, orgName: "org2", wantErr: true, readInvoked: true, writeTokInvoked: true, + writeAppCfgInvoked: false, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": false, "org2": true}, + }, // terms changed during auth for org2, doesn't update appConfig - {token: termsChangedToken, orgName: "org2", wantErr: true, readInvoked: true, writeTokInvoked: true, - writeAppCfgInvoked: false, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": false, "org2": true}}, + { + token: termsChangedToken, orgName: "org2", wantErr: true, readInvoked: true, writeTokInvoked: true, + writeAppCfgInvoked: false, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": false, "org2": true}, + }, // terms changed during auth for org1, now both tokens have the flag, doesn't update appConfig - {token: termsChangedToken, orgName: "org1", wantErr: true, readInvoked: true, writeTokInvoked: true, - writeAppCfgInvoked: false, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": true, "org2": true}}, + { + token: termsChangedToken, orgName: "org1", wantErr: true, readInvoked: true, writeTokInvoked: true, + writeAppCfgInvoked: false, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": true, "org2": true}, + }, // use a valid token without org, nothing is checked - {token: validToken, orgName: "", wantErr: false, readInvoked: false, writeTokInvoked: false, - writeAppCfgInvoked: false, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": true, "org2": true}}, + { + token: validToken, orgName: "", wantErr: false, readInvoked: false, writeTokInvoked: false, + writeAppCfgInvoked: false, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": true, "org2": true}, + }, // use an invalid token without org, call fails but nothing is checked because this is an unsaved token - {token: invalidToken, orgName: "", wantErr: true, readInvoked: false, writeTokInvoked: false, - writeAppCfgInvoked: false, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": true, "org2": true}}, + { + token: invalidToken, orgName: "", wantErr: true, readInvoked: false, writeTokInvoked: false, + writeAppCfgInvoked: false, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": true, "org2": true}, + }, // valid token for org1, resets that token's flag but not appConfig - {token: validToken, orgName: "org1", wantErr: false, readInvoked: true, writeTokInvoked: true, - writeAppCfgInvoked: false, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": false, "org2": true}}, + { + token: validToken, orgName: "org1", wantErr: false, readInvoked: true, writeTokInvoked: true, + writeAppCfgInvoked: false, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": false, "org2": true}, + }, // valid token again for org1, still no write to appConfig - {token: validToken, orgName: "org1", wantErr: false, readInvoked: true, writeTokInvoked: true, - writeAppCfgInvoked: false, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": false, "org2": true}}, + { + token: validToken, orgName: "org1", wantErr: false, readInvoked: true, writeTokInvoked: true, + writeAppCfgInvoked: false, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": false, "org2": true}, + }, // valid token again for org2, this time resets appConfig - {token: validToken, orgName: "org2", wantErr: false, readInvoked: true, writeTokInvoked: true, - writeAppCfgInvoked: true, wantAppCfgTermsFlag: false, wantToksTermsFlags: map[string]bool{"org1": false, "org2": false}}, + { + token: validToken, orgName: "org2", wantErr: false, readInvoked: true, writeTokInvoked: true, + writeAppCfgInvoked: true, wantAppCfgTermsFlag: false, wantToksTermsFlags: map[string]bool{"org1": false, "org2": false}, + }, } // order of calls is important, and test must not be parallelized as it would @@ -333,6 +368,67 @@ func TestMDMProfileSpecUnmarshalJSON(t *testing.T) { require.Equal(t, "oldpath", p.Path) require.Empty(t, p.Labels) }) + + t.Run("changing labels", func(t *testing.T) { + // When updating AppConfig, we unmarshal the incoming JSON into the existing AppConfig + // struct, see + // https://github.com/fleetdm/fleet/blob/d1144df1318b50482cbd9eb996b863443975f138/server/service/appconfig.go#L334-L335 + // + // But we found there were issues unmarshaling the slice of profile specs where if a key is present in an old + // element but not in the new element (e.g. element[0] of the old slice and element[0] of the + // new slice), both keys were preserved. This test is designed to cover that issue, which + // was addressed in the unmarshal function, see + // https://github.com/fleetdm/fleet/blob/1042702def54f095335d8b42ed5fdcc90468fa0d/server/fleet/mdm.go#L551-L552 + + storedConfig := fleet.AppConfig{ + OrgInfo: fleet.OrgInfo{ + OrgName: "Test", + }, + MDM: fleet.MDM{ + MacOSSettings: fleet.MacOSSettings{ + CustomSettings: []fleet.MDMProfileSpec{ + { + Path: "some-profile-2", + LabelsExcludeAny: []string{"bar"}, + }, + { + Path: "some-profile-1", + LabelsIncludeAll: []string{"foo"}, + }, + }, + }, + }, + } + + incomingConfig := fleet.AppConfig{ + OrgInfo: fleet.OrgInfo{ + OrgName: "Test", + }, + MDM: fleet.MDM{ + MacOSSettings: fleet.MacOSSettings{ + CustomSettings: []fleet.MDMProfileSpec{ + { + Path: "some-profile-1", + LabelsIncludeAll: []string{"foo"}, + }, + { + Path: "some-profile-2", + LabelsIncludeAny: []string{"bar"}, + }, + }, + }, + }, + } + b, err := json.Marshal(incomingConfig) + require.NoError(t, err) + + err = json.Unmarshal(b, &storedConfig) + require.NoError(t, err) + + require.Equal(t, storedConfig.MDM.MacOSSettings.CustomSettings, incomingConfig.MDM.MacOSSettings.CustomSettings) + require.Nil(t, storedConfig.MDM.MacOSSettings.CustomSettings[0].LabelsExcludeAny) // old key should be removed + require.Nil(t, storedConfig.MDM.MacOSSettings.CustomSettings[1].LabelsIncludeAll) // old key should be removed + }) } func TestMDMProfileSpecsMatch(t *testing.T) { diff --git a/server/service/appconfig_test.go b/server/service/appconfig_test.go index 87081047c6..1e601997c4 100644 --- a/server/service/appconfig_test.go +++ b/server/service/appconfig_test.go @@ -1511,7 +1511,8 @@ func TestModifyAppConfigForNDESSCEPProxy(t *testing.T) { svc, ctx = newTestServiceWithConfig(t, ds, fleetConfig, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}}) ctx = viewer.NewContext(ctx, viewer.Viewer{User: admin}) ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, - createdAt time.Time) error { + createdAt time.Time, + ) error { assert.IsType(t, fleet.ActivityAddedNDESSCEPProxy{}, activity) return nil } @@ -1560,7 +1561,8 @@ func TestModifyAppConfigForNDESSCEPProxy(t *testing.T) { scepURL = "https://new.com/mscep/mscep.dll" jsonPayload = fmt.Sprintf(jsonPayloadBase, scepURL, adminURL, username, "") ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, - createdAt time.Time) error { + createdAt time.Time, + ) error { assert.IsType(t, fleet.ActivityEditedNDESSCEPProxy{}, activity) return nil } @@ -1644,7 +1646,8 @@ func TestModifyAppConfigForNDESSCEPProxy(t *testing.T) { // Second, real run. appConfig.Integrations.NDESSCEPProxy.Valid = true ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, - createdAt time.Time) error { + createdAt time.Time, + ) error { assert.IsType(t, fleet.ActivityDeletedNDESSCEPProxy{}, activity) return nil } @@ -1682,5 +1685,4 @@ func TestModifyAppConfigForNDESSCEPProxy(t *testing.T) { ctx = viewer.NewContext(ctx, viewer.Viewer{User: admin}) _, err = svc.ModifyAppConfig(ctx, []byte(jsonPayload), fleet.ApplySpecOptions{}) assert.ErrorContains(t, err, "private key") - } diff --git a/server/service/integration_mdm_profiles_test.go b/server/service/integration_mdm_profiles_test.go index c36c3fe798..f1e2794fcf 100644 --- a/server/service/integration_mdm_profiles_test.go +++ b/server/service/integration_mdm_profiles_test.go @@ -2650,15 +2650,20 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() { // NOTE: label names starting with "-" are sent as "labels_excluding_any" // (and the leading "-" is removed from the name). Names starting with // "!" are sent as the deprecated "labels" field (and the "!" is removed). + // Names starting with a "~" prefix are sent as "labels_include_any" + // (and the leading "~" is removed. addLabelsFields := func(labelNames []string) map[string][]string { - var deprLabels, inclLabels, exclLabels []string + var deprLabels, inclAllLabels, inclAnyLabels, exclLabels []string for _, lbl := range labelNames { - if strings.HasPrefix(lbl, "-") { //nolint:gocritic // ignore ifElseChain + switch { + case strings.HasPrefix(lbl, "~"): + inclAnyLabels = append(inclAnyLabels, strings.TrimPrefix(lbl, "~")) + case strings.HasPrefix(lbl, "-"): exclLabels = append(exclLabels, strings.TrimPrefix(lbl, "-")) - } else if strings.HasPrefix(lbl, "!") { + case strings.HasPrefix(lbl, "!"): deprLabels = append(deprLabels, strings.TrimPrefix(lbl, "!")) - } else { - inclLabels = append(inclLabels, lbl) + default: + inclAllLabels = append(inclAllLabels, lbl) } } @@ -2666,12 +2671,15 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() { if len(deprLabels) > 0 { fields["labels"] = deprLabels } - if len(inclLabels) > 0 { - fields["labels_include_all"] = inclLabels + if len(inclAllLabels) > 0 { + fields["labels_include_all"] = inclAllLabels } if len(exclLabels) > 0 { fields["labels_exclude_any"] = exclLabels } + if len(inclAnyLabels) > 0 { + fields["labels_include_any"] = inclAnyLabels + } return fields } @@ -2875,15 +2883,20 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() { // profiles with invalid mix of labels assertAppleProfile("apple-invalid-profile-with-labels.mobileconfig", "apple-invalid-profile-with-labels", "ident-with-labels", 0, []string{"foo", "!bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`) + assertAppleProfile("apple-invalid-profile-with-labels.mobileconfig", "apple-invalid-profile-with-labels", "ident-with-labels", 0, []string{"foo", "~bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`) assertAppleDeclaration("apple-invalid-decl-with-labels.json", "ident-decl-with-labels", 0, []string{"foo", "-bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`) + assertAppleDeclaration("apple-invalid-decl-with-labels.json", "ident-decl-with-labels", 0, []string{"foo", "~bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`) assertWindowsProfile("win-invalid-profile-with-labels.xml", "./Test", 0, []string{"-foo", "!bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`) + assertWindowsProfile("win-invalid-profile-with-labels.xml", "./Test", 0, []string{"-foo", "~bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`) // profiles with valid labels uuidAppleWithLabel := assertAppleProfile("apple-profile-with-labels.mobileconfig", "apple-profile-with-labels", "ident-with-labels", 0, []string{"!foo"}, http.StatusOK, "") + uuidAppleWithInclAnyLabel := assertAppleProfile("apple-profile-with-incl-any-labels.mobileconfig", "apple-profile-with-incl-any-labels", "ident-with-incl-any-labels", 0, []string{"~foo", "~bar"}, http.StatusOK, "") uuidAppleDDMWithLabel := createAppleDeclaration("apple-decl-with-labels", "ident-decl-with-labels", 0, []string{"foo"}) uuidWindowsWithLabel := assertWindowsProfile("win-profile-with-labels.xml", "./Test", 0, []string{"-foo", "-bar"}, http.StatusOK, "") uuidAppleDDMTeamWithLabel := createAppleDeclaration("apple-team-decl-with-labels", "ident-team-decl-with-labels", testTeam.ID, []string{"-foo"}) uuidWindowsTeamWithLabel := assertWindowsProfile("win-team-profile-with-labels.xml", "./Test", testTeam.ID, []string{"foo", "bar"}, http.StatusOK, "") + uuidWindowsTeamWithInclAnyLabel := assertWindowsProfile("win-team-profile-with-incl-any-labels.xml", "./Test", testTeam.ID, []string{"foo", "bar"}, http.StatusOK, "") // Windows invalid content body, headers := generateNewProfileMultipartRequest(t, "win.xml", []byte("\x00\x01\x02"), s.token, nil) @@ -2923,6 +2936,13 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() { {LabelID: labelFoo.ID, LabelName: labelFoo.Name}, }, }, + { + ProfileUUID: uuidAppleWithInclAnyLabel, Platform: "darwin", Name: "apple-profile-with-incl-any-labels", Identifier: "ident-with-incl-any-labels", TeamID: nil, + LabelsIncludeAny: []fleet.ConfigurationProfileLabel{ + {LabelID: labelBar.ID, LabelName: labelBar.Name}, + {LabelID: labelFoo.ID, LabelName: labelFoo.Name}, + }, + }, { ProfileUUID: uuidWindowsWithLabel, Platform: "windows", Name: "win-profile-with-labels", TeamID: nil, LabelsExcludeAny: []fleet.ConfigurationProfileLabel{ @@ -2943,6 +2963,13 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() { {LabelID: labelFoo.ID, LabelName: labelFoo.Name}, }, }, + { + ProfileUUID: uuidWindowsTeamWithInclAnyLabel, Platform: "windows", Name: "win-team-profile-with-incl-any-labels", TeamID: &testTeam.ID, + LabelsIncludeAll: []fleet.ConfigurationProfileLabel{ + {LabelID: labelBar.ID, LabelName: labelBar.Name}, + {LabelID: labelFoo.ID, LabelName: labelFoo.Name}, + }, + }, } for _, prof := range expectedProfiles { var getResp getMDMConfigProfileResponse @@ -2963,6 +2990,9 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() { sort.Slice(getResp.LabelsExcludeAny, func(i, j int) bool { return getResp.LabelsExcludeAny[i].LabelName < getResp.LabelsExcludeAny[j].LabelName }) + sort.Slice(getResp.LabelsIncludeAny, func(i, j int) bool { + return getResp.LabelsIncludeAny[i].LabelName < getResp.LabelsIncludeAny[j].LabelName + }) require.Equal(t, prof, *getResp.MDMConfigProfilePayload) resp := s.Do("GET", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", prof.ProfileUUID), nil, http.StatusOK, "alt", "media") @@ -3104,6 +3134,8 @@ func (s *integrationMDMTestSuite) TestListMDMConfigProfiles() { require.NoError(t, err) lblBar, err := s.ds.NewLabel(ctx, &fleet.Label{Name: "bar", Query: "select 1"}) require.NoError(t, err) + lblBaz, err := s.ds.NewLabel(ctx, &fleet.Label{Name: "baz", Query: "select 1"}) + require.NoError(t, err) // create a couple profiles (Win and mac) for team 2, and none for team 3 tprof, err := fleet.NewMDMAppleConfigProfile(mcBytesForTest("tF", "tF.identifier", "tF.uuid"), nil) @@ -3131,19 +3163,33 @@ func (s *integrationMDMTestSuite) TestListMDMConfigProfiles() { }, }) require.NoError(t, err) + + // make tm2ProfH a "include-any" label-based profile + tm2ProfH, err := s.ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{ + Name: "tH", + TeamID: &tm2.ID, + SyncML: []byte(``), + LabelsIncludeAny: []fleet.ConfigurationProfileLabel{ + {LabelID: lblBar.ID, LabelName: lblBar.Name}, + {LabelID: lblBaz.ID, LabelName: lblBaz.Name}, + }, + }) + require.NoError(t, err) + // break lblFoo by deleting it require.NoError(t, s.ds.DeleteLabel(ctx, lblFoo.Name)) // test that all fields are correctly returned with team 2 var listResp listMDMConfigProfilesResponse s.DoJSON("GET", "/api/latest/fleet/configuration_profiles", nil, http.StatusOK, &listResp, "team_id", fmt.Sprint(tm2.ID)) - require.Len(t, listResp.Profiles, 2) + require.Len(t, listResp.Profiles, 3) require.NotZero(t, listResp.Profiles[0].CreatedAt) require.NotZero(t, listResp.Profiles[0].UploadedAt) require.NotZero(t, listResp.Profiles[1].CreatedAt) require.NotZero(t, listResp.Profiles[1].UploadedAt) listResp.Profiles[0].CreatedAt, listResp.Profiles[0].UploadedAt = time.Time{}, time.Time{} listResp.Profiles[1].CreatedAt, listResp.Profiles[1].UploadedAt = time.Time{}, time.Time{} + listResp.Profiles[2].CreatedAt, listResp.Profiles[2].UploadedAt = time.Time{}, time.Time{} require.Equal(t, &fleet.MDMConfigProfilePayload{ ProfileUUID: tm2ProfF.ProfileUUID, TeamID: tm2ProfF.TeamID, @@ -3168,6 +3214,17 @@ func (s *integrationMDMTestSuite) TestListMDMConfigProfiles() { {LabelID: 0, LabelName: lblFoo.Name, Broken: true}, }, }, listResp.Profiles[1]) + require.Equal(t, &fleet.MDMConfigProfilePayload{ + ProfileUUID: tm2ProfH.ProfileUUID, + TeamID: tm2ProfH.TeamID, + Name: tm2ProfH.Name, + Platform: "windows", + // labels are ordered by name + LabelsIncludeAny: []fleet.ConfigurationProfileLabel{ + {LabelID: lblBar.ID, LabelName: lblBar.Name}, + {LabelID: lblBaz.ID, LabelName: lblBaz.Name}, + }, + }, listResp.Profiles[2]) // get the specific include-all label-based profile returns the information var getProfResp getMDMConfigProfileResponse @@ -3203,6 +3260,21 @@ func (s *integrationMDMTestSuite) TestListMDMConfigProfiles() { }, }, getProfResp.MDMConfigProfilePayload) + // get the specific include-any label-based profile returns the information + getProfResp = getMDMConfigProfileResponse{} + s.DoJSON("GET", "/api/latest/fleet/mdm/profiles/"+tm2ProfH.ProfileUUID, nil, http.StatusOK, &getProfResp) + getProfResp.CreatedAt, getProfResp.UploadedAt = time.Time{}, time.Time{} + require.Equal(t, &fleet.MDMConfigProfilePayload{ + ProfileUUID: tm2ProfH.ProfileUUID, + TeamID: tm2ProfH.TeamID, + Name: tm2ProfH.Name, + Platform: "windows", + // labels are ordered by name + LabelsIncludeAny: []fleet.ConfigurationProfileLabel{ + {LabelID: lblBar.ID, LabelName: lblBar.Name}, + {LabelID: lblBaz.ID, LabelName: lblBaz.Name}, + }, + }, getProfResp.MDMConfigProfilePayload) // list for a non-existing team returns 404 s.DoJSON("GET", "/api/latest/fleet/configuration_profiles", nil, http.StatusNotFound, &listResp, "team_id", "99999") @@ -3252,7 +3324,7 @@ func (s *integrationMDMTestSuite) TestListMDMConfigProfiles() { { queries: []string{"per_page", "3"}, teamID: &tm2.ID, - wantNames: []string{"tF", "tG"}, + wantNames: []string{"tF", "tG", "tH"}, wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: false}, }, { @@ -4909,6 +4981,134 @@ func (s *integrationMDMTestSuite) TestHostMDMProfilesExcludeLabels() { }) } +func (s *integrationMDMTestSuite) TestMDMProfilesIncludeAnyLabels() { + t := s.T() + ctx := context.Background() + + triggerReconcileProfiles := func() { + s.awaitTriggerProfileSchedule(t) + // this will only mark them as "pending", as the response to confirm + // profile deployment is asynchronous, so we simulate it here by + // updating any "pending" (not NULL) profiles to "verifying" + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + if _, err := q.ExecContext(ctx, `UPDATE host_mdm_apple_profiles SET status = ? WHERE status = ?`, fleet.OSSettingsVerifying, fleet.OSSettingsPending); err != nil { + return err + } + if _, err := q.ExecContext(ctx, `UPDATE host_mdm_apple_declarations SET status = ? WHERE status = ?`, fleet.OSSettingsVerifying, fleet.OSSettingsPending); err != nil { + return err + } + if _, err := q.ExecContext(ctx, `UPDATE host_mdm_windows_profiles SET status = ? WHERE status = ?`, fleet.OSSettingsVerifying, fleet.OSSettingsPending); err != nil { + return err + } + return nil + }) + } + + // run the crons immediately, will create the Fleet-controlled profiles that + // will then be expected to be applied (e.g. com.fleetdm.fleetd.config and + // com.fleetdm.caroot) + // first create the no-team enroll secret (required to create the fleet profiles) + var applyResp applyEnrollSecretSpecResponse + s.DoJSON("POST", "/api/latest/fleet/spec/enroll_secret", + applyEnrollSecretSpecRequest{ + Spec: &fleet.EnrollSecretSpec{Secrets: []*fleet.EnrollSecret{{Secret: "super-global-secret"}}}, + }, http.StatusOK, &applyResp) + s.awaitTriggerProfileSchedule(t) + + // create an Apple and a Windows host + appleHost, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t) + windowsHost, _ := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t) + + // create a few labels, we'll use the first five for "exclude any" profiles and the remaining for "include any" + labels := make([]*fleet.Label, 10) + for i := 0; i < len(labels); i++ { + label, err := s.ds.NewLabel(ctx, &fleet.Label{Name: fmt.Sprintf("label-%d", i), Query: "select 1;"}) + require.NoError(t, err) + labels[i] = label + } + // simulate reporting label results for those hosts + appleHost.LabelUpdatedAt = time.Now() + windowsHost.LabelUpdatedAt = time.Now() + err := s.ds.UpdateHost(ctx, appleHost) + require.NoError(t, err) + err = s.ds.UpdateHost(ctx, windowsHost) + require.NoError(t, err) + + // set up some Apple profiles and declarations and Windows profiles + s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{ + {Name: "A1", Contents: mobileconfigForTest("A1", "A1"), LabelsIncludeAny: []string{labels[0].Name, labels[1].Name}}, + {Name: "W2", Contents: syncMLForTest("./Foo/W2"), LabelsIncludeAny: []string{labels[2].Name, labels[3].Name}}, + {Name: "D3", Contents: declarationForTest("D3"), LabelsIncludeAny: []string{labels[4].Name}}, + }}, http.StatusNoContent) + + // hosts are not members of any label yet, so running the cron applies no labels + s.awaitTriggerProfileSchedule(t) + s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{ + appleHost: { + {Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending}, + {Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending}, + }, + }) + s.assertHostWindowsConfigProfiles(map[*fleet.Host][]fleet.HostMDMWindowsProfile{ + windowsHost: {}, + }) + + // make hosts members of labels [1], [2], [3] and [4], meaning that each of the "include any" + // labels will now match at least one host + err = s.ds.AsyncBatchInsertLabelMembership(ctx, [][2]uint{ + {labels[0].ID, appleHost.ID}, + {labels[1].ID, appleHost.ID}, + {labels[2].ID, appleHost.ID}, + {labels[3].ID, appleHost.ID}, + {labels[4].ID, appleHost.ID}, + {labels[1].ID, windowsHost.ID}, + {labels[2].ID, windowsHost.ID}, + {labels[3].ID, windowsHost.ID}, + {labels[4].ID, windowsHost.ID}, + }) + require.NoError(t, err) + + triggerReconcileProfiles() + s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{ + appleHost: { + {Identifier: "A1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying}, + {Identifier: "D3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying}, + {Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying}, + {Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying}, + }, + }) + s.assertHostWindowsConfigProfiles(map[*fleet.Host][]fleet.HostMDMWindowsProfile{ + windowsHost: { + {Name: "W2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying}, + }, + }) + + // remove membership of labels [2] for Windows, and [1] and [4] for Apple, meaning + // that D3 will be removed on Apple, A1 will remain on Apple because the host is still a member + // of [0], and W2 will remain on Windows because the host is still a member of [3] + err = s.ds.AsyncBatchDeleteLabelMembership(ctx, [][2]uint{ + {labels[1].ID, appleHost.ID}, + {labels[4].ID, appleHost.ID}, + {labels[2].ID, windowsHost.ID}, + }) + require.NoError(t, err) + + s.awaitTriggerProfileSchedule(t) + s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{ + appleHost: { + {Identifier: "A1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying}, + {Identifier: "D3", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending}, + {Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying}, + {Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying}, + }, + }) + s.assertHostWindowsConfigProfiles(map[*fleet.Host][]fleet.HostMDMWindowsProfile{ + windowsHost: { + {Name: "W2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying}, + }, + }) +} + func (s *integrationMDMTestSuite) TestOTAProfile() { t := s.T() ctx := context.Background() diff --git a/server/service/mdm.go b/server/service/mdm.go index ced2a997f2..92524ba1f1 100644 --- a/server/service/mdm.go +++ b/server/service/mdm.go @@ -1304,7 +1304,7 @@ func newMDMConfigProfileEndpoint(ctx context.Context, request interface{}, svc f labels = req.LabelsExcludeAny labelsMode = fleet.LabelsExcludeAny default: - // TODO: should this be the default? + // default include all labels = req.LabelsIncludeAll labelsMode = fleet.LabelsIncludeAll } @@ -1407,14 +1407,13 @@ func (svc *Service) NewMDMWindowsConfigProfile(ctx context.Context, teamID uint, return nil, ctxerr.Wrap(ctx, err, "validating labels") } switch labelsMembershipMode { - case fleet.LabelsIncludeAll: - cp.LabelsIncludeAll = labelMap case fleet.LabelsIncludeAny: cp.LabelsIncludeAny = labelMap case fleet.LabelsExcludeAny: cp.LabelsExcludeAny = labelMap default: - // TODO what happens if mode is not set?s + // default include all + cp.LabelsIncludeAll = labelMap } err = validateWindowsProfileFleetVariables(string(cp.SyncML)) @@ -1593,7 +1592,7 @@ func (svc *Service) BatchSetMDMProfiles( labels := []string{} for i := range profiles { - // from this point on (after this condition), only LabelsIncludeAll or + // from this point on (after this condition), only LabelsIncludeAll, LabelsIncludeAny or // LabelsExcludeAny need to be checked. if len(profiles[i].Labels) > 0 { // must update the struct in the slice directly, because we don't have a From de2dd61c1ffea530f46cbb0b789ac3e336d78179 Mon Sep 17 00:00:00 2001 From: gillespi314 <73313222+gillespi314@users.noreply.github.com> Date: Thu, 14 Nov 2024 08:35:00 -0600 Subject: [PATCH 7/7] Update schema --- server/datastore/mysql/schema.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index 3d77dd9c78..cbcc985cb2 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -1101,9 +1101,9 @@ CREATE TABLE `migration_status_tables` ( `tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `id` (`id`) -) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=328 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=329 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,20241021224359,1,'2020-01-01 01:01:01'),(323,20241022140321,1,'2020-01-01 01:01:01'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'),(327,20241025141856,1,'2020-01-01 01:01:01'); +INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,20241021224359,1,'2020-01-01 01:01:01'),(323,20241022140321,1,'2020-01-01 01:01:01'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'),(327,20241025141856,1,'2020-01-01 01:01:01'),(328,20241030102721,1,'2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `mobile_device_management_solutions` (