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.

- [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` (