mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 17:08:53 +00:00
Feature branch for CP Exclude Labels (#20014)
Feature branch for this story: #17315
This commit is contained in:
commit
2daff642d8
55 changed files with 2998 additions and 946 deletions
5
changes/18849-config-profiles-exclude-labels
Normal file
5
changes/18849-config-profiles-exclude-labels
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
* Added the database migrations to create the new `exclude` column for labels associated with MDM profiles (and declarations).
|
||||
* Added the API changes to support the `labels_include_all` and `labels_exclude_any` fields (and accept the deprecated `labels` field as an alias for `labels_include_all`).
|
||||
* Added `fleetctl gitops` and `fleetctl apply` support for `labels_include_all` and `labels_exclude_any` to configure a custom setting.
|
||||
* Updated the profile reconciliation logic to handle the new "exclude any" labels.
|
||||
* Fix bug where macOS declarations were stuck in "to be removed" state indefinitely.
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
- add UI for uploading custom profiles with a target of hosts that include all/exclude
|
||||
any selected labels
|
||||
|
|
@ -1088,6 +1088,53 @@ func TestTeamSofwareInstallersGitOps(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestCustomSettingsGitOps(t *testing.T) {
|
||||
cases := []struct {
|
||||
file string
|
||||
wantErr string
|
||||
}{
|
||||
{"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_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_unknown_label.yml", `some or all the labels provided don't exist`},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(filepath.Base(c.file), func(t *testing.T) {
|
||||
ds, appCfgPtr, _ := setupFullGitOpsPremiumServer(t)
|
||||
(*appCfgPtr).MDM.EnabledAndConfigured = true
|
||||
(*appCfgPtr).MDM.WindowsEnabledAndConfigured = true
|
||||
labelToIDs := map[string]uint{
|
||||
fleet.BuiltinLabelMacOS14Plus: 1,
|
||||
"A": 2,
|
||||
"B": 3,
|
||||
"C": 4,
|
||||
}
|
||||
ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) {
|
||||
// for this test, recognize labels A, B and C (as well as the built-in macos 14+ one)
|
||||
ret := make(map[string]uint)
|
||||
for _, lbl := range labels {
|
||||
id, ok := labelToIDs[lbl]
|
||||
if ok {
|
||||
ret[lbl] = id
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
_, err := runAppNoChecks([]string{"gitops", "-f", c.file})
|
||||
if c.wantErr == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.ErrorContains(t, err, c.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func startSoftwareInstallerServer(t *testing.T) {
|
||||
// start the web server that will serve the installer
|
||||
b, err := os.ReadFile(filepath.Join("..", "..", "server", "service", "testdata", "software-installers", "ruby.deb"))
|
||||
|
|
|
|||
93
cmd/fleetctl/testdata/gitops/global_macos_custom_settings_valid_deprecated.yml
vendored
Normal file
93
cmd/fleetctl/testdata/gitops/global_macos_custom_settings_valid_deprecated.yml
vendored
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
controls:
|
||||
macos_settings:
|
||||
custom_settings:
|
||||
- path: ./lib/macos-password.mobileconfig
|
||||
labels:
|
||||
- A
|
||||
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
|
||||
99
cmd/fleetctl/testdata/gitops/global_macos_windows_custom_settings_valid.yml
vendored
Normal file
99
cmd/fleetctl/testdata/gitops/global_macos_windows_custom_settings_valid.yml
vendored
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
controls:
|
||||
macos_settings:
|
||||
custom_settings:
|
||||
- path: ./lib/macos-password.mobileconfig
|
||||
labels_include_all:
|
||||
- A
|
||||
- B
|
||||
windows_settings:
|
||||
custom_settings:
|
||||
- path: ./lib/windows-screenlock.xml
|
||||
labels_exclude_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
|
||||
95
cmd/fleetctl/testdata/gitops/global_windows_custom_settings_invalid_label_mix.yml
vendored
Normal file
95
cmd/fleetctl/testdata/gitops/global_windows_custom_settings_invalid_label_mix.yml
vendored
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
controls:
|
||||
windows_settings:
|
||||
custom_settings:
|
||||
- path: ./lib/windows-screenlock.xml
|
||||
labels_include_all:
|
||||
- B
|
||||
labels_exclude_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
|
||||
93
cmd/fleetctl/testdata/gitops/global_windows_custom_settings_unknown_label.yml
vendored
Normal file
93
cmd/fleetctl/testdata/gitops/global_windows_custom_settings_unknown_label.yml
vendored
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
controls:
|
||||
windows_settings:
|
||||
custom_settings:
|
||||
- path: ./lib/windows-screenlock.xml
|
||||
labels_include_all:
|
||||
- ZZZ
|
||||
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
|
||||
21
cmd/fleetctl/testdata/gitops/team_macos_custom_settings_valid_deprecated.yml
vendored
Normal file
21
cmd/fleetctl/testdata/gitops/team_macos_custom_settings_valid_deprecated.yml
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
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:
|
||||
- A
|
||||
- B
|
||||
policies:
|
||||
queries:
|
||||
software:
|
||||
29
cmd/fleetctl/testdata/gitops/team_macos_windows_custom_settings_invalid_labels_mix.yml
vendored
Normal file
29
cmd/fleetctl/testdata/gitops/team_macos_windows_custom_settings_invalid_labels_mix.yml
vendored
Normal file
|
|
@ -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_all:
|
||||
- A
|
||||
labels:
|
||||
- B
|
||||
windows_settings:
|
||||
custom_settings:
|
||||
- path: ./lib/windows-screenlock.xml
|
||||
labels_include_all:
|
||||
- A
|
||||
labels_exclude_any:
|
||||
- C
|
||||
policies:
|
||||
queries:
|
||||
software:
|
||||
25
cmd/fleetctl/testdata/gitops/team_macos_windows_custom_settings_unknown_label.yml
vendored
Normal file
25
cmd/fleetctl/testdata/gitops/team_macos_windows_custom_settings_unknown_label.yml
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
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_all:
|
||||
- ZZZ
|
||||
windows_settings:
|
||||
custom_settings:
|
||||
- path: ./lib/windows-screenlock.xml
|
||||
labels_exclude_any:
|
||||
- ZZZ
|
||||
policies:
|
||||
queries:
|
||||
software:
|
||||
26
cmd/fleetctl/testdata/gitops/team_macos_windows_custom_settings_valid.yml
vendored
Normal file
26
cmd/fleetctl/testdata/gitops/team_macos_windows_custom_settings_valid.yml
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
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_all:
|
||||
- A
|
||||
- B
|
||||
windows_settings:
|
||||
custom_settings:
|
||||
- path: ./lib/windows-screenlock.xml
|
||||
labels_exclude_any:
|
||||
- C
|
||||
policies:
|
||||
queries:
|
||||
software:
|
||||
|
|
@ -1119,7 +1119,7 @@ func (svc *Service) mdmAppleEditedMacOSUpdates(ctx context.Context, teamID *uint
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.Labels = []fleet.ConfigurationProfileLabel{
|
||||
d.LabelsIncludeAll = []fleet.ConfigurationProfileLabel{
|
||||
{LabelName: fleet.BuiltinLabelMacOS14Plus, LabelID: lblIDs[fleet.BuiltinLabelMacOS14Plus]},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -950,6 +950,9 @@ func (svc *Service) createTeamFromSpec(
|
|||
)
|
||||
}
|
||||
|
||||
validateTeamCustomSettings(invalid, "macos", macOSSettings.CustomSettings)
|
||||
validateTeamCustomSettings(invalid, "windows", spec.MDM.WindowsSettings.CustomSettings.Value)
|
||||
|
||||
var hostExpirySettings fleet.HostExpirySettings
|
||||
if spec.HostExpirySettings != nil {
|
||||
if spec.HostExpirySettings.HostExpiryEnabled && spec.HostExpirySettings.HostExpiryWindow <= 0 {
|
||||
|
|
@ -1006,6 +1009,7 @@ func (svc *Service) createTeamFromSpec(
|
|||
WindowsUpdates: spec.MDM.WindowsUpdates,
|
||||
MacOSSettings: macOSSettings,
|
||||
MacOSSetup: macOSSetup,
|
||||
WindowsSettings: spec.MDM.WindowsSettings,
|
||||
},
|
||||
HostExpirySettings: hostExpirySettings,
|
||||
WebhookSettings: fleet.TeamWebhookSettings{
|
||||
|
|
@ -1183,6 +1187,9 @@ func (svc *Service) editTeamFromSpec(
|
|||
team.Config.HostExpirySettings = *spec.HostExpirySettings
|
||||
}
|
||||
|
||||
validateTeamCustomSettings(invalid, "macos", team.Config.MDM.MacOSSettings.CustomSettings)
|
||||
validateTeamCustomSettings(invalid, "windows", team.Config.MDM.WindowsSettings.CustomSettings.Value)
|
||||
|
||||
// If host status webhook is not provided, do not change it
|
||||
if spec.WebhookSettings.HostStatusWebhook != nil {
|
||||
fleet.ValidateEnabledHostStatusIntegrations(*spec.WebhookSettings.HostStatusWebhook, invalid)
|
||||
|
|
@ -1282,6 +1289,25 @@ func (svc *Service) editTeamFromSpec(
|
|||
return nil
|
||||
}
|
||||
|
||||
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} {
|
||||
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))
|
||||
}
|
||||
if len(prof.Labels) > 0 {
|
||||
customSettings[i].LabelsIncludeAll = customSettings[i].Labels
|
||||
customSettings[i].Labels = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *Service) validateTeamCalendarIntegrations(
|
||||
calendarIntegration *fleet.TeamGoogleCalendarIntegration,
|
||||
appCfg *fleet.AppConfig, dryRun bool, invalid *fleet.InvalidArgumentError,
|
||||
|
|
|
|||
|
|
@ -29,6 +29,12 @@ export interface IMacOsMigrationSettings {
|
|||
webhook_url: string;
|
||||
}
|
||||
|
||||
interface ICustomSetting {
|
||||
path: string;
|
||||
labels_include_all?: string[];
|
||||
labels_exclude_any?: string[];
|
||||
}
|
||||
|
||||
export interface IMdmConfig {
|
||||
enable_disk_encryption: boolean;
|
||||
enabled_and_configured: boolean;
|
||||
|
|
@ -42,7 +48,7 @@ export interface IMdmConfig {
|
|||
deadline: string | null;
|
||||
};
|
||||
macos_settings: {
|
||||
custom_settings: null;
|
||||
custom_settings: null | ICustomSetting[];
|
||||
enable_disk_encryption: boolean;
|
||||
};
|
||||
macos_setup: {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { ReactNode } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
export default PropTypes.shape({
|
||||
|
|
@ -10,6 +11,7 @@ export interface IDropdownOption {
|
|||
disabled?: boolean;
|
||||
label: string | JSX.Element;
|
||||
value: string | number;
|
||||
helpText?: ReactNode;
|
||||
premiumOnly?: boolean;
|
||||
tooltipContent?: string | JSX.Element;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,8 @@ export type ProfilePlatform = "darwin" | "windows";
|
|||
|
||||
export interface IProfileLabel {
|
||||
name: string;
|
||||
broken: boolean;
|
||||
id?: number; // id is only present when the label is not broken
|
||||
broken?: boolean;
|
||||
}
|
||||
|
||||
export interface IMdmProfile {
|
||||
|
|
@ -83,7 +84,8 @@ export interface IMdmProfile {
|
|||
created_at: string;
|
||||
updated_at: string;
|
||||
checksum: string | null; // null for windows profiles
|
||||
labels?: IProfileLabel[];
|
||||
labels_include_all?: IProfileLabel[];
|
||||
labels_exclude_any?: IProfileLabel[];
|
||||
}
|
||||
|
||||
export type MdmProfileStatus = "verified" | "verifying" | "pending" | "failed";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
import AddProfileModal from "./components/ProfileUploader/components/AddProfileModal/AddProfileModal";
|
||||
import DeleteProfileModal from "./components/DeleteProfileModal/DeleteProfileModal";
|
||||
import ProfileLabelsModal from "./components/ProfileLabelsModal/ProfileLabelsModal";
|
||||
import ProfileListItem from "./components/ProfileListItem";
|
||||
|
|
@ -101,7 +101,7 @@ const CustomSettings = ({
|
|||
onMutation();
|
||||
renderFlash("success", "Successfully deleted!");
|
||||
} catch (e) {
|
||||
renderFlash("error", "Couldn’t delete. Please try again.");
|
||||
renderFlash("error", "Couldn't delete. Please try again.");
|
||||
} finally {
|
||||
selectedProfile.current = null;
|
||||
setShowDeleteProfileModal(false);
|
||||
|
|
@ -169,6 +169,10 @@ const CustomSettings = ({
|
|||
);
|
||||
};
|
||||
|
||||
const hasLabels =
|
||||
!!profileLabelsModalData?.labels_include_all?.length ||
|
||||
!!profileLabelsModalData?.labels_exclude_any?.length;
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<SectionHeader title="Custom settings" />
|
||||
|
|
@ -189,7 +193,6 @@ const CustomSettings = ({
|
|||
)}
|
||||
{showAddProfileModal && (
|
||||
<AddProfileModal
|
||||
baseClass="add-profile"
|
||||
currentTeamId={currentTeamId}
|
||||
isPremiumTier={!!isPremiumTier}
|
||||
onUpload={onUploadProfile}
|
||||
|
|
@ -204,7 +207,7 @@ const CustomSettings = ({
|
|||
onDelete={onDeleteProfile}
|
||||
/>
|
||||
)}
|
||||
{!!isPremiumTier && !!profileLabelsModalData?.labels?.length && (
|
||||
{isPremiumTier && hasLabels && (
|
||||
<ProfileLabelsModal
|
||||
baseClass={baseClass}
|
||||
profile={profileLabelsModalData}
|
||||
|
|
|
|||
|
|
@ -94,136 +94,11 @@
|
|||
padding: 28.5px 0;
|
||||
}
|
||||
|
||||
&__modal-content-wrap {
|
||||
margin-top: $pad-large;
|
||||
|
||||
.add-profile__file {
|
||||
padding: $pad-medium $pad-large;
|
||||
}
|
||||
}
|
||||
|
||||
&__upload-button {
|
||||
margin-top: 8px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&__file-chooser {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: $pad-medium;
|
||||
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&--button-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: $pad-small;
|
||||
cursor: pointer;
|
||||
height: 38px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
&__selected-file {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
&--details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&--name {
|
||||
font-size: $x-small;
|
||||
font-weight: $bold;
|
||||
}
|
||||
&--platform {
|
||||
font-size: $xx-small;
|
||||
color: $ui-fleet-black-75;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__profile-graphic {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $pad-small;
|
||||
}
|
||||
|
||||
&__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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,25 +6,42 @@ 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,
|
||||
}: {
|
||||
baseClass: string;
|
||||
profileName: string;
|
||||
}) => (
|
||||
<div className={`${baseClass}__description`}>
|
||||
<b>{profileName}</b> will only be applied to hosts that have all these
|
||||
labels:
|
||||
</div>
|
||||
);
|
||||
targetType,
|
||||
}: IModalDescriptionProps) => {
|
||||
const targetTypeText =
|
||||
targetType === "includeAll" ? (
|
||||
<>
|
||||
have <b>all</b>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
don't have <b>any</b>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}__description`}>
|
||||
<b>{profileName}</b> profile only applies to hosts that {targetTypeText}{" "}
|
||||
of these labels:
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const BrokenLabelWarning = () => (
|
||||
<InfoBanner color="yellow">
|
||||
<span>
|
||||
The configuration profile is{" "}
|
||||
<TooltipWrapper
|
||||
tipContent={`It won’t be applied to new hosts because one or more labels are deleted. To apply the profile to new hosts, please delete it and upload a new profile.`}
|
||||
tipContent={`It won't be applied to new hosts because one or more labels are deleted. To apply the profile to new hosts, please delete it and upload a new profile.`}
|
||||
underline
|
||||
>
|
||||
broken
|
||||
|
|
@ -67,7 +84,14 @@ const ProfileLabelsModal = ({
|
|||
profile,
|
||||
setModalData,
|
||||
}: IProfileLabelsModalProps) => {
|
||||
if (!profile?.labels?.length) {
|
||||
if (!profile) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { name, labels_include_all, labels_exclude_any } = profile;
|
||||
const labels = labels_include_all || labels_exclude_any;
|
||||
|
||||
if (!labels?.length) {
|
||||
// caller ensures this never happens
|
||||
return null;
|
||||
}
|
||||
|
|
@ -75,9 +99,13 @@ const ProfileLabelsModal = ({
|
|||
return (
|
||||
<Modal title="Custom target" onExit={() => setModalData(null)}>
|
||||
<div className={`${baseClass}__modal-content-wrap`}>
|
||||
{profile.labels.some((label) => label.broken) && <BrokenLabelWarning />}
|
||||
<ModalDescription baseClass={baseClass} profileName={profile.name} />
|
||||
<LabelsList baseClass={baseClass} labels={profile.labels} />
|
||||
{labels.some((label) => label.broken) && <BrokenLabelWarning />}
|
||||
<ModalDescription
|
||||
baseClass={baseClass}
|
||||
profileName={name}
|
||||
targetType={labels_include_all ? "includeAll" : "excludeAny"}
|
||||
/>
|
||||
<LabelsList baseClass={baseClass} labels={labels} />
|
||||
<div className="modal-cta-wrap">
|
||||
<Button variant="brand" onClick={() => setModalData(null)}>
|
||||
Done
|
||||
|
|
|
|||
|
|
@ -83,7 +83,13 @@ const ProfileListItem = ({
|
|||
onDelete,
|
||||
setProfileLabelsModalData,
|
||||
}: IProfileListItemProps) => {
|
||||
const { created_at, labels, name, platform } = profile;
|
||||
const {
|
||||
created_at,
|
||||
labels_include_all,
|
||||
labels_exclude_any,
|
||||
name,
|
||||
platform,
|
||||
} = profile;
|
||||
const subClass = "list-item";
|
||||
|
||||
const onClickDownload = async () => {
|
||||
|
|
@ -95,6 +101,21 @@ const ProfileListItem = ({
|
|||
FileSaver.saveAs(file);
|
||||
};
|
||||
|
||||
const labels = labels_include_all || labels_exclude_any;
|
||||
|
||||
const renderLabelInfo = () => {
|
||||
if (!isPremium || labels === undefined || labels.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${subClass}__labels`}>
|
||||
{labels?.some((label) => label.broken) && <Icon name="warning" />}
|
||||
<LabelCount className={subClass} count={labels.length} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classnames(subClass, baseClass)}>
|
||||
<div className={`${subClass}__main-content`}>
|
||||
|
|
@ -111,14 +132,9 @@ const ProfileListItem = ({
|
|||
</div>
|
||||
</div>
|
||||
<div className={`${subClass}__actions-wrap`}>
|
||||
{isPremium && !!labels?.length && (
|
||||
<div className={`${subClass}__labels`}>
|
||||
{labels?.some((l) => l.broken) && <Icon name="warning" />}
|
||||
<LabelCount className={subClass} count={labels.length} />
|
||||
</div>
|
||||
)}
|
||||
{renderLabelInfo()}
|
||||
<div className={`${subClass}__actions`}>
|
||||
{isPremium && !!labels?.length && (
|
||||
{isPremium && labels !== undefined && labels.length && (
|
||||
<Button
|
||||
className={`${subClass}__action-button`}
|
||||
variant="text-icon"
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import { ILabelSummary } from "interfaces/label";
|
|||
import labelsAPI from "services/entities/labels";
|
||||
import mdmAPI from "services/entities/mdm";
|
||||
|
||||
// @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
import Button from "components/buttons/Button";
|
||||
import Card from "components/Card";
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
|
|
@ -19,24 +21,28 @@ import Modal from "components/Modal";
|
|||
import Radio from "components/forms/fields/Radio";
|
||||
import Spinner from "components/Spinner";
|
||||
|
||||
import ProfileGraphic from "./AddProfileGraphic";
|
||||
import ProfileGraphic from "../AddProfileGraphic";
|
||||
|
||||
import {
|
||||
DEFAULT_ERROR_MESSAGE,
|
||||
getErrorMessage,
|
||||
parseFile,
|
||||
} from "../../helpers";
|
||||
import {
|
||||
CUSTOM_TARGET_OPTIONS,
|
||||
CustomTargetOption,
|
||||
generateLabelKey,
|
||||
listNamesFromSelectedLabels,
|
||||
} from "../helpers";
|
||||
} from "./helpers";
|
||||
|
||||
const FileChooser = ({
|
||||
baseClass,
|
||||
isLoading,
|
||||
onFileOpen,
|
||||
}: {
|
||||
baseClass: string;
|
||||
const baseClass = "add-profile-modal";
|
||||
|
||||
interface IFileChooserProps {
|
||||
isLoading: boolean;
|
||||
onFileOpen: (files: FileList | null) => void;
|
||||
}) => (
|
||||
}
|
||||
|
||||
const FileChooser = ({ isLoading, onFileOpen }: IFileChooserProps) => (
|
||||
<div className={`${baseClass}__file-chooser`}>
|
||||
<ProfileGraphic baseClass={baseClass} showMessage />
|
||||
<Button
|
||||
|
|
@ -62,19 +68,17 @@ const FileChooser = ({
|
|||
</div>
|
||||
);
|
||||
|
||||
// TODO: if we reuse this one more time, we should consider moving this
|
||||
// into FileUploader as a default preview. Currently we have this in
|
||||
// AddSoftwareForm.tsx and here.
|
||||
const FileDetails = ({
|
||||
baseClass,
|
||||
details: { name, platform },
|
||||
}: {
|
||||
baseClass: string;
|
||||
interface IFileDetailsProps {
|
||||
details: {
|
||||
name: string;
|
||||
platform: string;
|
||||
};
|
||||
}) => (
|
||||
}
|
||||
|
||||
// TODO: if we reuse this one more time, we should consider moving this
|
||||
// into FileUploader as a default preview. Currently we have this in
|
||||
// AddSoftwareForm.tsx and here.
|
||||
const FileDetails = ({ details: { name, platform } }: IFileDetailsProps) => (
|
||||
<div className={`${baseClass}__selected-file`}>
|
||||
<ProfileGraphic baseClass={baseClass} />
|
||||
<div className={`${baseClass}__selected-file--details`}>
|
||||
|
|
@ -86,15 +90,15 @@ const FileDetails = ({
|
|||
</div>
|
||||
);
|
||||
|
||||
const TargetChooser = ({
|
||||
baseClass,
|
||||
selectedTarget,
|
||||
setSelectedTarget,
|
||||
}: {
|
||||
baseClass: string;
|
||||
interface ITargetChooserProps {
|
||||
selectedTarget: string;
|
||||
setSelectedTarget: React.Dispatch<React.SetStateAction<string>>;
|
||||
}) => {
|
||||
}
|
||||
|
||||
const TargetChooser = ({
|
||||
selectedTarget,
|
||||
setSelectedTarget,
|
||||
}: ITargetChooserProps) => {
|
||||
return (
|
||||
<div className={`form-field`}>
|
||||
<div className="form-field__label">Target</div>
|
||||
|
|
@ -120,67 +124,95 @@ const TargetChooser = ({
|
|||
);
|
||||
};
|
||||
|
||||
const LabelChooser = ({
|
||||
baseClass,
|
||||
isError,
|
||||
isLoading,
|
||||
labels,
|
||||
selectedLabels,
|
||||
setSelectedLabels,
|
||||
}: {
|
||||
baseClass: string;
|
||||
interface ILabelChooserProps {
|
||||
isError: boolean;
|
||||
isLoading: boolean;
|
||||
labels: ILabelSummary[];
|
||||
selectedLabels: Record<string, boolean>;
|
||||
customTargetOption: CustomTargetOption;
|
||||
setSelectedLabels: React.Dispatch<
|
||||
React.SetStateAction<Record<string, boolean>>
|
||||
>;
|
||||
}) => {
|
||||
onSelectCustomTargetOption: (val: CustomTargetOption) => void;
|
||||
}
|
||||
|
||||
const LabelChooser = ({
|
||||
isError,
|
||||
isLoading,
|
||||
labels,
|
||||
selectedLabels,
|
||||
customTargetOption,
|
||||
setSelectedLabels,
|
||||
onSelectCustomTargetOption,
|
||||
}: ILabelChooserProps) => {
|
||||
const updateSelectedLabels = useCallback(
|
||||
({ name, value }: { name: string; value: boolean }) => {
|
||||
setSelectedLabels((prevItems) => ({ ...prevItems, [name]: value }));
|
||||
},
|
||||
[setSelectedLabels]
|
||||
);
|
||||
|
||||
const descriptionText =
|
||||
customTargetOption === "labelsIncludeAll" ? (
|
||||
<>
|
||||
Profile will only be applied to hosts that have <b>all</b> these labels:
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Profile will be applied to hosts that don't have <b>any</b> of
|
||||
these labels:{" "}
|
||||
</>
|
||||
);
|
||||
|
||||
const renderLabels = () => {
|
||||
if (isLoading) {
|
||||
return <Spinner centered={false} />;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <DataError />;
|
||||
}
|
||||
|
||||
if (!labels.length) {
|
||||
return (
|
||||
<div className={`${baseClass}__no-labels`}>
|
||||
<b>No labels exist in Fleet</b>
|
||||
<span>Add labels to target specific hosts.</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return labels.map((label) => {
|
||||
return (
|
||||
<div className={`${baseClass}__label`} key={label.name}>
|
||||
<Checkbox
|
||||
className={`${baseClass}__checkbox`}
|
||||
name={label.name}
|
||||
value={!!selectedLabels[label.name]}
|
||||
onChange={updateSelectedLabels}
|
||||
parseTarget
|
||||
/>
|
||||
<div className={`${baseClass}__label-name`}>{label.name}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`${baseClass}__description`}>
|
||||
Profile will only be applied to hosts that have all these labels:
|
||||
</div>
|
||||
<div className={`${baseClass}__checkboxes`}>
|
||||
{isLoading && <Spinner centered={false} />}
|
||||
{!isLoading && isError && <DataError />}
|
||||
{!isLoading && !isError && !labels.length && (
|
||||
<div className={`${baseClass}__no-labels`}>
|
||||
<b>No labels exist in Fleet</b>
|
||||
<span>Add labels to target specific hosts.</span>
|
||||
</div>
|
||||
)}
|
||||
{!isLoading &&
|
||||
!isError &&
|
||||
!!labels.length &&
|
||||
labels.map((label) => {
|
||||
return (
|
||||
<div className={`${baseClass}__label`} key={label.name}>
|
||||
<Checkbox
|
||||
className={`${baseClass}__checkbox`}
|
||||
name={label.name}
|
||||
value={!!selectedLabels[label.name]}
|
||||
onChange={updateSelectedLabels}
|
||||
parseTarget
|
||||
/>
|
||||
<div className={`${baseClass}__label-name`}>{label.name}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
<div className={`${baseClass}__custom-label-chooser`}>
|
||||
<Dropdown
|
||||
value={customTargetOption}
|
||||
options={CUSTOM_TARGET_OPTIONS}
|
||||
searchable={false}
|
||||
onChange={onSelectCustomTargetOption}
|
||||
/>
|
||||
<div className={`${baseClass}__description`}>{descriptionText}</div>
|
||||
<div className={`${baseClass}__checkboxes`}>{renderLabels()}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface IAddProfileModalProps {
|
||||
baseClass: string;
|
||||
currentTeamId: number;
|
||||
isPremiumTier: boolean;
|
||||
onUpload: () => void;
|
||||
|
|
@ -188,7 +220,6 @@ interface IAddProfileModalProps {
|
|||
}
|
||||
|
||||
const AddProfileModal = ({
|
||||
baseClass,
|
||||
currentTeamId,
|
||||
isPremiumTier,
|
||||
onUpload,
|
||||
|
|
@ -205,18 +236,20 @@ const AddProfileModal = ({
|
|||
const [selectedLabels, setSelectedLabels] = useState<Record<string, boolean>>(
|
||||
{}
|
||||
);
|
||||
const [
|
||||
customTargetOption,
|
||||
setCustomTargetOption,
|
||||
] = useState<CustomTargetOption>("labelsIncludeAll");
|
||||
|
||||
const fileRef = useRef<File | null>(null);
|
||||
|
||||
// NOTE: labels are not automatically refetched in the current implementation
|
||||
const {
|
||||
data: labels,
|
||||
isLoading: isLoadingLabels,
|
||||
isFetching: isFetchingLabels,
|
||||
isError: isErrorLabels,
|
||||
// refetch: refetchLabels,
|
||||
} = useQuery<ILabelSummary[], Error>(
|
||||
["custom_labels"], // NOTE: consider adding selectedTarget to the queryKey to refetch labels when target changes
|
||||
["custom_labels"],
|
||||
() =>
|
||||
labelsAPI
|
||||
.summary()
|
||||
|
|
@ -246,10 +279,15 @@ const AddProfileModal = ({
|
|||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const labelKey = generateLabelKey(
|
||||
selectedTarget,
|
||||
customTargetOption,
|
||||
selectedLabels
|
||||
);
|
||||
await mdmAPI.uploadProfile({
|
||||
file,
|
||||
teamId: currentTeamId,
|
||||
labels: listNamesFromSelectedLabels(selectedLabels),
|
||||
...labelKey,
|
||||
});
|
||||
renderFlash("success", "Successfully uploaded!");
|
||||
onUpload();
|
||||
|
|
@ -282,6 +320,10 @@ const AddProfileModal = ({
|
|||
}
|
||||
};
|
||||
|
||||
const onSelectCustomTargetOption = (val: CustomTargetOption) => {
|
||||
setCustomTargetOption(val);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal title="Add profile" onExit={onDone}>
|
||||
<>
|
||||
|
|
@ -291,30 +333,26 @@ const AddProfileModal = ({
|
|||
<div className={`${baseClass}__modal-content-wrap`}>
|
||||
<Card color="gray" className={`${baseClass}__file`}>
|
||||
{!fileDetails ? (
|
||||
<FileChooser
|
||||
baseClass={baseClass}
|
||||
isLoading={isLoading}
|
||||
onFileOpen={onFileOpen}
|
||||
/>
|
||||
<FileChooser isLoading={isLoading} onFileOpen={onFileOpen} />
|
||||
) : (
|
||||
<FileDetails baseClass={baseClass} details={fileDetails} />
|
||||
<FileDetails details={fileDetails} />
|
||||
)}
|
||||
</Card>
|
||||
{isPremiumTier && (
|
||||
<div className={`${baseClass}__target`}>
|
||||
<TargetChooser
|
||||
baseClass={baseClass}
|
||||
selectedTarget={selectedTarget}
|
||||
setSelectedTarget={setSelectedTarget}
|
||||
/>
|
||||
{selectedTarget === "Custom" && (
|
||||
<LabelChooser
|
||||
baseClass={baseClass}
|
||||
customTargetOption={customTargetOption}
|
||||
isError={isErrorLabels}
|
||||
isLoading={isFetchingLabels}
|
||||
labels={labels || []}
|
||||
selectedLabels={selectedLabels}
|
||||
setSelectedLabels={setSelectedLabels}
|
||||
onSelectCustomTargetOption={onSelectCustomTargetOption}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -326,7 +364,6 @@ const AddProfileModal = ({
|
|||
onClick={onFileUpload}
|
||||
isLoading={isLoading}
|
||||
disabled={
|
||||
// TODO: consider adding tooltip to explain why button is disabled
|
||||
(selectedTarget === "Custom" &&
|
||||
!listNamesFromSelectedLabels(selectedLabels).length) ||
|
||||
!fileDetails
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
.add-profile-modal {
|
||||
&__modal-content-wrap {
|
||||
margin-top: $pad-large;
|
||||
|
||||
.add-profile__file {
|
||||
padding: $pad-medium $pad-large;
|
||||
}
|
||||
}
|
||||
|
||||
&__upload-button {
|
||||
margin-top: 8px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&__file-chooser {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: $pad-medium;
|
||||
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&--button-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: $pad-small;
|
||||
cursor: pointer;
|
||||
height: 38px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
&__selected-file {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
&--details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&--name {
|
||||
font-size: $x-small;
|
||||
font-weight: $bold;
|
||||
}
|
||||
|
||||
&--platform {
|
||||
font-size: $xx-small;
|
||||
color: $ui-fleet-black-75;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__profile-graphic {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $pad-small;
|
||||
}
|
||||
|
||||
&__button-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: $pad-medium;
|
||||
}
|
||||
|
||||
&__target {
|
||||
margin: $pad-large 0;
|
||||
}
|
||||
|
||||
&__custom-label-chooser {
|
||||
margin-top: $pad-medium;
|
||||
}
|
||||
|
||||
&__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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import React from "react";
|
||||
|
||||
import { IDropdownOption } from "interfaces/dropdownOption";
|
||||
import { snakeCase } from "lodash";
|
||||
|
||||
export const CUSTOM_TARGET_OPTIONS: IDropdownOption[] = [
|
||||
{
|
||||
value: "labelsIncludeAll",
|
||||
label: "Include all ",
|
||||
helpText: (
|
||||
<>
|
||||
Profile will only be applied to hosts that have <b>all</b> of these
|
||||
labels{" "}
|
||||
</>
|
||||
),
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
value: "labelsExcludeAny",
|
||||
label: "Exclude all",
|
||||
helpText: (
|
||||
<>
|
||||
Profile will be applied to hosts that don't have <b>any</b> of
|
||||
these labels{" "}
|
||||
</>
|
||||
),
|
||||
disabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
export const listNamesFromSelectedLabels = (dict: Record<string, boolean>) => {
|
||||
return Object.entries(dict).reduce((acc, [labelName, isSelected]) => {
|
||||
if (isSelected) {
|
||||
acc.push(labelName);
|
||||
}
|
||||
return acc;
|
||||
}, [] as string[]);
|
||||
};
|
||||
|
||||
export type CustomTargetOption = "labelsIncludeAll" | "labelsExcludeAny";
|
||||
|
||||
export const generateLabelKey = (
|
||||
target: string,
|
||||
customTargetOption: CustomTargetOption,
|
||||
selectedLabels: Record<string, boolean>
|
||||
) => {
|
||||
if (target !== "Custom") {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
[customTargetOption]: listNamesFromSelectedLabels(selectedLabels),
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./AddProfileModal";
|
||||
|
|
@ -2,61 +2,6 @@ import React from "react";
|
|||
import { AxiosResponse } from "axios";
|
||||
import { IApiError } from "interfaces/errors";
|
||||
|
||||
// TODO: mobileconfig parser is a work in progress and not yet used in production
|
||||
// https://developer.apple.com/documentation/devicemanagement/configuring_multiple_devices_using_profiles#3234127
|
||||
const parseMobileconfig = (file: File): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(file);
|
||||
reader.onerror = (error) => {
|
||||
reject(error);
|
||||
};
|
||||
reader.onabort = (error) => {
|
||||
reject(error);
|
||||
};
|
||||
reader.onload = () => {
|
||||
try {
|
||||
// parse mobile as xml
|
||||
const xmlDoc = new DOMParser().parseFromString(
|
||||
reader.result as string,
|
||||
"text/xml"
|
||||
);
|
||||
// check for any parser errors
|
||||
const parserErrors = xmlDoc.getElementsByTagName("parsererror");
|
||||
if (parserErrors.length > 0) {
|
||||
console.warn("parserErrors", parserErrors);
|
||||
throw new Error("Invalid file: parser error");
|
||||
}
|
||||
// get the top-level object, we assume it is the first `<dict>` element in the `<plist>`
|
||||
// https://developer.apple.com/documentation/devicemanagement/toplevel
|
||||
const tlo = xmlDoc.getElementsByTagName("dict")?.[0];
|
||||
if (tlo?.parentElement?.tagName !== "plist") {
|
||||
throw new Error("Invalid file: missing plist");
|
||||
}
|
||||
// get the payload display name from the top-level object, note that there may be other
|
||||
// `<dict>` elements in the `<plist>`, some of which contain `<key>PayloadDisplayName</key>`
|
||||
// elements, but we ignore those for now
|
||||
const pdnKey = Array.from(tlo.children).find(
|
||||
(child) =>
|
||||
child.tagName === "key" &&
|
||||
child.textContent === "PayloadDisplayName"
|
||||
);
|
||||
const pdnVal =
|
||||
(pdnKey?.nextElementSibling?.tagName === "string" &&
|
||||
pdnKey?.nextElementSibling?.textContent) ||
|
||||
"";
|
||||
// if the payload display name is empty, use the file name
|
||||
const result = pdnVal || file.name;
|
||||
console.log("parseMobileconfig result: ", result);
|
||||
resolve(result);
|
||||
} catch (error) {
|
||||
console.error("error", error);
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const parseFile = async (file: File): Promise<[string, string]> => {
|
||||
// get the file name and extension
|
||||
const nameParts = file.name.split(".");
|
||||
|
|
@ -68,14 +13,6 @@ export const parseFile = async (file: File): Promise<[string, string]> => {
|
|||
return [name, "Windows"];
|
||||
}
|
||||
case "mobileconfig": {
|
||||
// // TODO: enable this once mobileconfig parser is vetted
|
||||
// try {
|
||||
// const parsedName = await parseMobileConfig(file);
|
||||
// return [parsedName, "macOS"];
|
||||
// } catch (e) {
|
||||
// console.log("error", e);
|
||||
// return [name, "macOS"];
|
||||
// }
|
||||
return [name, "macOS"];
|
||||
}
|
||||
case "json": {
|
||||
|
|
@ -87,15 +24,6 @@ export const parseFile = async (file: File): Promise<[string, string]> => {
|
|||
}
|
||||
};
|
||||
|
||||
export const listNamesFromSelectedLabels = (dict: Record<string, boolean>) => {
|
||||
return Object.entries(dict).reduce((acc, [labelName, isSelected]) => {
|
||||
if (isSelected) {
|
||||
acc.push(labelName);
|
||||
}
|
||||
return acc;
|
||||
}, [] as string[]);
|
||||
};
|
||||
|
||||
export const DEFAULT_ERROR_MESSAGE =
|
||||
"Couldn’t add configuration profile. Please try again.";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { createMockMdmProfile } from "__mocks__/mdmMock";
|
||||
import {
|
||||
DiskEncryptionStatus,
|
||||
IHostMdmProfile,
|
||||
|
|
@ -45,7 +46,8 @@ export interface IMdmProfilesResponse {
|
|||
export interface IUploadProfileApiParams {
|
||||
file: File;
|
||||
teamId?: number;
|
||||
labels?: string[];
|
||||
labelsIncludeAll?: string[];
|
||||
labelsExcludeAny?: string[];
|
||||
}
|
||||
|
||||
export const isDDMProfile = (profile: IMdmProfile | IHostMdmProfile) => {
|
||||
|
|
@ -62,7 +64,7 @@ export interface IAppleSetupEnrollmentProfileResponse {
|
|||
name: string;
|
||||
uploaded_at: string;
|
||||
// enrollment profile is an object with keys found here https://developer.apple.com/documentation/devicemanagement/profile.
|
||||
enrollment_profile: Record<string, any>;
|
||||
enrollment_profile: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const mdmService = {
|
||||
|
|
@ -97,7 +99,12 @@ const mdmService = {
|
|||
return sendRequest("GET", path);
|
||||
},
|
||||
|
||||
uploadProfile: ({ file, teamId, labels }: IUploadProfileApiParams) => {
|
||||
uploadProfile: ({
|
||||
file,
|
||||
teamId,
|
||||
labelsIncludeAll,
|
||||
labelsExcludeAny,
|
||||
}: IUploadProfileApiParams) => {
|
||||
const { MDM_PROFILES } = endpoints;
|
||||
|
||||
const formData = new FormData();
|
||||
|
|
@ -107,9 +114,15 @@ const mdmService = {
|
|||
formData.append("team_id", teamId.toString());
|
||||
}
|
||||
|
||||
labels?.forEach((label) => {
|
||||
formData.append("labels", label);
|
||||
});
|
||||
if (labelsIncludeAll || labelsExcludeAny) {
|
||||
const labels = labelsIncludeAll || labelsExcludeAny;
|
||||
const labelKey = labelsIncludeAll
|
||||
? "labels_include_all"
|
||||
: "labels_exclude_any";
|
||||
labels?.forEach((label) => {
|
||||
formData.append(labelKey, label);
|
||||
});
|
||||
}
|
||||
|
||||
return sendRequest("POST", MDM_PROFILES, formData);
|
||||
},
|
||||
|
|
@ -272,7 +285,7 @@ const mdmService = {
|
|||
return new Promise((resolve, reject) => {
|
||||
reader.addEventListener("load", () => {
|
||||
try {
|
||||
const body: Record<string, any> = {
|
||||
const body: Record<string, unknown> = {
|
||||
name: file.name,
|
||||
enrollment_profile: JSON.parse(reader.result as string),
|
||||
};
|
||||
|
|
@ -284,7 +297,7 @@ const mdmService = {
|
|||
);
|
||||
} catch {
|
||||
// catches invalid JSON
|
||||
reject("Couldn’t upload. The file should include valid JSON.");
|
||||
reject("Couldn't upload. The file should include valid JSON.");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -72,10 +73,17 @@ INSERT INTO
|
|||
// filled in.
|
||||
profileID, _ = res.LastInsertId()
|
||||
|
||||
for i := range cp.Labels {
|
||||
cp.Labels[i].ProfileUUID = profUUID
|
||||
labels := make([]fleet.ConfigurationProfileLabel, 0, len(cp.LabelsIncludeAll)+len(cp.LabelsExcludeAny))
|
||||
for i := range cp.LabelsIncludeAll {
|
||||
cp.LabelsIncludeAll[i].ProfileUUID = profUUID
|
||||
labels = append(labels, cp.LabelsIncludeAll[i])
|
||||
}
|
||||
if err := batchSetProfileLabelAssociationsDB(ctx, tx, cp.Labels, "darwin"); err != nil {
|
||||
for i := range cp.LabelsExcludeAny {
|
||||
cp.LabelsExcludeAny[i].ProfileUUID = profUUID
|
||||
cp.LabelsExcludeAny[i].Exclude = true
|
||||
labels = append(labels, cp.LabelsExcludeAny[i])
|
||||
}
|
||||
if err := batchSetProfileLabelAssociationsDB(ctx, tx, labels, "darwin"); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "inserting darwin profile label associations")
|
||||
}
|
||||
|
||||
|
|
@ -223,9 +231,12 @@ WHERE
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(labels) > 0 {
|
||||
// ensure we leave Labels nil if there are none
|
||||
res.Labels = labels
|
||||
for _, lbl := range labels {
|
||||
if lbl.Exclude {
|
||||
res.LabelsExcludeAny = append(res.LabelsExcludeAny, lbl)
|
||||
} else {
|
||||
res.LabelsIncludeAll = append(res.LabelsIncludeAll, lbl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -262,9 +273,12 @@ WHERE
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(labels) > 0 {
|
||||
// ensure we leave Labels nil if there are none
|
||||
res.Labels = labels
|
||||
for _, lbl := range labels {
|
||||
if lbl.Exclude {
|
||||
res.LabelsExcludeAny = append(res.LabelsExcludeAny, lbl)
|
||||
} else {
|
||||
res.LabelsIncludeAll = append(res.LabelsIncludeAll, lbl)
|
||||
}
|
||||
}
|
||||
|
||||
return &res, nil
|
||||
|
|
@ -1540,10 +1554,15 @@ ON DUPLICATE KEY UPDATE
|
|||
return ctxerr.Wrapf(ctx, err, "profile %q is in the database but was not incoming", newlyInsertedProf.Identifier)
|
||||
}
|
||||
|
||||
for _, label := range incomingProf.Labels {
|
||||
for _, label := range incomingProf.LabelsIncludeAll {
|
||||
label.ProfileUUID = newlyInsertedProf.ProfileUUID
|
||||
incomingLabels = append(incomingLabels, label)
|
||||
}
|
||||
for _, label := range incomingProf.LabelsExcludeAny {
|
||||
label.ProfileUUID = newlyInsertedProf.ProfileUUID
|
||||
label.Exclude = true
|
||||
incomingLabels = append(incomingLabels, label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1665,12 +1684,12 @@ 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 (?)"))
|
||||
`, fmt.Sprintf(appleMDMProfilesDesiredStateQuery, "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)"))
|
||||
|
||||
// TODO: if a very large number (~65K) of host uuids was matched (via
|
||||
// uuids, teams or profile IDs), could result in too many placeholders (not
|
||||
// an immediate concern).
|
||||
stmt, args, err := sqlx.In(toInstallStmt, uuids, uuids, fleet.MDMOperationTypeRemove)
|
||||
stmt, args, err := sqlx.In(toInstallStmt, uuids, uuids, uuids, fleet.MDMOperationTypeRemove)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "building profiles to install statement")
|
||||
}
|
||||
|
|
@ -1705,6 +1724,7 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB(
|
|||
-- except "remove" operations in any state
|
||||
( hmap.operation_type IS NULL OR hmap.operation_type != ? ) AND
|
||||
-- except "would be removed" profiles if they are a broken label-based profile
|
||||
-- (regardless of if it is an include-all or exclude-any label)
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM mdm_configuration_profile_labels mcpl
|
||||
|
|
@ -1712,12 +1732,12 @@ 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 (?)"))
|
||||
`, fmt.Sprintf(appleMDMProfilesDesiredStateQuery, "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)"))
|
||||
|
||||
// TODO: if a very large number (~65K) of host uuids was matched (via
|
||||
// uuids, teams or profile IDs), could result in too many placeholders (not
|
||||
// an immediate concern). Note that uuids are provided twice.
|
||||
stmt, args, err = sqlx.In(toRemoveStmt, uuids, uuids, uuids, fleet.MDMOperationTypeRemove)
|
||||
stmt, args, err = sqlx.In(toRemoveStmt, uuids, uuids, uuids, uuids, fleet.MDMOperationTypeRemove)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "building profiles to remove statement")
|
||||
}
|
||||
|
|
@ -1867,29 +1887,55 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB(
|
|||
return nil
|
||||
}
|
||||
|
||||
// mdmEntityTypeToTable tracks what table should be used in the templates for
|
||||
// SQL statements based on the given entity type.
|
||||
var mdmEntityTypeToTable = map[string]string{
|
||||
"declaration": "declaration",
|
||||
"profile": "configuration_profile",
|
||||
// mdmEntityTypeToDynamicNames tracks what names should be used in the
|
||||
// templates for SQL statements based on the given entity type. The dynamic
|
||||
// names are deliberately spelled out in full (instead of using an fmt.Sprintf
|
||||
// approach) so that they are greppable in the codebase.
|
||||
var mdmEntityTypeToDynamicNames = map[string]map[string]string{
|
||||
"declaration": {
|
||||
"entityUUIDColumn": "declaration_uuid",
|
||||
"entityIdentifierColumn": "declaration_identifier",
|
||||
"entityNameColumn": "declaration_name",
|
||||
"countEntityLabelsColumn": "count_declaration_labels",
|
||||
"mdmAppleEntityTable": "mdm_apple_declarations",
|
||||
"mdmEntityLabelsTable": "mdm_declaration_labels",
|
||||
"appleEntityUUIDColumn": "apple_declaration_uuid",
|
||||
"hostMDMAppleEntityTable": "host_mdm_apple_declarations",
|
||||
},
|
||||
"profile": {
|
||||
"entityUUIDColumn": "profile_uuid",
|
||||
"entityIdentifierColumn": "profile_identifier",
|
||||
"entityNameColumn": "profile_name",
|
||||
"countEntityLabelsColumn": "count_profile_labels",
|
||||
"mdmAppleEntityTable": "mdm_apple_configuration_profiles",
|
||||
"mdmEntityLabelsTable": "mdm_configuration_profile_labels",
|
||||
"appleEntityUUIDColumn": "apple_profile_uuid",
|
||||
"hostMDMAppleEntityTable": "host_mdm_apple_profiles",
|
||||
},
|
||||
}
|
||||
|
||||
// generateDesiredStateQuery generates a query string that represents the
|
||||
// desired state of an Apple entity based on its type (profile or declaration)
|
||||
func generateDesiredStateQuery(entityType string) string {
|
||||
return fmt.Sprintf(`
|
||||
dynamicNames := mdmEntityTypeToDynamicNames[entityType]
|
||||
if dynamicNames == nil {
|
||||
panic(fmt.Sprintf("unknown entity type %q", entityType))
|
||||
}
|
||||
|
||||
return os.Expand(`
|
||||
-- non label-based entities
|
||||
SELECT
|
||||
mae.%[1]s_uuid,
|
||||
mae.${entityUUIDColumn},
|
||||
h.uuid as host_uuid,
|
||||
h.platform as host_platform,
|
||||
mae.identifier as %[1]s_identifier,
|
||||
mae.name as %[1]s_name,
|
||||
mae.identifier as ${entityIdentifierColumn},
|
||||
mae.name as ${entityNameColumn},
|
||||
mae.checksum as checksum,
|
||||
0 as count_%[1]s_labels,
|
||||
0 as ${countEntityLabelsColumn},
|
||||
0 as count_non_broken_labels,
|
||||
0 as count_host_labels
|
||||
FROM
|
||||
mdm_apple_%[2]ss mae
|
||||
${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
|
||||
|
|
@ -1900,44 +1946,81 @@ func generateDesiredStateQuery(entityType string) string {
|
|||
ne.type = 'Device' AND
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM mdm_%[2]s_labels mel
|
||||
WHERE mel.apple_%[1]s_uuid = mae.%[1]s_uuid
|
||||
FROM ${mdmEntityLabelsTable} mel
|
||||
WHERE mel.${appleEntityUUIDColumn} = mae.${entityUUIDColumn}
|
||||
) AND
|
||||
( %[3]s )
|
||||
( %s )
|
||||
|
||||
UNION
|
||||
|
||||
-- label-based entities where the host is a member of all the labels
|
||||
-- label-based entities where the host is a member of all the labels (include-all).
|
||||
-- by design, "include" labels cannot match if they are broken (the host cannot be
|
||||
-- a member of a deleted label).
|
||||
SELECT
|
||||
mae.%[1]s_uuid,
|
||||
mae.${entityUUIDColumn},
|
||||
h.uuid as host_uuid,
|
||||
h.platform as host_platform,
|
||||
mae.identifier as %[1]s_identifier,
|
||||
mae.name as %[1]s_name,
|
||||
mae.identifier as ${entityIdentifierColumn},
|
||||
mae.name as ${entityNameColumn},
|
||||
mae.checksum as checksum,
|
||||
COUNT(*) as count_%[1]s_labels,
|
||||
COUNT(*) as ${countEntityLabelsColumn},
|
||||
COUNT(mel.label_id) as count_non_broken_labels,
|
||||
COUNT(lm.label_id) as count_host_labels
|
||||
FROM
|
||||
mdm_apple_%[2]ss mae
|
||||
${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 mdm_%[2]s_labels mel
|
||||
ON mel.apple_%[1]s_uuid = mae.%[1]s_uuid
|
||||
JOIN ${mdmEntityLabelsTable} mel
|
||||
ON mel.${appleEntityUUIDColumn} = mae.${entityUUIDColumn} AND mel.exclude = 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
|
||||
( %[3]s )
|
||||
( %s )
|
||||
GROUP BY
|
||||
mae.%[1]s_uuid, h.uuid, h.platform, mae.identifier, mae.name, mae.checksum
|
||||
mae.${entityUUIDColumn}, h.uuid, h.platform, mae.identifier, mae.name, mae.checksum
|
||||
HAVING
|
||||
count_%[1]s_labels > 0 AND count_host_labels = count_%[1]s_labels
|
||||
${countEntityLabelsColumn} > 0 AND count_host_labels = ${countEntityLabelsColumn}
|
||||
|
||||
`, entityType, mdmEntityTypeToTable[entityType], "%s")
|
||||
UNION
|
||||
|
||||
-- label-based entities where the host is NOT a member of any of the labels (exclude-any).
|
||||
-- explicitly ignore profiles with broken excluded labels so that they are never applied.
|
||||
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 = 1
|
||||
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
|
||||
-- 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
|
||||
`, func(s string) string { return dynamicNames[s] })
|
||||
}
|
||||
|
||||
// generateEntitiesToInstallQuery is a set difference between:
|
||||
|
|
@ -1977,20 +2060,25 @@ func generateDesiredStateQuery(entityType string) string {
|
|||
// where one of the labels does not exist anymore, will not be considered for
|
||||
// installation.
|
||||
func generateEntitiesToInstallQuery(entityType string) string {
|
||||
return fmt.Sprintf(`
|
||||
( %[3]s ) as ds
|
||||
LEFT JOIN host_mdm_apple_%[1]ss hmae
|
||||
ON hmae.%[1]s_uuid = ds.%[1]s_uuid AND hmae.host_uuid = ds.host_uuid
|
||||
dynamicNames := mdmEntityTypeToDynamicNames[entityType]
|
||||
if dynamicNames == nil {
|
||||
panic(fmt.Sprintf("unknown entity type %q", entityType))
|
||||
}
|
||||
|
||||
return fmt.Sprintf(os.Expand(`
|
||||
( %s ) as ds
|
||||
LEFT JOIN ${hostMDMAppleEntityTable} hmae
|
||||
ON hmae.${entityUUIDColumn} = ds.${entityUUIDColumn} AND hmae.host_uuid = ds.host_uuid
|
||||
WHERE
|
||||
-- entity has been updated
|
||||
( hmae.checksum != ds.checksum ) OR
|
||||
-- entity in A but not in B
|
||||
( hmae.%[1]s_uuid IS NULL AND hmae.host_uuid IS NULL ) OR
|
||||
( hmae.${entityUUIDColumn} IS NULL AND hmae.host_uuid IS NULL ) OR
|
||||
-- entities in A and B but with operation type "remove"
|
||||
( 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 )
|
||||
`, entityType, mdmEntityTypeToTable[entityType], fmt.Sprintf(generateDesiredStateQuery(entityType), "TRUE", "TRUE"))
|
||||
`, func(s string) string { return dynamicNames[s] }), fmt.Sprintf(generateDesiredStateQuery(entityType), "TRUE", "TRUE", "TRUE"))
|
||||
}
|
||||
|
||||
// generateEntitiesToRemoveQuery is a set difference between:
|
||||
|
|
@ -2020,24 +2108,30 @@ func generateEntitiesToInstallQuery(entityType string) string {
|
|||
// entity but no longer does (and that label-based entity is not "broken"),
|
||||
// the entity will be removed from the host.
|
||||
func generateEntitiesToRemoveQuery(entityType string) string {
|
||||
return fmt.Sprintf(`
|
||||
( %[3]s ) as ds
|
||||
RIGHT JOIN host_mdm_apple_%[1]ss hmae
|
||||
ON hmae.%[1]s_uuid = ds.%[1]s_uuid AND hmae.host_uuid = ds.host_uuid
|
||||
dynamicNames := mdmEntityTypeToDynamicNames[entityType]
|
||||
if dynamicNames == nil {
|
||||
panic(fmt.Sprintf("unknown entity type %q", entityType))
|
||||
}
|
||||
|
||||
return fmt.Sprintf(os.Expand(`
|
||||
( %s ) as ds
|
||||
RIGHT JOIN ${hostMDMAppleEntityTable} hmae
|
||||
ON hmae.${entityUUIDColumn} = ds.${entityUUIDColumn} AND hmae.host_uuid = ds.host_uuid
|
||||
WHERE
|
||||
-- entities that are in B but not in A
|
||||
ds.%[1]s_uuid IS NULL AND ds.host_uuid IS NULL AND
|
||||
ds.${entityUUIDColumn} IS NULL AND ds.host_uuid IS NULL AND
|
||||
-- except "remove" operations in a terminal state or already pending
|
||||
( hmae.operation_type IS NULL OR hmae.operation_type != ? OR hmae.status IS NULL ) AND
|
||||
-- except "would be removed" entities if they are a broken label-based entities
|
||||
-- (regardless of if it is an include-all or exclude-any label)
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM mdm_%[2]s_labels mcpl
|
||||
FROM ${mdmEntityLabelsTable} mcpl
|
||||
WHERE
|
||||
mcpl.apple_%[1]s_uuid = hmae.%[1]s_uuid AND
|
||||
mcpl.${appleEntityUUIDColumn} = hmae.${entityUUIDColumn} AND
|
||||
mcpl.label_id IS NULL
|
||||
)
|
||||
`, entityType, mdmEntityTypeToTable[entityType], fmt.Sprintf(generateDesiredStateQuery(entityType), "TRUE", "TRUE"))
|
||||
`, func(s string) string { return dynamicNames[s] }), fmt.Sprintf(generateDesiredStateQuery(entityType), "TRUE", "TRUE", "TRUE"))
|
||||
}
|
||||
|
||||
func (ds *Datastore) ListMDMAppleProfilesToInstall(ctx context.Context) ([]*fleet.MDMAppleProfilePayload, error) {
|
||||
|
|
@ -3706,10 +3800,15 @@ WHERE
|
|||
return nil, ctxerr.Wrapf(ctx, err, "declaration %q is in the database but was not incoming", newlyInsertedDecl.Name)
|
||||
}
|
||||
|
||||
for _, label := range incomingDecl.Labels {
|
||||
for _, label := range incomingDecl.LabelsIncludeAll {
|
||||
label.ProfileUUID = newlyInsertedDecl.DeclarationUUID
|
||||
incomingLabels = append(incomingLabels, label)
|
||||
}
|
||||
for _, label := range incomingDecl.LabelsExcludeAny {
|
||||
label.ProfileUUID = newlyInsertedDecl.DeclarationUUID
|
||||
label.Exclude = true
|
||||
incomingLabels = append(incomingLabels, label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3806,10 +3905,18 @@ func (ds *Datastore) insertOrUpsertMDMAppleDeclaration(ctx context.Context, insO
|
|||
return ctxerr.Wrap(ctx, err, "reload apple mdm declaration")
|
||||
}
|
||||
|
||||
for i := range declaration.Labels {
|
||||
declaration.Labels[i].ProfileUUID = declUUID
|
||||
labels := make([]fleet.ConfigurationProfileLabel, 0,
|
||||
len(declaration.LabelsIncludeAll)+len(declaration.LabelsExcludeAny))
|
||||
for i := range declaration.LabelsIncludeAll {
|
||||
declaration.LabelsIncludeAll[i].ProfileUUID = declUUID
|
||||
labels = append(labels, declaration.LabelsIncludeAll[i])
|
||||
}
|
||||
if err := batchSetDeclarationLabelAssociationsDB(ctx, tx, declaration.Labels); err != nil {
|
||||
for i := range declaration.LabelsExcludeAny {
|
||||
declaration.LabelsExcludeAny[i].ProfileUUID = declUUID
|
||||
declaration.LabelsExcludeAny[i].Exclude = true
|
||||
labels = append(labels, declaration.LabelsExcludeAny[i])
|
||||
}
|
||||
if err := batchSetDeclarationLabelAssociationsDB(ctx, tx, labels); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "inserting mdm declaration label associations")
|
||||
}
|
||||
|
||||
|
|
@ -3839,11 +3946,12 @@ func batchSetDeclarationLabelAssociationsDB(ctx context.Context, tx sqlx.ExtCont
|
|||
|
||||
upsertStmt := `
|
||||
INSERT INTO mdm_declaration_labels
|
||||
(apple_declaration_uuid, label_id, label_name)
|
||||
(apple_declaration_uuid, label_id, label_name, exclude)
|
||||
VALUES
|
||||
%s
|
||||
ON DUPLICATE KEY UPDATE
|
||||
label_id = VALUES(label_id)
|
||||
label_id = VALUES(label_id),
|
||||
exclude = VALUES(exclude)
|
||||
`
|
||||
|
||||
var (
|
||||
|
|
@ -3859,9 +3967,9 @@ func batchSetDeclarationLabelAssociationsDB(ctx context.Context, tx sqlx.ExtCont
|
|||
insertBuilder.WriteString(",")
|
||||
deleteBuilder.WriteString(",")
|
||||
}
|
||||
insertBuilder.WriteString("(?, ?, ?)")
|
||||
insertBuilder.WriteString("(?, ?, ?, ?)")
|
||||
deleteBuilder.WriteString("(?, ?)")
|
||||
insertParams = append(insertParams, pl.ProfileUUID, pl.LabelID, pl.LabelName)
|
||||
insertParams = append(insertParams, pl.ProfileUUID, pl.LabelID, pl.LabelName, pl.Exclude)
|
||||
deleteParams = append(deleteParams, pl.ProfileUUID, pl.LabelID)
|
||||
|
||||
setProfileUUIDs[pl.ProfileUUID] = struct{}{}
|
||||
|
|
@ -3907,10 +4015,18 @@ FROM
|
|||
host_mdm_apple_declarations hmad
|
||||
JOIN mdm_apple_declarations mad ON hmad.declaration_uuid = mad.declaration_uuid
|
||||
WHERE
|
||||
hmad.host_uuid = ?`
|
||||
hmad.host_uuid = ? AND hmad.operation_type = ?`
|
||||
|
||||
// NOTE: the token generated as part of this query decides if the DDM session
|
||||
// proceeds with sending the declarations - if the token differs from what
|
||||
// the host last applied, it will proceed. That's why we use only the "to be
|
||||
// installed" declarations for the token generation. If some declarations get
|
||||
// removed, then they will be ignored in the token generation, which will
|
||||
// change the token and make the DDM session proceed (and declarations not
|
||||
// sent get removed).
|
||||
|
||||
var res fleet.MDMAppleDDMDeclarationsToken
|
||||
if err := sqlx.GetContext(ctx, ds.reader(ctx), &res, stmt, hostUUID); err != nil {
|
||||
if err := sqlx.GetContext(ctx, ds.reader(ctx), &res, stmt, hostUUID, fleet.MDMOperationTypeInstall); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "get DDM declarations token")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ func testNewMDMAppleConfigProfileLabels(t *testing.T, ds *Datastore) {
|
|||
Identifier: "DummyTestIdentifier",
|
||||
Mobileconfig: dummyMC,
|
||||
TeamID: nil,
|
||||
Labels: []fleet.ConfigurationProfileLabel{
|
||||
LabelsIncludeAll: []fleet.ConfigurationProfileLabel{
|
||||
{LabelName: "foo", LabelID: 1},
|
||||
},
|
||||
}
|
||||
|
|
@ -174,7 +174,7 @@ func testNewMDMAppleConfigProfileLabels(t *testing.T, ds *Datastore) {
|
|||
}
|
||||
label, err = ds.NewLabel(ctx, label)
|
||||
require.NoError(t, err)
|
||||
cp.Labels = []fleet.ConfigurationProfileLabel{
|
||||
cp.LabelsIncludeAll = []fleet.ConfigurationProfileLabel{
|
||||
{LabelName: label.Name, LabelID: label.ID},
|
||||
}
|
||||
prof, err := ds.NewMDMAppleConfigProfile(ctx, cp)
|
||||
|
|
@ -207,11 +207,11 @@ func testNewMDMAppleConfigProfileDuplicateIdentifier(t *testing.T, ds *Datastore
|
|||
storedCP, err := ds.GetMDMAppleConfigProfileByDeprecatedID(ctx, newCP.ProfileID)
|
||||
require.NoError(t, err)
|
||||
checkConfigProfile(t, *newCP, *storedCP)
|
||||
require.Nil(t, storedCP.Labels)
|
||||
require.Nil(t, storedCP.LabelsIncludeAll)
|
||||
storedCP, err = ds.GetMDMAppleConfigProfile(ctx, newCP.ProfileUUID)
|
||||
require.NoError(t, err)
|
||||
checkConfigProfile(t, *newCP, *storedCP)
|
||||
require.Nil(t, storedCP.Labels)
|
||||
require.Nil(t, storedCP.LabelsIncludeAll)
|
||||
|
||||
// create a label-based profile
|
||||
lbl, err := ds.NewLabel(ctx, &fleet.Label{Name: "lbl", Query: "select 1"})
|
||||
|
|
@ -221,7 +221,7 @@ func testNewMDMAppleConfigProfileDuplicateIdentifier(t *testing.T, ds *Datastore
|
|||
Name: "label-based",
|
||||
Identifier: "label-based",
|
||||
Mobileconfig: mobileconfig.Mobileconfig([]byte("LabelTestMobileconfigBytes")),
|
||||
Labels: []fleet.ConfigurationProfileLabel{
|
||||
LabelsIncludeAll: []fleet.ConfigurationProfileLabel{
|
||||
{LabelName: lbl.Name, LabelID: lbl.ID},
|
||||
},
|
||||
}
|
||||
|
|
@ -232,21 +232,21 @@ func testNewMDMAppleConfigProfileDuplicateIdentifier(t *testing.T, ds *Datastore
|
|||
// only included in the uuid one
|
||||
prof, err := ds.GetMDMAppleConfigProfileByDeprecatedID(ctx, labelProf.ProfileID)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, prof.Labels)
|
||||
require.Nil(t, prof.LabelsIncludeAll)
|
||||
prof, err = ds.GetMDMAppleConfigProfile(ctx, labelProf.ProfileUUID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, prof.Labels, 1)
|
||||
require.Equal(t, lbl.Name, prof.Labels[0].LabelName)
|
||||
require.False(t, prof.Labels[0].Broken)
|
||||
require.Len(t, prof.LabelsIncludeAll, 1)
|
||||
require.Equal(t, lbl.Name, prof.LabelsIncludeAll[0].LabelName)
|
||||
require.False(t, prof.LabelsIncludeAll[0].Broken)
|
||||
|
||||
// break the profile by deleting the label
|
||||
require.NoError(t, ds.DeleteLabel(ctx, lbl.Name))
|
||||
|
||||
prof, err = ds.GetMDMAppleConfigProfile(ctx, labelProf.ProfileUUID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, prof.Labels, 1)
|
||||
require.Equal(t, lbl.Name, prof.Labels[0].LabelName)
|
||||
require.True(t, prof.Labels[0].Broken)
|
||||
require.Len(t, prof.LabelsIncludeAll, 1)
|
||||
require.Equal(t, lbl.Name, prof.LabelsIncludeAll[0].LabelName)
|
||||
require.True(t, prof.LabelsIncludeAll[0].Broken)
|
||||
}
|
||||
|
||||
func generateCP(name string, identifier string, teamID uint) *fleet.MDMAppleConfigProfile {
|
||||
|
|
@ -1094,7 +1094,7 @@ func expectAppleDeclarations(
|
|||
|
||||
require.Equal(t, wantD.Name, gotD.Name)
|
||||
require.Equal(t, wantD.Identifier, gotD.Identifier)
|
||||
require.Equal(t, wantD.Labels, gotD.Labels)
|
||||
require.Equal(t, wantD.LabelsIncludeAll, gotD.LabelsIncludeAll)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
|
@ -1263,6 +1263,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".
|
||||
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)
|
||||
|
|
@ -1271,12 +1273,18 @@ func configProfileForTest(t *testing.T, name, identifier, uuid string, labels ..
|
|||
cp.Checksum = sum[:]
|
||||
|
||||
for _, lbl := range labels {
|
||||
cp.Labels = append(cp.Labels, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID})
|
||||
if strings.HasPrefix(lbl.Name, "exclude-") {
|
||||
cp.LabelsExcludeAny = append(cp.LabelsExcludeAny, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID})
|
||||
} else {
|
||||
cp.LabelsIncludeAll = append(cp.LabelsIncludeAll, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID})
|
||||
}
|
||||
}
|
||||
|
||||
return cp
|
||||
}
|
||||
|
||||
// if the label name starts with "exclude-", the label is considered an "exclude-any", otherwise
|
||||
// it is an "include-all".
|
||||
func declForTest(name, identifier, payloadContent string, labels ...*fleet.Label) *fleet.MDMAppleDeclaration {
|
||||
tmpl := `{
|
||||
"Type": "com.apple.configuration.decl%s",
|
||||
|
|
@ -1295,7 +1303,11 @@ func declForTest(name, identifier, payloadContent string, labels ...*fleet.Label
|
|||
}
|
||||
|
||||
for _, l := range labels {
|
||||
decl.Labels = append(decl.Labels, fleet.ConfigurationProfileLabel{LabelName: l.Name, LabelID: l.ID})
|
||||
if strings.HasPrefix(l.Name, "exclude-") {
|
||||
decl.LabelsExcludeAny = append(decl.LabelsExcludeAny, fleet.ConfigurationProfileLabel{LabelName: l.Name, LabelID: l.ID})
|
||||
} else {
|
||||
decl.LabelsIncludeAll = append(decl.LabelsIncludeAll, fleet.ConfigurationProfileLabel{LabelName: l.Name, LabelID: l.ID})
|
||||
}
|
||||
}
|
||||
|
||||
return decl
|
||||
|
|
@ -4858,14 +4870,14 @@ func testSetOrUpdateMDMAppleDDMDeclaration(t *testing.T, ds *Datastore) {
|
|||
|
||||
d1Ori, err := ds.GetMDMAppleDeclaration(ctx, d1.DeclarationUUID)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, d1Ori.Labels)
|
||||
require.Empty(t, d1Ori.LabelsIncludeAll)
|
||||
|
||||
// update d1 with different identifier and labels
|
||||
d1, err = ds.SetOrUpdateMDMAppleDeclaration(ctx, &fleet.MDMAppleDeclaration{
|
||||
Identifier: "i1b",
|
||||
Name: "d1",
|
||||
RawJSON: json.RawMessage(`{"Identifier": "i1b"}`),
|
||||
Labels: []fleet.ConfigurationProfileLabel{{LabelName: l1.Name, LabelID: l1.ID}},
|
||||
Identifier: "i1b",
|
||||
Name: "d1",
|
||||
RawJSON: json.RawMessage(`{"Identifier": "i1b"}`),
|
||||
LabelsIncludeAll: []fleet.ConfigurationProfileLabel{{LabelName: l1.Name, LabelID: l1.ID}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, d1.DeclarationUUID, d1Ori.DeclarationUUID)
|
||||
|
|
@ -4873,39 +4885,39 @@ func testSetOrUpdateMDMAppleDDMDeclaration(t *testing.T, ds *Datastore) {
|
|||
|
||||
d1B, err := ds.GetMDMAppleDeclaration(ctx, d1.DeclarationUUID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, d1B.Labels, 1)
|
||||
require.Equal(t, l1.ID, d1B.Labels[0].LabelID)
|
||||
require.Len(t, d1B.LabelsIncludeAll, 1)
|
||||
require.Equal(t, l1.ID, d1B.LabelsIncludeAll[0].LabelID)
|
||||
|
||||
// update d1 with different label
|
||||
d1, err = ds.SetOrUpdateMDMAppleDeclaration(ctx, &fleet.MDMAppleDeclaration{
|
||||
Identifier: "i1b",
|
||||
Name: "d1",
|
||||
RawJSON: json.RawMessage(`{"Identifier": "i1b"}`),
|
||||
Labels: []fleet.ConfigurationProfileLabel{{LabelName: l2.Name, LabelID: l2.ID}},
|
||||
Identifier: "i1b",
|
||||
Name: "d1",
|
||||
RawJSON: json.RawMessage(`{"Identifier": "i1b"}`),
|
||||
LabelsIncludeAll: []fleet.ConfigurationProfileLabel{{LabelName: l2.Name, LabelID: l2.ID}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, d1.DeclarationUUID, d1Ori.DeclarationUUID)
|
||||
|
||||
d1C, err := ds.GetMDMAppleDeclaration(ctx, d1.DeclarationUUID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, d1C.Labels, 1)
|
||||
require.Equal(t, l2.ID, d1C.Labels[0].LabelID)
|
||||
require.Len(t, d1C.LabelsIncludeAll, 1)
|
||||
require.Equal(t, l2.ID, d1C.LabelsIncludeAll[0].LabelID)
|
||||
|
||||
// update d1tm1 with different identifier and label
|
||||
d1tm1B, err := ds.SetOrUpdateMDMAppleDeclaration(ctx, &fleet.MDMAppleDeclaration{
|
||||
Identifier: "i1b",
|
||||
Name: "d1",
|
||||
TeamID: &tm1.ID,
|
||||
RawJSON: json.RawMessage(`{"Identifier": "i1b"}`),
|
||||
Labels: []fleet.ConfigurationProfileLabel{{LabelName: l1.Name, LabelID: l1.ID}},
|
||||
Identifier: "i1b",
|
||||
Name: "d1",
|
||||
TeamID: &tm1.ID,
|
||||
RawJSON: json.RawMessage(`{"Identifier": "i1b"}`),
|
||||
LabelsIncludeAll: []fleet.ConfigurationProfileLabel{{LabelName: l1.Name, LabelID: l1.ID}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, d1tm1B.DeclarationUUID, d1tm1.DeclarationUUID)
|
||||
|
||||
d1tm1B, err = ds.GetMDMAppleDeclaration(ctx, d1tm1B.DeclarationUUID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, d1tm1B.Labels, 1)
|
||||
require.Equal(t, l1.ID, d1tm1B.Labels[0].LabelID)
|
||||
require.Len(t, d1tm1B.LabelsIncludeAll, 1)
|
||||
require.Equal(t, l1.ID, d1tm1B.LabelsIncludeAll[0].LabelID)
|
||||
|
||||
// delete no-team d1
|
||||
err = ds.DeleteMDMAppleDeclarationByName(ctx, nil, "d1")
|
||||
|
|
|
|||
|
|
@ -125,7 +125,6 @@ func (ds *Datastore) ListMDMConfigProfiles(ctx context.Context, teamID *uint, op
|
|||
|
||||
var profs []*fleet.MDMConfigProfilePayload
|
||||
|
||||
// TODO(roberto): Consider using UNION ALL here, as we know there won't be any duplicates between the tables.
|
||||
const selectStmt = `
|
||||
SELECT
|
||||
profile_uuid,
|
||||
|
|
@ -152,7 +151,7 @@ FROM (
|
|||
team_id = ? AND
|
||||
identifier NOT IN (?)
|
||||
|
||||
UNION
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
profile_uuid,
|
||||
|
|
@ -169,7 +168,7 @@ FROM (
|
|||
team_id = ? AND
|
||||
name NOT IN (?)
|
||||
|
||||
UNION
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
declaration_uuid AS profile_uuid,
|
||||
|
|
@ -249,7 +248,11 @@ FROM (
|
|||
}
|
||||
for _, label := range labels {
|
||||
if prof, ok := profMap[label.ProfileUUID]; ok {
|
||||
prof.Labels = append(prof.Labels, label)
|
||||
if label.Exclude {
|
||||
prof.LabelsExcludeAny = append(prof.LabelsExcludeAny, label)
|
||||
} else {
|
||||
prof.LabelsIncludeAll = append(prof.LabelsIncludeAll, label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -263,7 +266,8 @@ SELECT
|
|||
COALESCE(apple_profile_uuid, windows_profile_uuid) as profile_uuid,
|
||||
label_name,
|
||||
COALESCE(label_id, 0) as label_id,
|
||||
IF(label_id IS NULL, 1, 0) as broken
|
||||
IF(label_id IS NULL, 1, 0) as broken,
|
||||
exclude
|
||||
FROM
|
||||
mdm_configuration_profile_labels mcpl
|
||||
WHERE
|
||||
|
|
@ -274,7 +278,8 @@ SELECT
|
|||
apple_declaration_uuid as profile_uuid,
|
||||
label_name,
|
||||
COALESCE(label_id, 0) as label_id,
|
||||
IF(label_id IS NULL, 1, 0) as broken
|
||||
IF(label_id IS NULL, 1, 0) as broken,
|
||||
exclude
|
||||
FROM
|
||||
mdm_declaration_labels mdl
|
||||
WHERE
|
||||
|
|
@ -282,7 +287,6 @@ WHERE
|
|||
ORDER BY
|
||||
profile_uuid, label_name
|
||||
`
|
||||
|
||||
// ensure there's at least one (non-matching) value in the slice so the IN
|
||||
// clause is valid
|
||||
if len(winProfUUIDs) == 0 {
|
||||
|
|
@ -678,49 +682,83 @@ func (ds *Datastore) GetHostMDMProfilesExpectedForVerification(ctx context.Conte
|
|||
|
||||
func (ds *Datastore) getHostMDMWindowsProfilesExpectedForVerification(ctx context.Context, teamID, hostID uint) (map[string]*fleet.ExpectedMDMProfile, error) {
|
||||
stmt := `
|
||||
-- profiles without labels
|
||||
SELECT
|
||||
name,
|
||||
syncml AS raw_profile,
|
||||
min(mwcp.uploaded_at) AS earliest_install_date,
|
||||
0 AS count_profile_labels,
|
||||
0 AS count_non_broken_labels,
|
||||
0 AS count_host_labels
|
||||
FROM
|
||||
mdm_windows_configuration_profiles mwcp
|
||||
WHERE
|
||||
mwcp.team_id = ?
|
||||
AND NOT EXISTS (
|
||||
mwcp.team_id = ? AND
|
||||
NOT EXISTS (
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
mdm_configuration_profile_labels mcpl
|
||||
WHERE
|
||||
mcpl.apple_profile_uuid = mwcp.profile_uuid)
|
||||
GROUP BY name, syncml
|
||||
UNION
|
||||
SELECT
|
||||
name,
|
||||
syncml AS raw_profile,
|
||||
min(mwcp.uploaded_at) AS earliest_install_date,
|
||||
COUNT(*) AS count_profile_labels,
|
||||
COUNT(lm.label_id) AS count_host_labels
|
||||
FROM
|
||||
mdm_windows_configuration_profiles mwcp
|
||||
JOIN mdm_configuration_profile_labels mcpl ON mcpl.windows_profile_uuid = mwcp.profile_uuid
|
||||
LEFT OUTER JOIN label_membership lm ON lm.label_id = mcpl.label_id
|
||||
AND lm.host_id = ?
|
||||
WHERE
|
||||
mwcp.team_id = ?
|
||||
GROUP BY
|
||||
name, syncml
|
||||
HAVING
|
||||
count_profile_labels > 0
|
||||
AND count_host_labels = count_profile_labels
|
||||
mcpl.windows_profile_uuid = mwcp.profile_uuid
|
||||
)
|
||||
GROUP BY name, syncml
|
||||
|
||||
`
|
||||
UNION
|
||||
|
||||
-- label-based profiles where the host is a member of all the labels (include-all).
|
||||
-- by design, "include" labels cannot match if they are broken (the host cannot be
|
||||
-- a member of a deleted label).
|
||||
SELECT
|
||||
name,
|
||||
syncml AS raw_profile,
|
||||
min(mwcp.uploaded_at) AS earliest_install_date,
|
||||
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 mdm_configuration_profile_labels mcpl
|
||||
ON mcpl.windows_profile_uuid = mwcp.profile_uuid AND mcpl.exclude = 0
|
||||
LEFT OUTER JOIN label_membership lm
|
||||
ON lm.label_id = mcpl.label_id AND lm.host_id = ?
|
||||
WHERE
|
||||
mwcp.team_id = ?
|
||||
GROUP BY
|
||||
name, syncml
|
||||
HAVING
|
||||
count_profile_labels > 0 AND
|
||||
count_host_labels = count_profile_labels
|
||||
|
||||
UNION
|
||||
|
||||
-- label-based entities where the host is NOT a member of any of the labels (exclude-any).
|
||||
-- explicitly ignore profiles with broken excluded labels so that they are never applied.
|
||||
SELECT
|
||||
name,
|
||||
syncml AS raw_profile,
|
||||
min(mwcp.uploaded_at) AS earliest_install_date,
|
||||
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 mdm_configuration_profile_labels mcpl
|
||||
ON mcpl.windows_profile_uuid = mwcp.profile_uuid AND mcpl.exclude = 1
|
||||
LEFT OUTER JOIN label_membership lm
|
||||
ON lm.label_id = mcpl.label_id AND lm.host_id = ?
|
||||
WHERE
|
||||
mwcp.team_id = ?
|
||||
GROUP BY
|
||||
name, syncml
|
||||
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
|
||||
`
|
||||
var profiles []*fleet.ExpectedMDMProfile
|
||||
// Note: teamID provided twice
|
||||
err := sqlx.SelectContext(ctx, ds.reader(ctx), &profiles, stmt, teamID, hostID, teamID)
|
||||
err := sqlx.SelectContext(ctx, ds.reader(ctx), &profiles, stmt, teamID, hostID, teamID, hostID, teamID)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "running query for windows profiles")
|
||||
}
|
||||
|
|
@ -735,9 +773,11 @@ GROUP BY name, syncml
|
|||
|
||||
func (ds *Datastore) getHostMDMAppleProfilesExpectedForVerification(ctx context.Context, teamID, hostID uint) (map[string]*fleet.ExpectedMDMProfile, error) {
|
||||
stmt := `
|
||||
-- profiles without labels
|
||||
SELECT
|
||||
macp.identifier AS identifier,
|
||||
0 AS count_profile_labels,
|
||||
0 AS count_non_broken_labels,
|
||||
0 AS count_host_labels,
|
||||
earliest_install_date
|
||||
FROM
|
||||
|
|
@ -748,49 +788,89 @@ FROM
|
|||
min(uploaded_at) AS earliest_install_date
|
||||
FROM
|
||||
mdm_apple_configuration_profiles
|
||||
GROUP BY
|
||||
checksum) cs ON macp.checksum = cs.checksum
|
||||
GROUP BY checksum
|
||||
) cs ON macp.checksum = cs.checksum
|
||||
WHERE
|
||||
macp.team_id = ?
|
||||
AND NOT EXISTS (
|
||||
macp.team_id = ? AND
|
||||
NOT EXISTS (
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
mdm_configuration_profile_labels mcpl
|
||||
WHERE
|
||||
mcpl.apple_profile_uuid = macp.profile_uuid)
|
||||
UNION
|
||||
-- label-based profiles where the host is a member of all the labels
|
||||
SELECT
|
||||
macp.identifier AS identifier,
|
||||
COUNT(*) AS count_profile_labels,
|
||||
COUNT(lm.label_id) AS count_host_labels,
|
||||
min(earliest_install_date) AS earliest_install_date
|
||||
FROM
|
||||
mdm_apple_configuration_profiles macp
|
||||
JOIN (
|
||||
SELECT
|
||||
checksum,
|
||||
min(uploaded_at) AS earliest_install_date
|
||||
FROM
|
||||
mdm_apple_configuration_profiles
|
||||
GROUP BY
|
||||
checksum) cs ON macp.checksum = cs.checksum
|
||||
JOIN mdm_configuration_profile_labels mcpl ON mcpl.apple_profile_uuid = macp.profile_uuid
|
||||
LEFT OUTER JOIN label_membership lm ON lm.label_id = mcpl.label_id
|
||||
AND lm.host_id = ?
|
||||
WHERE
|
||||
macp.team_id = ?
|
||||
GROUP BY
|
||||
identifier
|
||||
HAVING
|
||||
count_profile_labels > 0
|
||||
AND count_host_labels = count_profile_labels
|
||||
`
|
||||
mcpl.apple_profile_uuid = macp.profile_uuid
|
||||
)
|
||||
|
||||
UNION
|
||||
|
||||
-- label-based profiles where the host is a member of all the labels (include-all)
|
||||
-- by design, "include" labels cannot match if they are broken (the host cannot be
|
||||
-- a member of a deleted label).
|
||||
SELECT
|
||||
macp.identifier AS identifier,
|
||||
COUNT(*) AS count_profile_labels,
|
||||
COUNT(mcpl.label_id) AS count_non_broken_labels,
|
||||
COUNT(lm.label_id) AS count_host_labels,
|
||||
min(earliest_install_date) AS earliest_install_date
|
||||
FROM
|
||||
mdm_apple_configuration_profiles macp
|
||||
JOIN (
|
||||
SELECT
|
||||
checksum,
|
||||
min(uploaded_at) AS earliest_install_date
|
||||
FROM
|
||||
mdm_apple_configuration_profiles
|
||||
GROUP BY checksum
|
||||
) cs ON macp.checksum = cs.checksum
|
||||
JOIN mdm_configuration_profile_labels mcpl
|
||||
ON mcpl.apple_profile_uuid = macp.profile_uuid AND mcpl.exclude = 0
|
||||
LEFT OUTER JOIN label_membership lm
|
||||
ON lm.label_id = mcpl.label_id AND lm.host_id = ?
|
||||
WHERE
|
||||
macp.team_id = ?
|
||||
GROUP BY
|
||||
identifier
|
||||
HAVING
|
||||
count_profile_labels > 0 AND
|
||||
count_host_labels = count_profile_labels
|
||||
|
||||
UNION
|
||||
|
||||
-- label-based entities where the host is NOT a member of any of the labels (exclude-any).
|
||||
-- explicitly ignore profiles with broken excluded labels so that they are never applied.
|
||||
SELECT
|
||||
macp.identifier AS identifier,
|
||||
COUNT(*) AS count_profile_labels,
|
||||
COUNT(mcpl.label_id) AS count_non_broken_labels,
|
||||
COUNT(lm.label_id) AS count_host_labels,
|
||||
min(earliest_install_date) AS earliest_install_date
|
||||
FROM
|
||||
mdm_apple_configuration_profiles macp
|
||||
JOIN (
|
||||
SELECT
|
||||
checksum,
|
||||
min(uploaded_at) AS earliest_install_date
|
||||
FROM
|
||||
mdm_apple_configuration_profiles
|
||||
GROUP BY checksum
|
||||
) cs ON macp.checksum = cs.checksum
|
||||
JOIN mdm_configuration_profile_labels mcpl
|
||||
ON mcpl.apple_profile_uuid = macp.profile_uuid AND mcpl.exclude = 1
|
||||
LEFT OUTER JOIN label_membership lm
|
||||
ON lm.label_id = mcpl.label_id AND lm.host_id = ?
|
||||
WHERE
|
||||
macp.team_id = ?
|
||||
GROUP BY
|
||||
identifier
|
||||
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
|
||||
`
|
||||
|
||||
var rows []*fleet.ExpectedMDMProfile
|
||||
// Note: teamID provided twice
|
||||
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &rows, stmt, teamID, hostID, teamID); err != nil {
|
||||
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &rows, stmt, teamID, hostID, teamID, hostID, teamID); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, fmt.Sprintf("getting expected profiles for host in team %d", teamID))
|
||||
}
|
||||
|
||||
|
|
@ -915,11 +995,12 @@ func batchSetProfileLabelAssociationsDB(
|
|||
|
||||
upsertStmt := `
|
||||
INSERT INTO mdm_configuration_profile_labels
|
||||
(%s_profile_uuid, label_id, label_name)
|
||||
(%s_profile_uuid, label_id, label_name, exclude)
|
||||
VALUES
|
||||
%s
|
||||
ON DUPLICATE KEY UPDATE
|
||||
label_id = VALUES(label_id)
|
||||
label_id = VALUES(label_id),
|
||||
exclude = VALUES(exclude)
|
||||
`
|
||||
|
||||
var (
|
||||
|
|
@ -935,9 +1016,9 @@ func batchSetProfileLabelAssociationsDB(
|
|||
insertBuilder.WriteString(",")
|
||||
deleteBuilder.WriteString(",")
|
||||
}
|
||||
insertBuilder.WriteString("(?, ?, ?)")
|
||||
insertBuilder.WriteString("(?, ?, ?, ?)")
|
||||
deleteBuilder.WriteString("(?, ?)")
|
||||
insertParams = append(insertParams, pl.ProfileUUID, pl.LabelID, pl.LabelName)
|
||||
insertParams = append(insertParams, pl.ProfileUUID, pl.LabelID, pl.LabelName, pl.Exclude)
|
||||
deleteParams = append(deleteParams, pl.ProfileUUID, pl.LabelID)
|
||||
|
||||
setProfileUUIDs[pl.ProfileUUID] = struct{}{}
|
||||
|
|
@ -1045,7 +1126,7 @@ SELECT
|
|||
COALESCE(MAX(hm.fleet_enroll_ref), '') AS enroll_reference,
|
||||
ne.enrolled_from_migration
|
||||
FROM (
|
||||
-- grab only the latest certificate associated with this device
|
||||
-- grab only the latest certificate associated with this device
|
||||
SELECT
|
||||
n1.id,
|
||||
n1.sha256,
|
||||
|
|
@ -1162,14 +1243,14 @@ func (ds *Datastore) GetHostMDMProfileInstallStatus(ctx context.Context, hostUUI
|
|||
}
|
||||
|
||||
selectStmt := fmt.Sprintf(`
|
||||
SELECT
|
||||
SELECT
|
||||
COALESCE(status, ?) as status
|
||||
FROM
|
||||
%s
|
||||
WHERE
|
||||
operation_type = ?
|
||||
AND host_uuid = ?
|
||||
AND %s = ?
|
||||
AND %s = ?
|
||||
`, table, column)
|
||||
|
||||
var status fleet.MDMDeliveryStatus
|
||||
|
|
|
|||
|
|
@ -37,10 +37,7 @@ func TestMDMShared(t *testing.T) {
|
|||
{"TestBulkSetPendingMDMHostProfiles", testBulkSetPendingMDMHostProfiles},
|
||||
{"TestBulkSetPendingMDMHostProfilesBatch2", testBulkSetPendingMDMHostProfilesBatch2},
|
||||
{"TestBulkSetPendingMDMHostProfilesBatch3", testBulkSetPendingMDMHostProfilesBatch3},
|
||||
{
|
||||
"TestGetHostMDMProfilesExpectedForVerification",
|
||||
testGetHostMDMProfilesExpectedForVerification,
|
||||
},
|
||||
{"TestGetHostMDMProfilesExpectedForVerification", testGetHostMDMProfilesExpectedForVerification},
|
||||
{"TestBatchSetProfileLabelAssociations", testBatchSetProfileLabelAssociations},
|
||||
{"TestBatchSetProfilesTransactionError", testBatchSetMDMProfilesTransactionError},
|
||||
{"TestMDMEULA", testMDMEULA},
|
||||
|
|
@ -49,6 +46,7 @@ func TestMDMShared(t *testing.T) {
|
|||
{"TestMDMProfilesSummaryAndHostFilters", testMDMProfilesSummaryAndHostFilters},
|
||||
{"TestIsHostConnectedToFleetMDM", testIsHostConnectedToFleetMDM},
|
||||
{"TestAreHostsConnectedToFleetMDM", testAreHostsConnectedToFleetMDM},
|
||||
{"TestBulkSetPendingMDMHostProfilesExcludeAny", testBulkSetPendingMDMHostProfilesExcludeAny},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
|
|
@ -476,7 +474,7 @@ func testListMDMConfigProfiles(t *testing.T, ds *Datastore) {
|
|||
// create label-based profiles for i==0, meaning CDEF will be label-based
|
||||
acp := *generateCP(string(rune('C'+inc)), string(rune('C'+inc)), 0)
|
||||
if i == 0 {
|
||||
acp.Labels = []fleet.ConfigurationProfileLabel{
|
||||
acp.LabelsIncludeAll = []fleet.ConfigurationProfileLabel{
|
||||
{LabelName: labels[0].Name, LabelID: labels[0].ID},
|
||||
{LabelName: labels[1].Name, LabelID: labels[1].ID},
|
||||
}
|
||||
|
|
@ -486,7 +484,7 @@ func testListMDMConfigProfiles(t *testing.T, ds *Datastore) {
|
|||
|
||||
acp = *generateCP(string(rune('C'+inc+1)), string(rune('C'+inc+1)), team.ID)
|
||||
if i == 0 {
|
||||
acp.Labels = []fleet.ConfigurationProfileLabel{
|
||||
acp.LabelsIncludeAll = []fleet.ConfigurationProfileLabel{
|
||||
{LabelName: labels[2].Name, LabelID: labels[2].ID},
|
||||
{LabelName: labels[3].Name, LabelID: labels[3].ID},
|
||||
}
|
||||
|
|
@ -500,7 +498,7 @@ func testListMDMConfigProfiles(t *testing.T, ds *Datastore) {
|
|||
SyncML: winProf,
|
||||
}
|
||||
if i == 0 {
|
||||
wcp.Labels = []fleet.ConfigurationProfileLabel{
|
||||
wcp.LabelsIncludeAll = []fleet.ConfigurationProfileLabel{
|
||||
{LabelName: labels[4].Name, LabelID: labels[4].ID},
|
||||
{LabelName: labels[5].Name, LabelID: labels[5].ID},
|
||||
}
|
||||
|
|
@ -514,7 +512,7 @@ func testListMDMConfigProfiles(t *testing.T, ds *Datastore) {
|
|||
SyncML: winProf,
|
||||
}
|
||||
if i == 0 {
|
||||
wcp.Labels = []fleet.ConfigurationProfileLabel{
|
||||
wcp.LabelsIncludeAll = []fleet.ConfigurationProfileLabel{
|
||||
{LabelName: labels[6].Name, LabelID: labels[6].ID},
|
||||
{LabelName: labels[7].Name, LabelID: labels[7].ID},
|
||||
}
|
||||
|
|
@ -723,14 +721,14 @@ func testListMDMConfigProfiles(t *testing.T, ds *Datastore) {
|
|||
got[i] = p.Name
|
||||
|
||||
wantProfs := profLabels[p.Name]
|
||||
require.Equal(t, len(wantProfs), len(p.Labels), "profile name: %s", p.Name)
|
||||
require.Equal(t, len(wantProfs), len(p.LabelsIncludeAll), "profile name: %s", p.Name)
|
||||
if len(wantProfs) > 0 {
|
||||
// clear the profile uuids from the labels list
|
||||
for i, l := range p.Labels {
|
||||
for i, l := range p.LabelsIncludeAll {
|
||||
l.ProfileUUID = ""
|
||||
p.Labels[i] = l
|
||||
p.LabelsIncludeAll[i] = l
|
||||
}
|
||||
require.ElementsMatch(t, wantProfs, p.Labels, "profile name: %s", p.Name)
|
||||
require.ElementsMatch(t, wantProfs, p.LabelsIncludeAll, "profile name: %s", p.Name)
|
||||
}
|
||||
}
|
||||
require.Equal(t, got, c.wantNames)
|
||||
|
|
@ -764,6 +762,91 @@ func testBulkSetPendingMDMHostProfilesBatch3(t *testing.T, ds *Datastore) {
|
|||
testBulkSetPendingMDMHostProfiles(t, ds)
|
||||
}
|
||||
|
||||
type anyProfile struct {
|
||||
ProfileUUID string
|
||||
Status *fleet.MDMDeliveryStatus
|
||||
OperationType fleet.MDMOperationType
|
||||
IdentifierOrName string
|
||||
}
|
||||
|
||||
// only asserts the profile ID, status and operation
|
||||
func assertHostProfiles(t *testing.T, ds *Datastore, want map[*fleet.Host][]anyProfile) {
|
||||
ctx := context.Background()
|
||||
for h, wantProfs := range want {
|
||||
var gotProfs []anyProfile
|
||||
|
||||
switch h.Platform {
|
||||
case "windows":
|
||||
profs, err := ds.GetHostMDMWindowsProfiles(ctx, h.UUID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(wantProfs), len(profs), "host uuid: %s", h.UUID)
|
||||
for _, p := range profs {
|
||||
gotProfs = append(gotProfs, anyProfile{
|
||||
ProfileUUID: p.ProfileUUID,
|
||||
Status: p.Status,
|
||||
OperationType: p.OperationType,
|
||||
IdentifierOrName: p.Name,
|
||||
})
|
||||
}
|
||||
default:
|
||||
profs, err := ds.GetHostMDMAppleProfiles(ctx, h.UUID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(wantProfs), len(profs), "host uuid: %s", h.UUID)
|
||||
for _, p := range profs {
|
||||
gotProfs = append(gotProfs, anyProfile{
|
||||
ProfileUUID: p.ProfileUUID,
|
||||
Status: p.Status,
|
||||
OperationType: p.OperationType,
|
||||
IdentifierOrName: p.Identifier,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
sortProfs := func(profs []anyProfile) []anyProfile {
|
||||
sort.Slice(profs, func(i, j int) bool {
|
||||
l, r := profs[i], profs[j]
|
||||
if l.ProfileUUID == r.ProfileUUID {
|
||||
return l.OperationType < r.OperationType
|
||||
}
|
||||
|
||||
// default alphabetical comparison
|
||||
return l.IdentifierOrName < r.IdentifierOrName
|
||||
})
|
||||
return profs
|
||||
}
|
||||
|
||||
gotProfs = sortProfs(gotProfs)
|
||||
wantProfs = sortProfs(wantProfs)
|
||||
for i, wp := range wantProfs {
|
||||
gp := gotProfs[i]
|
||||
require.Equal(
|
||||
t,
|
||||
wp.ProfileUUID,
|
||||
gp.ProfileUUID,
|
||||
"host uuid: %s, prof id or name: %s",
|
||||
h.UUID,
|
||||
gp.IdentifierOrName,
|
||||
)
|
||||
require.Equal(
|
||||
t,
|
||||
wp.Status,
|
||||
gp.Status,
|
||||
"host uuid: %s, prof id or name: %s",
|
||||
h.UUID,
|
||||
gp.IdentifierOrName,
|
||||
)
|
||||
require.Equal(
|
||||
t,
|
||||
wp.OperationType,
|
||||
gp.OperationType,
|
||||
"host uuid: %s, prof id or name: %s",
|
||||
h.UUID,
|
||||
gp.IdentifierOrName,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
|
||||
|
|
@ -775,95 +858,6 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
return ids
|
||||
}
|
||||
|
||||
type anyProfile struct {
|
||||
ProfileUUID string
|
||||
Status *fleet.MDMDeliveryStatus
|
||||
OperationType fleet.MDMOperationType
|
||||
IdentifierOrName string
|
||||
}
|
||||
|
||||
// only asserts the profile ID, status and operation
|
||||
assertHostProfiles := func(want map[*fleet.Host][]anyProfile) {
|
||||
// TODO(mna): it would help readability of this test to capture the "last
|
||||
// state" of this call and accept the diff as the expected result, merging
|
||||
// them together before the assertions. Would need some hackery to clear a
|
||||
// profile from the list.
|
||||
|
||||
for h, wantProfs := range want {
|
||||
var gotProfs []anyProfile
|
||||
|
||||
switch h.Platform {
|
||||
case "windows":
|
||||
profs, err := ds.GetHostMDMWindowsProfiles(ctx, h.UUID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(wantProfs), len(profs), "host uuid: %s", h.UUID)
|
||||
for _, p := range profs {
|
||||
gotProfs = append(gotProfs, anyProfile{
|
||||
ProfileUUID: p.ProfileUUID,
|
||||
Status: p.Status,
|
||||
OperationType: p.OperationType,
|
||||
IdentifierOrName: p.Name,
|
||||
})
|
||||
}
|
||||
default:
|
||||
profs, err := ds.GetHostMDMAppleProfiles(ctx, h.UUID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(wantProfs), len(profs), "host uuid: %s", h.UUID)
|
||||
for _, p := range profs {
|
||||
gotProfs = append(gotProfs, anyProfile{
|
||||
ProfileUUID: p.ProfileUUID,
|
||||
Status: p.Status,
|
||||
OperationType: p.OperationType,
|
||||
IdentifierOrName: p.Identifier,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
sortProfs := func(profs []anyProfile) []anyProfile {
|
||||
sort.Slice(profs, func(i, j int) bool {
|
||||
l, r := profs[i], profs[j]
|
||||
if l.ProfileUUID == r.ProfileUUID {
|
||||
return l.OperationType < r.OperationType
|
||||
}
|
||||
|
||||
// default alphabetical comparison
|
||||
return l.IdentifierOrName < r.IdentifierOrName
|
||||
})
|
||||
return profs
|
||||
}
|
||||
|
||||
gotProfs = sortProfs(gotProfs)
|
||||
wantProfs = sortProfs(wantProfs)
|
||||
for i, wp := range wantProfs {
|
||||
gp := gotProfs[i]
|
||||
require.Equal(
|
||||
t,
|
||||
wp.ProfileUUID,
|
||||
gp.ProfileUUID,
|
||||
"host uuid: %s, prof id or name: %s",
|
||||
h.UUID,
|
||||
gp.IdentifierOrName,
|
||||
)
|
||||
require.Equal(
|
||||
t,
|
||||
wp.Status,
|
||||
gp.Status,
|
||||
"host uuid: %s, prof id or name: %s",
|
||||
h.UUID,
|
||||
gp.IdentifierOrName,
|
||||
)
|
||||
require.Equal(
|
||||
t,
|
||||
wp.OperationType,
|
||||
gp.OperationType,
|
||||
"host uuid: %s, prof id or name: %s",
|
||||
h.UUID,
|
||||
gp.IdentifierOrName,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getProfs := func(teamID *uint) []*fleet.MDMConfigProfilePayload {
|
||||
// TODO(roberto): the docs says that you can pass a comma separated
|
||||
// list of columns to OrderKey, but that doesn't seem to work
|
||||
|
|
@ -947,7 +941,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
allHosts = append(allHosts, windowsHosts...)
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, hostIDsFromHosts(allHosts...), nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assertHostProfiles(map[*fleet.Host][]anyProfile{
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {},
|
||||
darwinHosts[1]: {},
|
||||
darwinHosts[2]: {},
|
||||
|
|
@ -1007,7 +1001,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
// bulk set for all created hosts, enrolled hosts get the no-team profiles
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, hostIDsFromHosts(allHosts...), nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assertHostProfiles(map[*fleet.Host][]anyProfile{
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
{
|
||||
ProfileUUID: globalProfiles[0].ProfileUUID,
|
||||
|
|
@ -1192,7 +1186,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
assertHostProfiles(map[*fleet.Host][]anyProfile{
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
{
|
||||
ProfileUUID: globalProfiles[0].ProfileUUID,
|
||||
|
|
@ -1363,7 +1357,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
[]string{darwinHosts[1].UUID, windowsHosts[1].UUID},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
assertHostProfiles(map[*fleet.Host][]anyProfile{
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
{
|
||||
ProfileUUID: globalProfiles[0].ProfileUUID,
|
||||
|
|
@ -1519,7 +1513,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
// update status of the affected team
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{team1.ID}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assertHostProfiles(map[*fleet.Host][]anyProfile{
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
{
|
||||
ProfileUUID: globalProfiles[0].ProfileUUID,
|
||||
|
|
@ -1710,7 +1704,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{team1.ID}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
assertHostProfiles(map[*fleet.Host][]anyProfile{
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
{
|
||||
ProfileUUID: tm1Profiles[0].ProfileUUID,
|
||||
|
|
@ -1871,7 +1865,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{team1.ID}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
assertHostProfiles(map[*fleet.Host][]anyProfile{
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
{
|
||||
ProfileUUID: newTm1Profiles[0].ProfileUUID,
|
||||
|
|
@ -2041,7 +2035,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, ds.MDMAppleStoreDDMStatusReport(ctx, darwinHosts[1].UUID, nil))
|
||||
require.NoError(t, ds.MDMAppleStoreDDMStatusReport(ctx, darwinHosts[2].UUID, nil))
|
||||
|
||||
assertHostProfiles(map[*fleet.Host][]anyProfile{
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
{
|
||||
ProfileUUID: globalProfiles[4].ProfileUUID,
|
||||
|
|
@ -2172,7 +2166,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{newDarwinProfileUUID}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
assertHostProfiles(map[*fleet.Host][]anyProfile{
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
{
|
||||
ProfileUUID: globalProfiles[4].ProfileUUID,
|
||||
|
|
@ -2283,7 +2277,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{newWindowsProfileUUID}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
assertHostProfiles(map[*fleet.Host][]anyProfile{
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
{
|
||||
ProfileUUID: globalProfiles[4].ProfileUUID,
|
||||
|
|
@ -2413,7 +2407,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{team2.ID, 0}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
assertHostProfiles(map[*fleet.Host][]anyProfile{
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
{
|
||||
ProfileUUID: globalProfiles[4].ProfileUUID,
|
||||
|
|
@ -2620,7 +2614,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
assertHostProfiles(map[*fleet.Host][]anyProfile{
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
{
|
||||
ProfileUUID: globalProfiles[4].ProfileUUID,
|
||||
|
|
@ -2795,7 +2789,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
assertHostProfiles(map[*fleet.Host][]anyProfile{
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
{
|
||||
ProfileUUID: globalProfiles[4].ProfileUUID,
|
||||
|
|
@ -2999,7 +2993,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
assertHostProfiles(map[*fleet.Host][]anyProfile{
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
{
|
||||
ProfileUUID: globalProfiles[4].ProfileUUID,
|
||||
|
|
@ -3218,7 +3212,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// nothing changes - broken label-based profiles are simply ignored
|
||||
assertHostProfiles(map[*fleet.Host][]anyProfile{
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
{
|
||||
ProfileUUID: globalProfiles[4].ProfileUUID,
|
||||
|
|
@ -3433,7 +3427,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
assertHostProfiles(map[*fleet.Host][]anyProfile{
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
{
|
||||
ProfileUUID: globalProfiles[4].ProfileUUID,
|
||||
|
|
@ -3646,7 +3640,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
assertHostProfiles(map[*fleet.Host][]anyProfile{
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
{
|
||||
ProfileUUID: globalProfiles[4].ProfileUUID,
|
||||
|
|
@ -3855,7 +3849,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{team2.ID}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
assertHostProfiles(map[*fleet.Host][]anyProfile{
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
{
|
||||
ProfileUUID: globalProfiles[4].ProfileUUID,
|
||||
|
|
@ -4054,7 +4048,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{team2.ID}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
assertHostProfiles(map[*fleet.Host][]anyProfile{
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
{
|
||||
ProfileUUID: globalProfiles[4].ProfileUUID,
|
||||
|
|
@ -4264,7 +4258,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{team2.ID}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
assertHostProfiles(map[*fleet.Host][]anyProfile{
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
{
|
||||
ProfileUUID: globalProfiles[4].ProfileUUID,
|
||||
|
|
@ -4479,7 +4473,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{team2.ID}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
assertHostProfiles(map[*fleet.Host][]anyProfile{
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
{
|
||||
ProfileUUID: globalProfiles[4].ProfileUUID,
|
||||
|
|
@ -4684,7 +4678,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{team2.ID}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
assertHostProfiles(map[*fleet.Host][]anyProfile{
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
{
|
||||
ProfileUUID: globalProfiles[4].ProfileUUID,
|
||||
|
|
@ -4886,7 +4880,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
assertHostProfiles(map[*fleet.Host][]anyProfile{
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
{
|
||||
ProfileUUID: globalProfiles[4].ProfileUUID,
|
||||
|
|
@ -5824,8 +5818,9 @@ func testBatchSetProfileLabelAssociations(t *testing.T, ds *Datastore) {
|
|||
t,
|
||||
batchSetProfileLabelAssociationsDB(ctx, ds.writer(ctx), wantOtherWin, "windows"),
|
||||
)
|
||||
// make it an "exclude" label on the other macos profile
|
||||
wantOtherMac := []fleet.ConfigurationProfileLabel{
|
||||
{ProfileUUID: otherMacProfile.ProfileUUID, LabelName: label.Name, LabelID: label.ID},
|
||||
{ProfileUUID: otherMacProfile.ProfileUUID, LabelName: label.Name, LabelID: label.ID, Exclude: true},
|
||||
}
|
||||
require.NoError(
|
||||
t,
|
||||
|
|
@ -5839,17 +5834,13 @@ func testBatchSetProfileLabelAssociations(t *testing.T, ds *Datastore) {
|
|||
|
||||
for platform, uuid := range platforms {
|
||||
expectLabels := func(t *testing.T, profUUID, platform string, want []fleet.ConfigurationProfileLabel) {
|
||||
if len(want) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
p := platform
|
||||
if p == "darwin" {
|
||||
p = "apple"
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(
|
||||
"SELECT %s_profile_uuid as profile_uuid, label_id, label_name FROM mdm_configuration_profile_labels WHERE %s_profile_uuid = ?",
|
||||
"SELECT %s_profile_uuid as profile_uuid, label_id, label_name, exclude FROM mdm_configuration_profile_labels WHERE %s_profile_uuid = ?",
|
||||
p,
|
||||
p,
|
||||
)
|
||||
|
|
@ -5888,6 +5879,19 @@ func testBatchSetProfileLabelAssociations(t *testing.T, ds *Datastore) {
|
|||
// does not change other profiles
|
||||
expectLabels(t, otherWinProfile.ProfileUUID, "windows", wantOtherWin)
|
||||
expectLabels(t, otherMacProfile.ProfileUUID, "darwin", wantOtherMac)
|
||||
|
||||
// now set it with Exclude mode
|
||||
profileLabels = []fleet.ConfigurationProfileLabel{
|
||||
{ProfileUUID: uuid, LabelName: label.Name, LabelID: label.ID, Exclude: true},
|
||||
}
|
||||
err = ds.withTx(ctx, func(tx sqlx.ExtContext) error {
|
||||
return batchSetProfileLabelAssociationsDB(ctx, tx, profileLabels, platform)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
expectLabels(t, uuid, platform, profileLabels)
|
||||
// does not change other profiles
|
||||
expectLabels(t, otherWinProfile.ProfileUUID, "windows", wantOtherWin)
|
||||
expectLabels(t, otherMacProfile.ProfileUUID, "darwin", wantOtherMac)
|
||||
})
|
||||
|
||||
t.Run("invalid profile UUID "+platform, func(t *testing.T) {
|
||||
|
|
@ -5933,8 +5937,8 @@ func testBatchSetProfileLabelAssociations(t *testing.T, ds *Datastore) {
|
|||
|
||||
// apply a batch set with the new label
|
||||
profileLabels := []fleet.ConfigurationProfileLabel{
|
||||
{ProfileUUID: uuid, LabelName: label.Name, LabelID: label.ID},
|
||||
{ProfileUUID: uuid, LabelName: newLabel.Name, LabelID: newLabel.ID},
|
||||
{ProfileUUID: uuid, LabelName: label.Name, LabelID: label.ID, Exclude: true},
|
||||
{ProfileUUID: uuid, LabelName: newLabel.Name, LabelID: newLabel.ID, Exclude: true},
|
||||
}
|
||||
err = ds.withTx(ctx, func(tx sqlx.ExtContext) error {
|
||||
return batchSetProfileLabelAssociationsDB(ctx, tx, profileLabels, platform)
|
||||
|
|
@ -5943,7 +5947,7 @@ func testBatchSetProfileLabelAssociations(t *testing.T, ds *Datastore) {
|
|||
// both are stored in the DB
|
||||
expectLabels(t, uuid, platform, profileLabels)
|
||||
|
||||
// batch apply again without the newLabel
|
||||
// batch apply again without the newLabel, and without Exclude flag
|
||||
profileLabels = []fleet.ConfigurationProfileLabel{
|
||||
{ProfileUUID: uuid, LabelName: label.Name, LabelID: label.ID},
|
||||
}
|
||||
|
|
@ -6911,3 +6915,263 @@ func testIsHostConnectedToFleetMDM(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
require.True(t, connected)
|
||||
}
|
||||
|
||||
func testBulkSetPendingMDMHostProfilesExcludeAny(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
|
||||
// create some "exclude" labels
|
||||
var labels []*fleet.Label
|
||||
for i := 0; i < 6; i++ {
|
||||
lbl, err := ds.NewLabel(ctx, &fleet.Label{Name: "exclude-label-" + strconv.Itoa(i), Query: "select 1"})
|
||||
require.NoError(t, err)
|
||||
labels = append(labels, lbl)
|
||||
}
|
||||
|
||||
// create an Apple profile, a Windows profile and an Apple Declaration with excluded labels
|
||||
appleProfs := []*fleet.MDMAppleConfigProfile{
|
||||
configProfileForTest(t, "A1", "A1", uuid.NewString(), labels[0], labels[1]),
|
||||
}
|
||||
windowsProfs := []*fleet.MDMWindowsConfigProfile{
|
||||
windowsConfigProfileForTest(t, "W1", "W1", labels[2]),
|
||||
}
|
||||
appleDecls := []*fleet.MDMAppleDeclaration{
|
||||
declForTest("D1", "D1", "{}", labels[3], labels[4], labels[5]),
|
||||
}
|
||||
|
||||
err := ds.BatchSetMDMProfiles(ctx, nil, appleProfs, windowsProfs, appleDecls)
|
||||
require.NoError(t, err)
|
||||
|
||||
// must reload them to get the profile/declaration uuid
|
||||
getProfs := func(teamID *uint) []*fleet.MDMConfigProfilePayload {
|
||||
// TODO(roberto): the docs says that you can pass a comma separated
|
||||
// list of columns to OrderKey, but that doesn't seem to work
|
||||
profs, _, err := ds.ListMDMConfigProfiles(ctx, teamID, fleet.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
sort.Slice(profs, func(i, j int) bool {
|
||||
l, r := profs[i], profs[j]
|
||||
if l.Platform != r.Platform {
|
||||
return l.Platform < r.Platform
|
||||
}
|
||||
|
||||
return l.Name < r.Name
|
||||
})
|
||||
return profs
|
||||
}
|
||||
allProfs := getProfs(nil)
|
||||
|
||||
// create an Apple and Windows hosts, not members of any host
|
||||
var i int
|
||||
winHost, err := ds.NewHost(ctx, &fleet.Host{
|
||||
Hostname: fmt.Sprintf("win-host%d-name", i),
|
||||
OsqueryHostID: ptr.String(fmt.Sprintf("osquery-%d", i)),
|
||||
NodeKey: ptr.String(fmt.Sprintf("nodekey-%d", i)),
|
||||
UUID: fmt.Sprintf("win-uuid-%d", i),
|
||||
Platform: "windows",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
windowsEnroll(t, ds, winHost)
|
||||
|
||||
i++
|
||||
appleHost, err := ds.NewHost(ctx, &fleet.Host{
|
||||
Hostname: fmt.Sprintf("apple-host%d-name", i),
|
||||
OsqueryHostID: ptr.String(fmt.Sprintf("osquery-%d", i)),
|
||||
NodeKey: ptr.String(fmt.Sprintf("nodekey-%d", i)),
|
||||
UUID: fmt.Sprintf("apple-uuid-%d", i),
|
||||
Platform: "darwin",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
nanoEnroll(t, ds, appleHost, false)
|
||||
|
||||
// do a sync, they get all platform-specific profiles since they are not part
|
||||
// of any label
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, []uint{winHost.ID, appleHost.ID}, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
appleHost: {
|
||||
{
|
||||
ProfileUUID: allProfs[0].ProfileUUID,
|
||||
Status: &fleet.MDMDeliveryPending,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
IdentifierOrName: allProfs[0].Identifier,
|
||||
},
|
||||
{
|
||||
ProfileUUID: allProfs[1].ProfileUUID,
|
||||
Status: &fleet.MDMDeliveryPending,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
IdentifierOrName: allProfs[1].Identifier,
|
||||
},
|
||||
},
|
||||
winHost: {
|
||||
{
|
||||
ProfileUUID: allProfs[2].ProfileUUID,
|
||||
Status: &fleet.MDMDeliveryPending,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
IdentifierOrName: allProfs[2].Name,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// make all hosts members of labels[1], [2], and [3] so that all profiles are
|
||||
// excluded
|
||||
err = ds.AsyncBatchInsertLabelMembership(ctx, [][2]uint{
|
||||
{labels[1].ID, appleHost.ID},
|
||||
{labels[2].ID, appleHost.ID},
|
||||
{labels[3].ID, appleHost.ID},
|
||||
{labels[1].ID, winHost.ID},
|
||||
{labels[2].ID, winHost.ID},
|
||||
{labels[3].ID, winHost.ID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, []uint{winHost.ID, appleHost.ID}, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
appleHost: {
|
||||
{
|
||||
ProfileUUID: allProfs[0].ProfileUUID,
|
||||
Status: &fleet.MDMDeliveryPending,
|
||||
OperationType: fleet.MDMOperationTypeRemove,
|
||||
IdentifierOrName: allProfs[0].Identifier,
|
||||
},
|
||||
{
|
||||
ProfileUUID: allProfs[1].ProfileUUID,
|
||||
Status: &fleet.MDMDeliveryPending,
|
||||
OperationType: fleet.MDMOperationTypeRemove,
|
||||
IdentifierOrName: allProfs[1].Identifier,
|
||||
},
|
||||
},
|
||||
// windows profiles are directly deleted without a pending state (there's no on-host removal of profiles)
|
||||
winHost: {},
|
||||
})
|
||||
|
||||
// make apple host member of labels[2], and windows host member of [3], which are irrelevant
|
||||
// for their platforms' profiles, so they get all profiles
|
||||
err = ds.AsyncBatchDeleteLabelMembership(ctx, [][2]uint{
|
||||
{labels[1].ID, appleHost.ID},
|
||||
{labels[3].ID, appleHost.ID},
|
||||
{labels[1].ID, winHost.ID},
|
||||
{labels[2].ID, winHost.ID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, []uint{winHost.ID, appleHost.ID}, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
appleHost: {
|
||||
{
|
||||
ProfileUUID: allProfs[0].ProfileUUID,
|
||||
Status: &fleet.MDMDeliveryPending,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
IdentifierOrName: allProfs[0].Identifier,
|
||||
},
|
||||
{
|
||||
ProfileUUID: allProfs[1].ProfileUUID,
|
||||
Status: &fleet.MDMDeliveryPending,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
IdentifierOrName: allProfs[1].Identifier,
|
||||
},
|
||||
},
|
||||
winHost: {
|
||||
{
|
||||
ProfileUUID: allProfs[2].ProfileUUID,
|
||||
Status: &fleet.MDMDeliveryPending,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
IdentifierOrName: allProfs[2].Name,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// delete labels 0, 2 and 3, breaking all profiles
|
||||
err = ds.DeleteLabel(ctx, labels[0].Name)
|
||||
require.NoError(t, err)
|
||||
err = ds.DeleteLabel(ctx, labels[2].Name)
|
||||
require.NoError(t, err)
|
||||
err = ds.DeleteLabel(ctx, labels[3].Name)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, []uint{winHost.ID, appleHost.ID}, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// broken profiles do not get reported as "to remove"
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
appleHost: {
|
||||
{
|
||||
ProfileUUID: allProfs[0].ProfileUUID,
|
||||
Status: &fleet.MDMDeliveryPending,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
IdentifierOrName: allProfs[0].Identifier,
|
||||
},
|
||||
{
|
||||
ProfileUUID: allProfs[1].ProfileUUID,
|
||||
Status: &fleet.MDMDeliveryPending,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
IdentifierOrName: allProfs[1].Identifier,
|
||||
},
|
||||
},
|
||||
winHost: {
|
||||
{
|
||||
ProfileUUID: allProfs[2].ProfileUUID,
|
||||
Status: &fleet.MDMDeliveryPending,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
IdentifierOrName: allProfs[2].Name,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// create a new windows and apple host, not a member of any label
|
||||
i++
|
||||
winHost2, err := ds.NewHost(ctx, &fleet.Host{
|
||||
Hostname: fmt.Sprintf("win-host%d-name", i),
|
||||
OsqueryHostID: ptr.String(fmt.Sprintf("osquery-%d", i)),
|
||||
NodeKey: ptr.String(fmt.Sprintf("nodekey-%d", i)),
|
||||
UUID: fmt.Sprintf("win-uuid-%d", i),
|
||||
Platform: "windows",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
windowsEnroll(t, ds, winHost2)
|
||||
|
||||
i++
|
||||
appleHost2, err := ds.NewHost(ctx, &fleet.Host{
|
||||
Hostname: fmt.Sprintf("apple-host%d-name", i),
|
||||
OsqueryHostID: ptr.String(fmt.Sprintf("osquery-%d", i)),
|
||||
NodeKey: ptr.String(fmt.Sprintf("nodekey-%d", i)),
|
||||
UUID: fmt.Sprintf("apple-uuid-%d", i),
|
||||
Platform: "darwin",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
nanoEnroll(t, ds, appleHost2, false)
|
||||
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, []uint{winHost.ID, appleHost.ID, winHost2.ID, appleHost2.ID}, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// broken profiles do not get reported as "to install"
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
appleHost: {
|
||||
{
|
||||
ProfileUUID: allProfs[0].ProfileUUID,
|
||||
Status: &fleet.MDMDeliveryPending,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
IdentifierOrName: allProfs[0].Identifier,
|
||||
},
|
||||
{
|
||||
ProfileUUID: allProfs[1].ProfileUUID,
|
||||
Status: &fleet.MDMDeliveryPending,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
IdentifierOrName: allProfs[1].Identifier,
|
||||
},
|
||||
},
|
||||
winHost: {
|
||||
{
|
||||
ProfileUUID: allProfs[2].ProfileUUID,
|
||||
Status: &fleet.MDMDeliveryPending,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
IdentifierOrName: allProfs[2].Name,
|
||||
},
|
||||
},
|
||||
appleHost2: {},
|
||||
winHost2: {},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -744,9 +744,12 @@ WHERE
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(labels) > 0 {
|
||||
// ensure we leave Labels nil if there are none
|
||||
res.Labels = labels
|
||||
for _, lbl := range labels {
|
||||
if lbl.Exclude {
|
||||
res.LabelsExcludeAny = append(res.LabelsExcludeAny, lbl)
|
||||
} else {
|
||||
res.LabelsIncludeAll = append(res.LabelsIncludeAll, lbl)
|
||||
}
|
||||
}
|
||||
|
||||
return &res, nil
|
||||
|
|
@ -1127,6 +1130,7 @@ const windowsMDMProfilesDesiredStateQuery = `
|
|||
mwcp.name,
|
||||
h.uuid as host_uuid,
|
||||
0 as count_profile_labels,
|
||||
0 as count_non_broken_labels,
|
||||
0 as count_host_labels
|
||||
FROM
|
||||
mdm_windows_configuration_profiles mwcp
|
||||
|
|
@ -1145,12 +1149,15 @@ const windowsMDMProfilesDesiredStateQuery = `
|
|||
|
||||
UNION
|
||||
|
||||
-- label-based profiles
|
||||
-- label-based profiles where the host is a member of all the labels (include-all).
|
||||
-- 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
|
||||
|
|
@ -1159,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
|
||||
ON mcpl.windows_profile_uuid = mwcp.profile_uuid AND mcpl.exclude = 0
|
||||
LEFT OUTER JOIN label_membership lm
|
||||
ON lm.label_id = mcpl.label_id AND lm.host_id = h.id
|
||||
WHERE
|
||||
|
|
@ -1169,6 +1176,36 @@ const windowsMDMProfilesDesiredStateQuery = `
|
|||
mwcp.profile_uuid, mwcp.name, h.uuid
|
||||
HAVING
|
||||
count_profile_labels > 0 AND count_host_labels = count_profile_labels
|
||||
|
||||
UNION
|
||||
|
||||
-- label-based entities where the host is NOT a member of any of the labels (exclude-any).
|
||||
-- explicitly ignore profiles with broken excluded labels so that they are never applied.
|
||||
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 = 1
|
||||
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
|
||||
-- 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
|
||||
`
|
||||
|
||||
func (ds *Datastore) ListMDMWindowsProfilesToInstall(ctx context.Context) ([]*fleet.MDMWindowsProfilePayload, error) {
|
||||
|
|
@ -1235,9 +1272,9 @@ func listMDMWindowsProfilesToInstallDB(
|
|||
|
||||
var err error
|
||||
args := []any{fleet.MDMOperationTypeInstall}
|
||||
query = fmt.Sprintf(query, hostFilter, hostFilter)
|
||||
query = fmt.Sprintf(query, hostFilter, hostFilter, hostFilter)
|
||||
if len(hostUUIDs) > 0 {
|
||||
query, args, err = sqlx.In(query, hostUUIDs, hostUUIDs, fleet.MDMOperationTypeInstall)
|
||||
query, args, err = sqlx.In(query, hostUUIDs, hostUUIDs, hostUUIDs, fleet.MDMOperationTypeInstall)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "building sqlx.In")
|
||||
}
|
||||
|
|
@ -1306,6 +1343,7 @@ func listMDMWindowsProfilesToRemoveDB(
|
|||
-- TODO(mna): why don't we have the same exception for "remove" operations as for Apple
|
||||
|
||||
-- except "would be removed" profiles if they are a broken label-based profile
|
||||
-- (regardless of if it is an include-all or exclude-any label)
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM mdm_configuration_profile_labels mcpl
|
||||
|
|
@ -1314,7 +1352,7 @@ func listMDMWindowsProfilesToRemoveDB(
|
|||
mcpl.label_id IS NULL
|
||||
) AND
|
||||
(%s)
|
||||
`, fmt.Sprintf(windowsMDMProfilesDesiredStateQuery, "TRUE", "TRUE"), hostFilter)
|
||||
`, fmt.Sprintf(windowsMDMProfilesDesiredStateQuery, "TRUE", "TRUE", "TRUE"), hostFilter)
|
||||
|
||||
var err error
|
||||
var args []any
|
||||
|
|
@ -1533,10 +1571,17 @@ INSERT INTO
|
|||
}
|
||||
}
|
||||
|
||||
for i := range cp.Labels {
|
||||
cp.Labels[i].ProfileUUID = profileUUID
|
||||
labels := make([]fleet.ConfigurationProfileLabel, 0, len(cp.LabelsIncludeAll)+len(cp.LabelsExcludeAny))
|
||||
for i := range cp.LabelsIncludeAll {
|
||||
cp.LabelsIncludeAll[i].ProfileUUID = profileUUID
|
||||
labels = append(labels, cp.LabelsIncludeAll[i])
|
||||
}
|
||||
if err := batchSetProfileLabelAssociationsDB(ctx, tx, cp.Labels, "windows"); err != nil {
|
||||
for i := range cp.LabelsExcludeAny {
|
||||
cp.LabelsExcludeAny[i].ProfileUUID = profileUUID
|
||||
cp.LabelsExcludeAny[i].Exclude = true
|
||||
labels = append(labels, cp.LabelsExcludeAny[i])
|
||||
}
|
||||
if err := batchSetProfileLabelAssociationsDB(ctx, tx, labels, "windows"); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "inserting windows profile label associations")
|
||||
}
|
||||
|
||||
|
|
@ -1767,10 +1812,15 @@ ON DUPLICATE KEY UPDATE
|
|||
return ctxerr.Wrapf(ctx, err, "profile %q is in the database but was not incoming", newlyInsertedProf.Name)
|
||||
}
|
||||
|
||||
for _, label := range incomingProf.Labels {
|
||||
for _, label := range incomingProf.LabelsIncludeAll {
|
||||
label.ProfileUUID = newlyInsertedProf.ProfileUUID
|
||||
incomingLabels = append(incomingLabels, label)
|
||||
}
|
||||
for _, label := range incomingProf.LabelsExcludeAny {
|
||||
label.ProfileUUID = newlyInsertedProf.ProfileUUID
|
||||
label.Exclude = true
|
||||
incomingLabels = append(incomingLabels, label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1805,10 +1805,10 @@ func testMDMWindowsConfigProfiles(t *testing.T, ds *Datastore) {
|
|||
_, err = ds.NewMDMWindowsConfigProfile(
|
||||
ctx,
|
||||
fleet.MDMWindowsConfigProfile{
|
||||
Name: "fake-labels",
|
||||
TeamID: nil,
|
||||
SyncML: []byte("<Replace></Replace>"),
|
||||
Labels: []fleet.ConfigurationProfileLabel{{LabelName: "foo", LabelID: 1}},
|
||||
Name: "fake-labels",
|
||||
TeamID: nil,
|
||||
SyncML: []byte("<Replace></Replace>"),
|
||||
LabelsIncludeAll: []fleet.ConfigurationProfileLabel{{LabelName: "foo", LabelID: 1}},
|
||||
})
|
||||
require.NotNil(t, err)
|
||||
require.True(t, fleet.IsForeignKey(err))
|
||||
|
|
@ -1825,10 +1825,10 @@ func testMDMWindowsConfigProfiles(t *testing.T, ds *Datastore) {
|
|||
profWithLabel, err := ds.NewMDMWindowsConfigProfile(
|
||||
ctx,
|
||||
fleet.MDMWindowsConfigProfile{
|
||||
Name: "with-labels",
|
||||
TeamID: nil,
|
||||
SyncML: []byte("<Replace></Replace>"),
|
||||
Labels: []fleet.ConfigurationProfileLabel{{LabelName: label.Name, LabelID: label.ID}},
|
||||
Name: "with-labels",
|
||||
TeamID: nil,
|
||||
SyncML: []byte("<Replace></Replace>"),
|
||||
LabelsIncludeAll: []fleet.ConfigurationProfileLabel{{LabelName: label.Name, LabelID: label.ID}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, profWithLabel.ProfileUUID)
|
||||
|
|
@ -1836,20 +1836,20 @@ func testMDMWindowsConfigProfiles(t *testing.T, ds *Datastore) {
|
|||
// get that profile with label
|
||||
prof, err := ds.GetMDMWindowsConfigProfile(ctx, profWithLabel.ProfileUUID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, prof.Labels, 1)
|
||||
require.Equal(t, label.Name, prof.Labels[0].LabelName)
|
||||
require.Equal(t, label.ID, prof.Labels[0].LabelID)
|
||||
require.False(t, prof.Labels[0].Broken)
|
||||
require.Len(t, prof.LabelsIncludeAll, 1)
|
||||
require.Equal(t, label.Name, prof.LabelsIncludeAll[0].LabelName)
|
||||
require.Equal(t, label.ID, prof.LabelsIncludeAll[0].LabelID)
|
||||
require.False(t, prof.LabelsIncludeAll[0].Broken)
|
||||
|
||||
// break that profile by deleting the label
|
||||
require.NoError(t, ds.DeleteLabel(ctx, label.Name))
|
||||
|
||||
prof, err = ds.GetMDMWindowsConfigProfile(ctx, profWithLabel.ProfileUUID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, prof.Labels, 1)
|
||||
require.Equal(t, label.Name, prof.Labels[0].LabelName)
|
||||
require.Zero(t, prof.Labels[0].LabelID)
|
||||
require.True(t, prof.Labels[0].Broken)
|
||||
require.Len(t, prof.LabelsIncludeAll, 1)
|
||||
require.Equal(t, label.Name, prof.LabelsIncludeAll[0].LabelName)
|
||||
require.Zero(t, prof.LabelsIncludeAll[0].LabelID)
|
||||
require.True(t, prof.LabelsIncludeAll[0].Broken)
|
||||
|
||||
_, err = ds.GetMDMWindowsConfigProfile(ctx, "not-valid")
|
||||
require.Error(t, err)
|
||||
|
|
@ -1864,7 +1864,7 @@ func testMDMWindowsConfigProfiles(t *testing.T, ds *Datastore) {
|
|||
require.Equal(t, "<Replace></Replace>", string(prof.SyncML))
|
||||
require.NotZero(t, prof.CreatedAt)
|
||||
require.NotZero(t, prof.UploadedAt)
|
||||
require.Nil(t, prof.Labels)
|
||||
require.Nil(t, prof.LabelsIncludeAll)
|
||||
|
||||
err = ds.DeleteMDMWindowsConfigProfile(ctx, "not-valid")
|
||||
require.Error(t, err)
|
||||
|
|
@ -2125,6 +2125,8 @@ func testBatchSetMDMWindowsProfiles(t *testing.T, ds *Datastore) {
|
|||
applyAndExpect(nil, ptr.Uint(1), nil)
|
||||
}
|
||||
|
||||
// if the label name starts with "exclude-", the label is considered an "exclude-any", otherwise
|
||||
// it is an "include-all".
|
||||
func windowsConfigProfileForTest(t *testing.T, name, locURI string, labels ...*fleet.Label) *fleet.MDMWindowsConfigProfile {
|
||||
prof := &fleet.MDMWindowsConfigProfile{
|
||||
Name: name,
|
||||
|
|
@ -2140,7 +2142,11 @@ func windowsConfigProfileForTest(t *testing.T, name, locURI string, labels ...*f
|
|||
}
|
||||
|
||||
for _, lbl := range labels {
|
||||
prof.Labels = append(prof.Labels, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID})
|
||||
if strings.HasPrefix(lbl.Name, "exclude-") {
|
||||
prof.LabelsExcludeAny = append(prof.LabelsExcludeAny, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID})
|
||||
} else {
|
||||
prof.LabelsIncludeAll = append(prof.LabelsIncludeAll, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID})
|
||||
}
|
||||
}
|
||||
|
||||
return prof
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
package tables
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func init() {
|
||||
MigrationClient.AddMigration(Up_20240703154849, Down_20240703154849)
|
||||
}
|
||||
|
||||
func Up_20240703154849(tx *sql.Tx) error {
|
||||
_, err := tx.Exec(`ALTER TABLE mdm_configuration_profile_labels ADD COLUMN exclude TINYINT(1) NOT NULL DEFAULT 0`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add exclude boolean to mdm_configuration_profile_labels: %w", err)
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`ALTER TABLE mdm_declaration_labels ADD COLUMN exclude TINYINT(1) NOT NULL DEFAULT 0`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add exclude boolean to mdm_declaration_labels: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Down_20240703154849(tx *sql.Tx) error {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package tables
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUp_20240703154849(t *testing.T) {
|
||||
db := applyUpToPrev(t)
|
||||
|
||||
// create an MDM profile and an MDM declaration
|
||||
profStmt := `
|
||||
INSERT INTO
|
||||
mdm_apple_configuration_profiles (team_id, identifier, name, mobileconfig, profile_uuid, checksum)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`
|
||||
|
||||
profUUID := uuid.NewString()
|
||||
mcBytes := []byte(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
</dict>
|
||||
</plist>
|
||||
`)
|
||||
|
||||
_, err := db.Exec(profStmt, 0, "TestPayloadIdentifier", "TestPayloadName", mcBytes, profUUID, "ABCD")
|
||||
require.NoError(t, err)
|
||||
|
||||
declStmt := `
|
||||
INSERT INTO
|
||||
mdm_apple_declarations (declaration_uuid, team_id, identifier, name, raw_json, checksum)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`
|
||||
|
||||
declUUID := uuid.NewString()
|
||||
_, err = db.Exec(declStmt, declUUID, 0, "TestDecl", "TestDecl", `{}`, "abcd")
|
||||
require.NoError(t, err)
|
||||
|
||||
// create a couple labels
|
||||
idlA := execNoErrLastID(t, db, `INSERT INTO labels (name, query) VALUES ('LA', 'select 1')`)
|
||||
idlB := execNoErrLastID(t, db, `INSERT INTO labels (name, query) VALUES ('LB', 'select 1')`)
|
||||
|
||||
// finally we can create the MDM profile label and MDM declaration label entries
|
||||
profLblID := execNoErrLastID(t, db, `INSERT INTO mdm_configuration_profile_labels (apple_profile_uuid, label_name, label_id) VALUES (?, ?, ?)`,
|
||||
profUUID, "LA", idlA)
|
||||
declLblID := execNoErrLastID(t, db, `INSERT INTO mdm_declaration_labels (apple_declaration_uuid, label_name, label_id) VALUES (?, ?, ?)`,
|
||||
declUUID, "LB", idlB)
|
||||
|
||||
// Apply current migration.
|
||||
applyNext(t, db)
|
||||
|
||||
// check that the "exclude" flag is false in the DB (set it to true to verify
|
||||
// that it did scan from the DB)
|
||||
exclude := true
|
||||
err = db.Get(&exclude, "SELECT exclude FROM mdm_configuration_profile_labels WHERE id = ?", profLblID)
|
||||
require.NoError(t, err)
|
||||
require.False(t, exclude)
|
||||
|
||||
exclude = true
|
||||
err = db.Get(&exclude, "SELECT exclude FROM mdm_declaration_labels WHERE id = ?", declLblID)
|
||||
require.NoError(t, err)
|
||||
require.False(t, exclude)
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -308,6 +308,18 @@ func (s MacOSSettings) ToMap() map[string]interface{} {
|
|||
func (s *MacOSSettings) FromMap(m map[string]interface{}) (map[string]bool, error) {
|
||||
set := make(map[string]bool)
|
||||
|
||||
extractLabelField := func(parentMap map[string]interface{}, fieldName string) []string {
|
||||
var ret []string
|
||||
if labels, ok := parentMap[fieldName].([]interface{}); ok {
|
||||
for _, label := range labels {
|
||||
if strLabel, ok := label.(string); ok {
|
||||
ret = append(ret, strLabel)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
if v, ok := m["custom_settings"]; ok {
|
||||
set["custom_settings"] = true
|
||||
|
||||
|
|
@ -322,15 +334,9 @@ func (s *MacOSSettings) FromMap(m map[string]interface{}) (map[string]bool, erro
|
|||
spec.Path = path
|
||||
}
|
||||
|
||||
// extract the Labels field (if they are not provided, labels are
|
||||
// cleared for that profile)
|
||||
if labels, ok := m["labels"].([]interface{}); ok {
|
||||
for _, label := range labels {
|
||||
if strLabel, ok := label.(string); ok {
|
||||
spec.Labels = append(spec.Labels, strLabel)
|
||||
}
|
||||
}
|
||||
}
|
||||
spec.Labels = extractLabelField(m, "labels")
|
||||
spec.LabelsIncludeAll = extractLabelField(m, "labels_include_all")
|
||||
spec.LabelsExcludeAny = extractLabelField(m, "labels_exclude_any")
|
||||
|
||||
csSpecs = append(csSpecs, spec)
|
||||
} else if m, ok := v.(string); ok { // for backwards compatibility with the old way to define profiles
|
||||
|
|
|
|||
|
|
@ -195,11 +195,11 @@ type MDMAppleConfigProfile struct {
|
|||
// representation of the configuration profile. It must be XML or PKCS7 parseable.
|
||||
Mobileconfig mobileconfig.Mobileconfig `db:"mobileconfig" json:"-"`
|
||||
// Checksum is an MD5 hash of the Mobileconfig bytes
|
||||
Checksum []byte `db:"checksum" json:"checksum,omitempty"`
|
||||
// Labels are the associated labels for this profile
|
||||
Labels []ConfigurationProfileLabel `db:"labels" json:"labels,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
|
||||
Checksum []byte `db:"checksum" json:"checksum,omitempty"`
|
||||
LabelsIncludeAll []ConfigurationProfileLabel `db:"-" json:"labels_include_all,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
|
||||
}
|
||||
|
||||
// ConfigurationProfileLabel represents the many-to-many relationship between
|
||||
|
|
@ -212,6 +212,7 @@ 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
|
||||
}
|
||||
|
||||
func NewMDMAppleConfigProfile(raw []byte, teamID *uint) (*MDMAppleConfigProfile, error) {
|
||||
|
|
@ -561,8 +562,9 @@ type MDMAppleDeclaration struct {
|
|||
// Checksum is a checksum of the JSON contents
|
||||
Checksum string `db:"checksum" json:"-"`
|
||||
|
||||
// Labels are the labels associated with this Declaration
|
||||
Labels []ConfigurationProfileLabel `db:"labels" json:"labels,omitempty"`
|
||||
// labels associated with this Declaration
|
||||
LabelsIncludeAll []ConfigurationProfileLabel `db:"-" json:"labels_include_all,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:"uploaded_at"`
|
||||
|
|
|
|||
|
|
@ -129,6 +129,8 @@ type ExpectedMDMProfile struct {
|
|||
CountProfileLabels uint `db:"count_profile_labels"`
|
||||
// CountHostLabels is used to enable queries that filter based on profile <-> label mappings.
|
||||
CountHostLabels uint `db:"count_host_labels"`
|
||||
// CountNonBrokenLabels is used to enable queries that filter based on profile <-> label mappings.
|
||||
CountNonBrokenLabels uint `db:"count_non_broken_labels"`
|
||||
}
|
||||
|
||||
// IsWithinGracePeriod returns true if the host is within the grace period for the profile.
|
||||
|
|
@ -364,23 +366,29 @@ func (m MDMConfigProfileAuthz) AuthzType() string {
|
|||
// MDMConfigProfilePayload is the platform-agnostic struct returned by
|
||||
// endpoints that return MDM configuration profiles (get/list profiles).
|
||||
type MDMConfigProfilePayload struct {
|
||||
ProfileUUID string `json:"profile_uuid" db:"profile_uuid"`
|
||||
TeamID *uint `json:"team_id" db:"team_id"` // null for no-team
|
||||
Name string `json:"name" db:"name"`
|
||||
Platform string `json:"platform" db:"platform"` // "windows" or "darwin"
|
||||
Identifier string `json:"identifier,omitempty" db:"identifier"` // only set for macOS
|
||||
Checksum []byte `json:"checksum,omitempty" db:"checksum"` // only set for macOS
|
||||
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
|
||||
Labels []ConfigurationProfileLabel `json:"labels,omitempty" db:"-"`
|
||||
ProfileUUID string `json:"profile_uuid" db:"profile_uuid"`
|
||||
TeamID *uint `json:"team_id" db:"team_id"` // null for no-team
|
||||
Name string `json:"name" db:"name"`
|
||||
Platform string `json:"platform" db:"platform"` // "windows" or "darwin"
|
||||
Identifier string `json:"identifier,omitempty" db:"identifier"` // only set for macOS
|
||||
Checksum []byte `json:"checksum,omitempty" db:"checksum"` // only set for macOS
|
||||
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:"-"`
|
||||
LabelsExcludeAny []ConfigurationProfileLabel `json:"labels_exclude_any,omitempty" db:"-"`
|
||||
}
|
||||
|
||||
// MDMProfileBatchPayload represents the payload to batch-set the profiles for
|
||||
// a team or no-team.
|
||||
type MDMProfileBatchPayload struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Contents []byte `json:"contents,omitempty"`
|
||||
Labels []string `json:"labels,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Contents []byte `json:"contents,omitempty"`
|
||||
|
||||
// Deprecated: Labels is the backwards-compatible way of specifying
|
||||
// LabelsIncludeAll.
|
||||
Labels []string `json:"labels,omitempty"`
|
||||
LabelsIncludeAll []string `json:"labels_include_all,omitempty"`
|
||||
LabelsExcludeAny []string `json:"labels_exclude_any,omitempty"`
|
||||
}
|
||||
|
||||
func NewMDMConfigProfilePayloadFromWindows(cp *MDMWindowsConfigProfile) *MDMConfigProfilePayload {
|
||||
|
|
@ -389,13 +397,14 @@ func NewMDMConfigProfilePayloadFromWindows(cp *MDMWindowsConfigProfile) *MDMConf
|
|||
tid = cp.TeamID
|
||||
}
|
||||
return &MDMConfigProfilePayload{
|
||||
ProfileUUID: cp.ProfileUUID,
|
||||
TeamID: tid,
|
||||
Name: cp.Name,
|
||||
Platform: "windows",
|
||||
CreatedAt: cp.CreatedAt,
|
||||
UploadedAt: cp.UploadedAt,
|
||||
Labels: cp.Labels,
|
||||
ProfileUUID: cp.ProfileUUID,
|
||||
TeamID: tid,
|
||||
Name: cp.Name,
|
||||
Platform: "windows",
|
||||
CreatedAt: cp.CreatedAt,
|
||||
UploadedAt: cp.UploadedAt,
|
||||
LabelsIncludeAll: cp.LabelsIncludeAll,
|
||||
LabelsExcludeAny: cp.LabelsExcludeAny,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -405,15 +414,16 @@ func NewMDMConfigProfilePayloadFromApple(cp *MDMAppleConfigProfile) *MDMConfigPr
|
|||
tid = cp.TeamID
|
||||
}
|
||||
return &MDMConfigProfilePayload{
|
||||
ProfileUUID: cp.ProfileUUID,
|
||||
TeamID: tid,
|
||||
Name: cp.Name,
|
||||
Identifier: cp.Identifier,
|
||||
Platform: "darwin",
|
||||
Checksum: cp.Checksum,
|
||||
CreatedAt: cp.CreatedAt,
|
||||
UploadedAt: cp.UploadedAt,
|
||||
Labels: cp.Labels,
|
||||
ProfileUUID: cp.ProfileUUID,
|
||||
TeamID: tid,
|
||||
Name: cp.Name,
|
||||
Identifier: cp.Identifier,
|
||||
Platform: "darwin",
|
||||
Checksum: cp.Checksum,
|
||||
CreatedAt: cp.CreatedAt,
|
||||
UploadedAt: cp.UploadedAt,
|
||||
LabelsIncludeAll: cp.LabelsIncludeAll,
|
||||
LabelsExcludeAny: cp.LabelsExcludeAny,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -423,23 +433,37 @@ func NewMDMConfigProfilePayloadFromAppleDDM(decl *MDMAppleDeclaration) *MDMConfi
|
|||
tid = decl.TeamID
|
||||
}
|
||||
return &MDMConfigProfilePayload{
|
||||
ProfileUUID: decl.DeclarationUUID,
|
||||
TeamID: tid,
|
||||
Name: decl.Name,
|
||||
Identifier: decl.Identifier,
|
||||
Platform: "darwin",
|
||||
Checksum: []byte(decl.Checksum),
|
||||
CreatedAt: decl.CreatedAt,
|
||||
UploadedAt: decl.UploadedAt,
|
||||
Labels: decl.Labels,
|
||||
ProfileUUID: decl.DeclarationUUID,
|
||||
TeamID: tid,
|
||||
Name: decl.Name,
|
||||
Identifier: decl.Identifier,
|
||||
Platform: "darwin",
|
||||
Checksum: []byte(decl.Checksum),
|
||||
CreatedAt: decl.CreatedAt,
|
||||
UploadedAt: decl.UploadedAt,
|
||||
LabelsIncludeAll: decl.LabelsIncludeAll,
|
||||
LabelsExcludeAny: decl.LabelsExcludeAny,
|
||||
}
|
||||
}
|
||||
|
||||
// MDMProfileSpec represents the spec used to define configuration
|
||||
// profiles via yaml files.
|
||||
type MDMProfileSpec struct {
|
||||
Path string `json:"path,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
|
||||
// Deprecated: the Labels field is now deprecated, it is superseded by
|
||||
// LabelsIncludeAll, so any value set via this field will be transferred to
|
||||
// LabelsIncludeAll.
|
||||
Labels []string `json:"labels,omitempty"`
|
||||
|
||||
// LabelsIncludeAll is a list of label names that the host must be a member
|
||||
// of in order to receive the profile. It must be a member of all listed
|
||||
// labels.
|
||||
LabelsIncludeAll []string `json:"labels_include_all,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.
|
||||
LabelsExcludeAny []string `json:"labels_exclude_any,omitempty"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface to add backwards
|
||||
|
|
@ -487,6 +511,14 @@ func (p *MDMProfileSpec) Copy() *MDMProfileSpec {
|
|||
clone.Labels = make([]string, len(p.Labels))
|
||||
copy(clone.Labels, p.Labels)
|
||||
}
|
||||
if len(p.LabelsIncludeAll) > 0 {
|
||||
clone.LabelsIncludeAll = make([]string, len(p.LabelsIncludeAll))
|
||||
copy(clone.LabelsIncludeAll, p.LabelsIncludeAll)
|
||||
}
|
||||
if len(p.LabelsExcludeAny) > 0 {
|
||||
clone.LabelsExcludeAny = make([]string, len(p.LabelsExcludeAny))
|
||||
copy(clone.LabelsExcludeAny, p.LabelsExcludeAny)
|
||||
}
|
||||
|
||||
return &clone
|
||||
}
|
||||
|
|
@ -506,35 +538,64 @@ func MDMProfileSpecsMatch(a, b []MDMProfileSpec) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
pathLabelCounts := make(map[string]map[string]int)
|
||||
pathLabelIncludeCounts := make(map[string]map[string]int)
|
||||
for _, v := range a {
|
||||
pathLabelCounts[v.Path] = labelCountMap(v.Labels)
|
||||
// the deprecated Labels field is only relevant if LabelsIncludeAll is
|
||||
// empty.
|
||||
if len(v.LabelsIncludeAll) > 0 {
|
||||
pathLabelIncludeCounts[v.Path] = labelCountMap(v.LabelsIncludeAll)
|
||||
} else {
|
||||
pathLabelIncludeCounts[v.Path] = labelCountMap(v.Labels)
|
||||
}
|
||||
}
|
||||
pathLabelExcludeCounts := make(map[string]map[string]int)
|
||||
for _, v := range a {
|
||||
pathLabelExcludeCounts[v.Path] = labelCountMap(v.LabelsExcludeAny)
|
||||
}
|
||||
|
||||
for _, v := range b {
|
||||
labels, ok := pathLabelCounts[v.Path]
|
||||
if !ok {
|
||||
includeLabels, okIncl := pathLabelIncludeCounts[v.Path]
|
||||
excludeLabels, okExcl := pathLabelExcludeCounts[v.Path]
|
||||
if !okIncl || !okExcl {
|
||||
return false
|
||||
}
|
||||
|
||||
bLabelCounts := labelCountMap(v.Labels)
|
||||
for label, count := range bLabelCounts {
|
||||
if labels[label] != count {
|
||||
var bLabelIncludeCounts map[string]int
|
||||
if len(v.LabelsIncludeAll) > 0 {
|
||||
bLabelIncludeCounts = labelCountMap(v.LabelsIncludeAll)
|
||||
} else {
|
||||
bLabelIncludeCounts = labelCountMap(v.Labels)
|
||||
}
|
||||
for label, count := range bLabelIncludeCounts {
|
||||
if includeLabels[label] != count {
|
||||
return false
|
||||
}
|
||||
labels[label] -= count
|
||||
includeLabels[label] -= count
|
||||
}
|
||||
|
||||
for _, count := range labels {
|
||||
for _, count := range includeLabels {
|
||||
if count != 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
delete(pathLabelCounts, v.Path)
|
||||
bLabelExcludeCounts := labelCountMap(v.LabelsExcludeAny)
|
||||
for label, count := range bLabelExcludeCounts {
|
||||
if excludeLabels[label] != count {
|
||||
return false
|
||||
}
|
||||
excludeLabels[label] -= count
|
||||
}
|
||||
for _, count := range excludeLabels {
|
||||
if count != 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
delete(pathLabelIncludeCounts, v.Path)
|
||||
delete(pathLabelExcludeCounts, v.Path)
|
||||
}
|
||||
|
||||
return len(pathLabelCounts) == 0
|
||||
return len(pathLabelIncludeCounts) == 0 && len(pathLabelExcludeCounts) == 0
|
||||
}
|
||||
|
||||
type MDMAssetName string
|
||||
|
|
|
|||
|
|
@ -316,6 +316,66 @@ func TestMDMProfileSpecsMatch(t *testing.T) {
|
|||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Include Labels Match",
|
||||
a: []fleet.MDMProfileSpec{
|
||||
{Path: "path1", LabelsIncludeAll: []string{"label1", "label2"}},
|
||||
{Path: "path2", LabelsIncludeAll: []string{"label3"}},
|
||||
},
|
||||
b: []fleet.MDMProfileSpec{
|
||||
{Path: "path1", LabelsIncludeAll: []string{"label2", "label1"}},
|
||||
{Path: "path2", LabelsIncludeAll: []string{"label3"}},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Exclude Labels Match",
|
||||
a: []fleet.MDMProfileSpec{
|
||||
{Path: "path1", LabelsExcludeAny: []string{"label1", "label2"}},
|
||||
{Path: "path2", LabelsExcludeAny: []string{"label3"}},
|
||||
},
|
||||
b: []fleet.MDMProfileSpec{
|
||||
{Path: "path1", LabelsExcludeAny: []string{"label2", "label1"}},
|
||||
{Path: "path2", LabelsExcludeAny: []string{"label3"}},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Include Labels Mismatch",
|
||||
a: []fleet.MDMProfileSpec{
|
||||
{Path: "path1", LabelsIncludeAll: []string{"label1", "label2"}},
|
||||
{Path: "path2", LabelsIncludeAll: []string{"label3"}},
|
||||
},
|
||||
b: []fleet.MDMProfileSpec{
|
||||
{Path: "path1", LabelsIncludeAll: []string{"label2", "label1"}},
|
||||
{Path: "path2", LabelsIncludeAll: []string{"label4"}},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Exclude Labels Mismatch",
|
||||
a: []fleet.MDMProfileSpec{
|
||||
{Path: "path1", LabelsExcludeAny: []string{"label1", "label2"}},
|
||||
{Path: "path2", LabelsExcludeAny: []string{"label3"}},
|
||||
},
|
||||
b: []fleet.MDMProfileSpec{
|
||||
{Path: "path1", LabelsExcludeAny: []string{"label2", "label1"}},
|
||||
{Path: "path3", LabelsExcludeAny: []string{"label3"}},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Deprecated Labels Match IncludeAll",
|
||||
a: []fleet.MDMProfileSpec{
|
||||
{Path: "path1", Labels: []string{"label1", "label2"}},
|
||||
{Path: "path2", LabelsExcludeAny: []string{"label3"}},
|
||||
},
|
||||
b: []fleet.MDMProfileSpec{
|
||||
{Path: "path1", LabelsIncludeAll: []string{"label2", "label1"}},
|
||||
{Path: "path2", LabelsExcludeAny: []string{"label3"}},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
|
|
|
|||
|
|
@ -710,9 +710,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) (*MDMAppleConfigProfile, error)
|
||||
NewMDMAppleConfigProfile(ctx context.Context, teamID uint, r io.Reader, labels []string, labelsExcludeMode bool) (*MDMAppleConfigProfile, error)
|
||||
// NewMDMAppleConfigProfileWithPayload creates a new declaration for the specified team.
|
||||
NewMDMAppleDeclaration(ctx context.Context, teamID uint, r io.Reader, labels []string, name string) (*MDMAppleDeclaration, error)
|
||||
NewMDMAppleDeclaration(ctx context.Context, teamID uint, r io.Reader, labels []string, name string, labelsExcludeMode bool) (*MDMAppleDeclaration, error)
|
||||
|
||||
// GetMDMAppleConfigProfileByDeprecatedID retrieves the specified Apple
|
||||
// configuration profile via its numeric ID. This method is deprecated and
|
||||
|
|
@ -974,7 +974,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) (*MDMWindowsConfigProfile, error)
|
||||
NewMDMWindowsConfigProfile(ctx context.Context, teamID uint, profileName string, r io.Reader, labels []string, labelsExcludeMode bool) (*MDMWindowsConfigProfile, error)
|
||||
|
||||
// NewMDMUnsupportedConfigProfile is called when a profile with an
|
||||
// unsupported extension is uploaded.
|
||||
|
|
|
|||
|
|
@ -33,12 +33,10 @@ type TeamPayload struct {
|
|||
// need to be able which part of the MDM config was provided in the request,
|
||||
// so the fields are pointers to structs.
|
||||
type TeamPayloadMDM struct {
|
||||
EnableDiskEncryption optjson.Bool `json:"enable_disk_encryption"`
|
||||
MacOSUpdates *MacOSUpdates `json:"macos_updates"`
|
||||
WindowsUpdates *WindowsUpdates `json:"windows_updates"`
|
||||
MacOSSettings *MacOSSettings `json:"macos_settings"`
|
||||
MacOSSetup *MacOSSetup `json:"macos_setup"`
|
||||
WindowsSettings *WindowsSettings `json:"windows_settings"`
|
||||
EnableDiskEncryption optjson.Bool `json:"enable_disk_encryption"`
|
||||
MacOSUpdates *MacOSUpdates `json:"macos_updates"`
|
||||
WindowsUpdates *WindowsUpdates `json:"windows_updates"`
|
||||
MacOSSetup *MacOSSetup `json:"macos_setup"`
|
||||
}
|
||||
|
||||
// Team is the data representation for the "Team" concept (group of hosts and
|
||||
|
|
|
|||
|
|
@ -32,13 +32,14 @@ type MDMWindowsBitLockerSummary struct {
|
|||
type MDMWindowsConfigProfile struct {
|
||||
// ProfileUUID is the unique identifier of the configuration profile in
|
||||
// Fleet. For Windows profiles, it is the letter "w" followed by a uuid.
|
||||
ProfileUUID string `db:"profile_uuid" json:"profile_uuid"`
|
||||
TeamID *uint `db:"team_id" json:"team_id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
SyncML []byte `db:"syncml" json:"-"`
|
||||
Labels []ConfigurationProfileLabel `db:"labels" json:"labels,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
|
||||
ProfileUUID string `db:"profile_uuid" json:"profile_uuid"`
|
||||
TeamID *uint `db:"team_id" json:"team_id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
SyncML []byte `db:"syncml" json:"-"`
|
||||
LabelsIncludeAll []ConfigurationProfileLabel `db:"-" json:"labels_include_all,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
|
||||
}
|
||||
|
||||
// ValidateUserProvided ensures that the SyncML content in the profile is valid
|
||||
|
|
@ -124,9 +125,7 @@ func (m *MDMWindowsConfigProfile) ValidateUserProvided() error {
|
|||
return err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -743,6 +743,25 @@ func (svc *Service) validateMDM(
|
|||
`Couldn't update macos_setup because MDM features aren't turned on in Fleet. Use fleetctl generate mdm-apple and then fleet serve with mdm configuration to turn on MDM features.`)
|
||||
}
|
||||
}
|
||||
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} {
|
||||
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))
|
||||
}
|
||||
if len(prof.Labels) > 0 {
|
||||
customSettings[i].LabelsIncludeAll = customSettings[i].Labels
|
||||
customSettings[i].Labels = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
checkCustomSettings("macos", mdm.MacOSSettings.CustomSettings)
|
||||
|
||||
if !mdm.WindowsEnabledAndConfigured {
|
||||
if mdm.WindowsSettings.CustomSettings.Set &&
|
||||
|
|
@ -752,6 +771,7 @@ func (svc *Service) validateMDM(
|
|||
`Couldn’t edit windows_settings.custom_settings. Windows MDM isn’t turned on. Visit https://fleetdm.com/docs/using-fleet to learn how to turn on MDM.`)
|
||||
}
|
||||
}
|
||||
checkCustomSettings("windows", mdm.WindowsSettings.CustomSettings.Value)
|
||||
|
||||
if name := mdm.AppleBMDefaultTeam; name != "" && name != oldMdm.AppleBMDefaultTeam {
|
||||
if !license.IsPremium() {
|
||||
|
|
|
|||
|
|
@ -306,7 +306,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)
|
||||
cp, err := svc.NewMDMAppleConfigProfile(ctx, req.TeamID, ff, nil, false)
|
||||
if err != nil {
|
||||
return &newMDMAppleConfigProfileResponse{Err: err}, nil
|
||||
}
|
||||
|
|
@ -315,7 +315,7 @@ func newMDMAppleConfigProfileEndpoint(ctx context.Context, request interface{},
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) NewMDMAppleConfigProfile(ctx context.Context, teamID uint, r io.Reader, labels []string) (*fleet.MDMAppleConfigProfile, error) {
|
||||
func (svc *Service) NewMDMAppleConfigProfile(ctx context.Context, teamID uint, r io.Reader, labels []string, labelsExcludeMode bool) (*fleet.MDMAppleConfigProfile, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.MDMConfigProfileAuthz{TeamID: &teamID}, fleet.ActionWrite); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
|
|
@ -359,7 +359,11 @@ func (svc *Service) NewMDMAppleConfigProfile(ctx context.Context, teamID uint, r
|
|||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "validating labels")
|
||||
}
|
||||
cp.Labels = labelMap
|
||||
if labelsExcludeMode {
|
||||
cp.LabelsExcludeAny = labelMap
|
||||
} else {
|
||||
cp.LabelsIncludeAll = labelMap
|
||||
}
|
||||
|
||||
newCP, err := svc.ds.NewMDMAppleConfigProfile(ctx, *cp)
|
||||
if err != nil {
|
||||
|
|
@ -401,7 +405,7 @@ func (svc *Service) NewMDMAppleConfigProfile(ctx context.Context, teamID uint, r
|
|||
return newCP, nil
|
||||
}
|
||||
|
||||
func (svc *Service) NewMDMAppleDeclaration(ctx context.Context, teamID uint, r io.Reader, labels []string, name string) (*fleet.MDMAppleDeclaration, error) {
|
||||
func (svc *Service) NewMDMAppleDeclaration(ctx context.Context, teamID uint, r io.Reader, labels []string, name string, labelsExcludeMode bool) (*fleet.MDMAppleDeclaration, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.MDMConfigProfileAuthz{TeamID: &teamID}, fleet.ActionWrite); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
|
|
@ -455,8 +459,11 @@ func (svc *Service) NewMDMAppleDeclaration(ctx context.Context, teamID uint, r i
|
|||
|
||||
d := fleet.NewMDMAppleDeclaration(data, tmID, name, rawDecl.Type, rawDecl.Identifier)
|
||||
|
||||
// TODO(roberto): this should be part of fleet.NewMDMAppleDeclaration
|
||||
d.Labels = validatedLabels
|
||||
if labelsExcludeMode {
|
||||
d.LabelsExcludeAny = validatedLabels
|
||||
} else {
|
||||
d.LabelsIncludeAll = validatedLabels
|
||||
}
|
||||
|
||||
decl, err := svc.ds.NewMDMAppleDeclaration(ctx, d)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -644,11 +644,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)
|
||||
_, err := svc.NewMDMAppleConfigProfile(ctx, 0, bytes.NewReader(mcBytes), nil, false)
|
||||
checkShouldFail(err, tt.shouldFailGlobal)
|
||||
|
||||
// test authz create new profile (team 1)
|
||||
_, err = svc.NewMDMAppleConfigProfile(ctx, 1, bytes.NewReader(mcBytes), nil)
|
||||
_, err = svc.NewMDMAppleConfigProfile(ctx, 1, bytes.NewReader(mcBytes), nil, false)
|
||||
checkShouldFail(err, tt.shouldFailTeam)
|
||||
|
||||
// test authz list profiles (no team)
|
||||
|
|
@ -710,7 +710,7 @@ func TestNewMDMAppleConfigProfile(t *testing.T) {
|
|||
return nil
|
||||
}
|
||||
|
||||
cp, err := svc.NewMDMAppleConfigProfile(ctx, 0, r, nil)
|
||||
cp, err := svc.NewMDMAppleConfigProfile(ctx, 0, r, nil, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "Foo", cp.Name)
|
||||
require.Equal(t, "Bar", cp.Identifier)
|
||||
|
|
|
|||
|
|
@ -375,9 +375,11 @@ func getProfilesContents(baseDir string, macProfiles []fleet.MDMProfileSpec, win
|
|||
extByName[name] = ext
|
||||
|
||||
result = append(result, fleet.MDMProfileBatchPayload{
|
||||
Name: name,
|
||||
Contents: fileContents,
|
||||
Labels: profile.Labels,
|
||||
Name: name,
|
||||
Contents: fileContents,
|
||||
Labels: profile.Labels,
|
||||
LabelsIncludeAll: profile.LabelsIncludeAll,
|
||||
LabelsExcludeAny: profile.LabelsExcludeAny,
|
||||
})
|
||||
|
||||
}
|
||||
|
|
@ -828,6 +830,18 @@ func extractAppCfgCustomSettings(appCfg interface{}, platformKey string) []fleet
|
|||
return []fleet.MDMProfileSpec{}
|
||||
}
|
||||
|
||||
extractLabelField := func(parentMap map[string]interface{}, fieldName string) []string {
|
||||
var ret []string
|
||||
if labels, ok := parentMap[fieldName].([]interface{}); ok {
|
||||
for _, label := range labels {
|
||||
if strLabel, ok := label.(string); ok {
|
||||
ret = append(ret, strLabel)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
csSpecs := make([]fleet.MDMProfileSpec, 0, len(csAny))
|
||||
for _, v := range csAny {
|
||||
if m, ok := v.(map[string]interface{}); ok {
|
||||
|
|
@ -838,14 +852,11 @@ func extractAppCfgCustomSettings(appCfg interface{}, platformKey string) []fleet
|
|||
profSpec.Path = path
|
||||
}
|
||||
|
||||
// extract the Labels field, labels are cleared if not provided
|
||||
if labels, ok := m["labels"].([]interface{}); ok {
|
||||
for _, label := range labels {
|
||||
if strLabel, ok := label.(string); ok {
|
||||
profSpec.Labels = append(profSpec.Labels, strLabel)
|
||||
}
|
||||
}
|
||||
}
|
||||
// at this stage we extract and return all supported label fields, the
|
||||
// validations are done later on in the Fleet API endpoint.
|
||||
profSpec.Labels = extractLabelField(m, "labels")
|
||||
profSpec.LabelsIncludeAll = extractLabelField(m, "labels_include_all")
|
||||
profSpec.LabelsExcludeAny = extractLabelField(m, "labels_exclude_any")
|
||||
|
||||
if profSpec.Path != "" {
|
||||
csSpecs = append(csSpecs, profSpec)
|
||||
|
|
|
|||
|
|
@ -168,10 +168,10 @@ func (s *integrationMDMTestSuite) TestAppleDDMBatchUpload() {
|
|||
require.Equal(t, "label_2", createResp.Label.Name)
|
||||
lbl2 := createResp.Label.Label
|
||||
|
||||
// Add with labels
|
||||
// Add with the deprecated "labels" and the new LabelsIncludeAll field
|
||||
s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
|
||||
{Name: "N5", Contents: decls[5], Labels: []string{lbl1.Name, lbl2.Name}},
|
||||
{Name: "N6", Contents: decls[6], Labels: []string{lbl1.Name}},
|
||||
{Name: "N6", Contents: decls[6], LabelsIncludeAll: []string{lbl1.Name}},
|
||||
}}, http.StatusNoContent)
|
||||
|
||||
s.DoJSON("GET", "/api/latest/fleet/mdm/profiles", &listMDMConfigProfilesRequest{}, http.StatusOK, &resp)
|
||||
|
|
@ -181,11 +181,11 @@ func (s *integrationMDMTestSuite) TestAppleDDMBatchUpload() {
|
|||
require.Equal(t, "darwin", resp.Profiles[0].Platform)
|
||||
require.Equal(t, "N6", resp.Profiles[1].Name)
|
||||
require.Equal(t, "darwin", resp.Profiles[1].Platform)
|
||||
require.Len(t, resp.Profiles[0].Labels, 2)
|
||||
require.Equal(t, lbl1.Name, resp.Profiles[0].Labels[0].LabelName)
|
||||
require.Equal(t, lbl2.Name, resp.Profiles[0].Labels[1].LabelName)
|
||||
require.Len(t, resp.Profiles[1].Labels, 1)
|
||||
require.Equal(t, lbl1.Name, resp.Profiles[1].Labels[0].LabelName)
|
||||
require.Len(t, resp.Profiles[0].LabelsIncludeAll, 2)
|
||||
require.Equal(t, lbl1.Name, resp.Profiles[0].LabelsIncludeAll[0].LabelName)
|
||||
require.Equal(t, lbl2.Name, resp.Profiles[0].LabelsIncludeAll[1].LabelName)
|
||||
require.Len(t, resp.Profiles[1].LabelsIncludeAll, 1)
|
||||
require.Equal(t, lbl1.Name, resp.Profiles[1].LabelsIncludeAll[0].LabelName)
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) TestMDMAppleDeviceManagementRequests() {
|
||||
|
|
|
|||
|
|
@ -519,14 +519,14 @@ func (s *integrationMDMTestSuite) recordAppleHostStatus(
|
|||
require.NoError(t, err)
|
||||
for cmd != nil {
|
||||
var fullCmd micromdm.CommandPayload
|
||||
require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
|
||||
|
||||
// command uuid is a random value, we only care that's set
|
||||
require.NotEmpty(t, fullCmd.CommandUUID)
|
||||
fullCmd.CommandUUID = ""
|
||||
require.NotEmpty(t, cmd.CommandUUID)
|
||||
|
||||
// strip the signature of the profiles so they can be easily compared
|
||||
if fullCmd.Command.RequestType == "InstallProfile" {
|
||||
if cmd.Command.RequestType == "InstallProfile" {
|
||||
require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
|
||||
fullCmd.CommandUUID = ""
|
||||
p7, err := pkcs7.Parse(fullCmd.Command.InstallProfile.Payload)
|
||||
require.NoError(t, err)
|
||||
fullCmd.Command.InstallProfile.Payload = p7.Content
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -217,12 +217,20 @@ func (s *integrationMDMTestSuite) SetupSuite() {
|
|||
schedule.WithLogger(logger),
|
||||
schedule.WithJob("manage_apple_profiles", func(ctx context.Context) error {
|
||||
if s.onProfileJobDone != nil {
|
||||
s.onProfileJobDone()
|
||||
defer s.onProfileJobDone()
|
||||
}
|
||||
err = ReconcileAppleProfiles(ctx, ds, mdmCommander, logger)
|
||||
require.NoError(s.T(), err)
|
||||
return err
|
||||
}),
|
||||
schedule.WithJob("manage_apple_declarations", func(ctx context.Context) error {
|
||||
if s.onProfileJobDone != nil {
|
||||
defer s.onProfileJobDone()
|
||||
}
|
||||
err = ReconcileAppleDeclarations(ctx, ds, mdmCommander, logger)
|
||||
require.NoError(s.T(), err)
|
||||
return err
|
||||
}),
|
||||
schedule.WithJob("manage_windows_profiles", func(ctx context.Context) error {
|
||||
if s.onProfileJobDone != nil {
|
||||
defer s.onProfileJobDone()
|
||||
|
|
@ -430,9 +438,9 @@ func (s *integrationMDMTestSuite) mockDEPResponse(handler http.Handler) {
|
|||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) awaitTriggerProfileSchedule(t *testing.T) {
|
||||
// two jobs running sequentially (macOS then Windows) on the same schedule
|
||||
// three jobs running sequentially (macOS profiles and declarations, then Windows) on the same schedule
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
wg.Add(3)
|
||||
s.onProfileJobDone = wg.Done
|
||||
_, err := s.profileSchedule.Trigger()
|
||||
require.NoError(t, err)
|
||||
|
|
@ -658,13 +666,13 @@ func checkNextPayloads(t *testing.T, mdmDevice *mdmtest.TestAppleMDMClient, forc
|
|||
require.NoError(t, err)
|
||||
for cmd != nil {
|
||||
var fullCmd micromdm.CommandPayload
|
||||
require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
|
||||
switch cmd.Command.RequestType {
|
||||
case "InstallProfile":
|
||||
require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
|
||||
installs = append(installs, fullCmd.Command.InstallProfile.Payload)
|
||||
case "RemoveProfile":
|
||||
require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
|
||||
removes = append(removes, fullCmd.Command.RemoveProfile.Identifier)
|
||||
|
||||
}
|
||||
|
||||
if forceDeviceErr {
|
||||
|
|
@ -2114,7 +2122,6 @@ func (s *integrationMDMTestSuite) TestTeamsMDMAppleDiskEncryption() {
|
|||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{
|
||||
MDM: &fleet.TeamPayloadMDM{
|
||||
EnableDiskEncryption: optjson.SetBool(true),
|
||||
MacOSSettings: &fleet.MacOSSettings{},
|
||||
},
|
||||
}, http.StatusOK, &modResp)
|
||||
require.True(t, modResp.Team.Config.MDM.EnableDiskEncryption)
|
||||
|
|
@ -3958,7 +3965,7 @@ func (s *integrationMDMTestSuite) TestMacosSetupAssistant() {
|
|||
}
|
||||
|
||||
// only asserts the profile identifier, status and operation (per host)
|
||||
func (s *integrationMDMTestSuite) assertHostConfigProfiles(want map[*fleet.Host][]fleet.HostMDMAppleProfile) {
|
||||
func (s *integrationMDMTestSuite) assertHostAppleConfigProfiles(want map[*fleet.Host][]fleet.HostMDMAppleProfile) {
|
||||
t := s.T()
|
||||
ds := s.ds
|
||||
ctx := context.Background()
|
||||
|
|
@ -3966,7 +3973,11 @@ func (s *integrationMDMTestSuite) assertHostConfigProfiles(want map[*fleet.Host]
|
|||
for h, wantProfs := range want {
|
||||
gotProfs, err := ds.GetHostMDMAppleProfiles(ctx, h.UUID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(wantProfs), len(gotProfs), "host uuid: %s", h.UUID)
|
||||
idents := make([]string, 0, len(gotProfs))
|
||||
for _, gp := range gotProfs {
|
||||
idents = append(idents, gp.Identifier)
|
||||
}
|
||||
require.Equal(t, len(wantProfs), len(gotProfs), "apple host uuid: %s, profiles: %v", h.UUID, idents)
|
||||
|
||||
sort.Slice(gotProfs, func(i, j int) bool {
|
||||
l, r := gotProfs[i], gotProfs[j]
|
||||
|
|
@ -3985,6 +3996,34 @@ func (s *integrationMDMTestSuite) assertHostConfigProfiles(want map[*fleet.Host]
|
|||
}
|
||||
}
|
||||
|
||||
// only asserts the profile name, status and operation (per host)
|
||||
func (s *integrationMDMTestSuite) assertHostWindowsConfigProfiles(want map[*fleet.Host][]fleet.HostMDMWindowsProfile) {
|
||||
t := s.T()
|
||||
ds := s.ds
|
||||
ctx := context.Background()
|
||||
|
||||
for h, wantProfs := range want {
|
||||
gotProfs, err := ds.GetHostMDMWindowsProfiles(ctx, h.UUID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(wantProfs), len(gotProfs), "host uuid: %s", h.UUID)
|
||||
|
||||
sort.Slice(gotProfs, func(i, j int) bool {
|
||||
l, r := gotProfs[i], gotProfs[j]
|
||||
return l.Name < r.Name
|
||||
})
|
||||
sort.Slice(wantProfs, func(i, j int) bool {
|
||||
l, r := wantProfs[i], wantProfs[j]
|
||||
return l.Name < r.Name
|
||||
})
|
||||
for i, wp := range wantProfs {
|
||||
gp := gotProfs[i]
|
||||
require.Equal(t, wp.Name, gp.Name, "host uuid: %s, prof id: %s", h.UUID, gp.Name)
|
||||
require.Equal(t, wp.OperationType, gp.OperationType, "host uuid: %s, prof id: %s", h.UUID, gp.Name)
|
||||
require.Equal(t, wp.Status, gp.Status, "host uuid: %s, prof id: %s", h.UUID, gp.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) assertConfigProfilesByIdentifier(teamID *uint, profileIdent string, exists bool) (profile *fleet.MDMAppleConfigProfile) {
|
||||
t := s.T()
|
||||
if teamID == nil {
|
||||
|
|
|
|||
|
|
@ -1194,9 +1194,10 @@ func isAppleDeclarationUUID(profileUUID string) bool {
|
|||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type newMDMConfigProfileRequest struct {
|
||||
TeamID uint
|
||||
Profile *multipart.FileHeader
|
||||
Labels []string
|
||||
TeamID uint
|
||||
Profile *multipart.FileHeader
|
||||
LabelsIncludeAll []string
|
||||
LabelsExcludeAny []string
|
||||
}
|
||||
|
||||
func (newMDMConfigProfileRequest) DecodeRequest(ctx context.Context, r *http.Request) (interface{}, error) {
|
||||
|
|
@ -1235,7 +1236,25 @@ func (newMDMConfigProfileRequest) DecodeRequest(ctx context.Context, r *http.Req
|
|||
}
|
||||
|
||||
// add labels
|
||||
decoded.Labels = r.MultipartForm.Value["labels"]
|
||||
var existsIncl, existsExcl, existsDepr bool
|
||||
var deprecatedLabels []string
|
||||
decoded.LabelsIncludeAll, existsIncl = r.MultipartForm.Value["labels_include_all"]
|
||||
decoded.LabelsExcludeAny, existsExcl = r.MultipartForm.Value["labels_exclude_any"]
|
||||
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} {
|
||||
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.`}
|
||||
}
|
||||
if existsDepr {
|
||||
decoded.LabelsIncludeAll = deprecatedLabels
|
||||
}
|
||||
|
||||
return &decoded, nil
|
||||
}
|
||||
|
|
@ -1260,10 +1279,18 @@ func newMDMConfigProfileEndpoint(ctx context.Context, request interface{}, svc f
|
|||
profileName := strings.TrimSuffix(filepath.Base(req.Profile.Filename), fileExt)
|
||||
isMobileConfig := strings.EqualFold(fileExt, ".mobileconfig")
|
||||
isJSON := strings.EqualFold(fileExt, ".json")
|
||||
|
||||
labels := req.LabelsIncludeAll
|
||||
excludeMode := false
|
||||
if len(req.LabelsExcludeAny) > 0 {
|
||||
labels = req.LabelsExcludeAny
|
||||
excludeMode = true
|
||||
}
|
||||
|
||||
if isMobileConfig || isJSON {
|
||||
// Then it's an Apple configuration file
|
||||
if isJSON {
|
||||
decl, err := svc.NewMDMAppleDeclaration(ctx, req.TeamID, ff, req.Labels, profileName)
|
||||
decl, err := svc.NewMDMAppleDeclaration(ctx, req.TeamID, ff, labels, profileName, excludeMode)
|
||||
if err != nil {
|
||||
return &newMDMConfigProfileResponse{Err: err}, nil
|
||||
}
|
||||
|
|
@ -1274,7 +1301,7 @@ func newMDMConfigProfileEndpoint(ctx context.Context, request interface{}, svc f
|
|||
|
||||
}
|
||||
|
||||
cp, err := svc.NewMDMAppleConfigProfile(ctx, req.TeamID, ff, req.Labels)
|
||||
cp, err := svc.NewMDMAppleConfigProfile(ctx, req.TeamID, ff, labels, excludeMode)
|
||||
if err != nil {
|
||||
return &newMDMConfigProfileResponse{Err: err}, nil
|
||||
}
|
||||
|
|
@ -1284,7 +1311,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, req.Labels)
|
||||
cp, err := svc.NewMDMWindowsConfigProfile(ctx, req.TeamID, profileName, ff, labels, excludeMode)
|
||||
if err != nil {
|
||||
return &newMDMConfigProfileResponse{Err: err}, nil
|
||||
}
|
||||
|
|
@ -1308,7 +1335,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) (*fleet.MDMWindowsConfigProfile, error) {
|
||||
func (svc *Service) NewMDMWindowsConfigProfile(ctx context.Context, teamID uint, profileName string, r io.Reader, labels []string, labelsExcludeMode bool) (*fleet.MDMWindowsConfigProfile, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.MDMConfigProfileAuthz{TeamID: &teamID}, fleet.ActionWrite); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
|
|
@ -1357,7 +1384,11 @@ func (svc *Service) NewMDMWindowsConfigProfile(ctx context.Context, teamID uint,
|
|||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "validating labels")
|
||||
}
|
||||
cp.Labels = labelMap
|
||||
if labelsExcludeMode {
|
||||
cp.LabelsExcludeAny = labelMap
|
||||
} else {
|
||||
cp.LabelsIncludeAll = labelMap
|
||||
}
|
||||
|
||||
newCP, err := svc.ds.NewMDMWindowsConfigProfile(ctx, cp)
|
||||
if err != nil {
|
||||
|
|
@ -1522,8 +1553,17 @@ func (svc *Service) BatchSetMDMProfiles(
|
|||
}
|
||||
|
||||
labels := []string{}
|
||||
for _, prof := range profiles {
|
||||
labels = append(labels, prof.Labels...)
|
||||
for i := range profiles {
|
||||
// from this point on (after this condition), only LabelsIncludeAll 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
|
||||
// pointer to it (it is a slice of structs, not of pointer to structs)
|
||||
profiles[i].LabelsIncludeAll = profiles[i].Labels
|
||||
profiles[i].Labels = nil
|
||||
}
|
||||
labels = append(labels, profiles[i].LabelsIncludeAll...)
|
||||
labels = append(labels, profiles[i].LabelsExcludeAny...)
|
||||
}
|
||||
labelMap, err := svc.batchValidateProfileLabels(ctx, labels)
|
||||
if err != nil {
|
||||
|
|
@ -1732,13 +1772,23 @@ func getAppleProfiles(
|
|||
}
|
||||
|
||||
mdmDecl := fleet.NewMDMAppleDeclaration(prof.Contents, tmID, prof.Name, rawDecl.Type, rawDecl.Identifier)
|
||||
for _, labelName := range prof.Labels {
|
||||
for _, labelName := range prof.LabelsIncludeAll {
|
||||
if lbl, ok := labelMap[labelName]; ok {
|
||||
declLabel := fleet.ConfigurationProfileLabel{
|
||||
LabelName: lbl.LabelName,
|
||||
LabelID: lbl.LabelID,
|
||||
}
|
||||
mdmDecl.Labels = append(mdmDecl.Labels, declLabel)
|
||||
mdmDecl.LabelsIncludeAll = append(mdmDecl.LabelsIncludeAll, declLabel)
|
||||
}
|
||||
}
|
||||
for _, labelName := range prof.LabelsExcludeAny {
|
||||
if lbl, ok := labelMap[labelName]; ok {
|
||||
declLabel := fleet.ConfigurationProfileLabel{
|
||||
LabelName: lbl.LabelName,
|
||||
LabelID: lbl.LabelID,
|
||||
Exclude: true,
|
||||
}
|
||||
mdmDecl.LabelsExcludeAny = append(mdmDecl.LabelsExcludeAny, declLabel)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1773,9 +1823,14 @@ func getAppleProfiles(
|
|||
"invalid mobileconfig profile")
|
||||
}
|
||||
|
||||
for _, labelName := range prof.Labels {
|
||||
for _, labelName := range prof.LabelsIncludeAll {
|
||||
if lbl, ok := labelMap[labelName]; ok {
|
||||
mdmProf.Labels = append(mdmProf.Labels, lbl)
|
||||
mdmProf.LabelsIncludeAll = append(mdmProf.LabelsIncludeAll, lbl)
|
||||
}
|
||||
}
|
||||
for _, labelName := range prof.LabelsExcludeAny {
|
||||
if lbl, ok := labelMap[labelName]; ok {
|
||||
mdmProf.LabelsExcludeAny = append(mdmProf.LabelsExcludeAny, lbl)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1844,9 +1899,14 @@ func getWindowsProfiles(
|
|||
Name: profile.Name,
|
||||
SyncML: profile.Contents,
|
||||
}
|
||||
for _, labelName := range profile.Labels {
|
||||
for _, labelName := range profile.LabelsIncludeAll {
|
||||
if lbl, ok := labelMap[labelName]; ok {
|
||||
mdmProf.Labels = append(mdmProf.Labels, lbl)
|
||||
mdmProf.LabelsIncludeAll = append(mdmProf.LabelsIncludeAll, lbl)
|
||||
}
|
||||
}
|
||||
for _, labelName := range profile.LabelsExcludeAny {
|
||||
if lbl, ok := labelMap[labelName]; ok {
|
||||
mdmProf.LabelsExcludeAny = append(mdmProf.LabelsExcludeAny, lbl)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1876,6 +1936,21 @@ func getWindowsProfiles(
|
|||
|
||||
func validateProfiles(profiles []fleet.MDMProfileBatchPayload) error {
|
||||
for _, profile := range profiles {
|
||||
// validate that only one of labels, labels_include_all and labels_exclude_any is provided.
|
||||
var count int
|
||||
for _, b := range []bool{
|
||||
len(profile.LabelsIncludeAll) > 0,
|
||||
len(profile.LabelsExcludeAny) > 0,
|
||||
len(profile.Labels) > 0,
|
||||
} {
|
||||
if b {
|
||||
count++
|
||||
}
|
||||
}
|
||||
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.`)
|
||||
}
|
||||
|
||||
if len(profile.Contents) > 1024*1024 {
|
||||
return fleet.NewInvalidArgumentError("mdm", "maximum configuration profile file size is 1 MB")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1088,11 +1088,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)
|
||||
_, err = svc.NewMDMWindowsConfigProfile(ctx, 0, "prof", strings.NewReader(winProfContent), nil, false)
|
||||
checkShouldFail(t, err, tt.shouldFailGlobalWrite)
|
||||
|
||||
// test authz create new profile (team 1)
|
||||
_, err = svc.NewMDMWindowsConfigProfile(ctx, 1, "prof", strings.NewReader(winProfContent), nil)
|
||||
_, err = svc.NewMDMWindowsConfigProfile(ctx, 1, "prof", strings.NewReader(winProfContent), nil, false)
|
||||
checkShouldFail(t, err, tt.shouldFailTeamWrite)
|
||||
|
||||
// test authz delete config profile (no team)
|
||||
|
|
@ -1174,7 +1174,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)
|
||||
_, err := svc.NewMDMWindowsConfigProfile(ctx, c.tmID, "foo", strings.NewReader(c.profile), nil, false)
|
||||
if c.wantErr != "" {
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, c.wantErr)
|
||||
|
|
@ -1567,6 +1567,34 @@ func TestValidateProfiles(t *testing.T) {
|
|||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Windows Profile With Deprecated Labels",
|
||||
profiles: []fleet.MDMProfileBatchPayload{
|
||||
{Name: "windowsProfile", Labels: []string{"a"}, Contents: []byte("<replace><Target><LocURI>Custom/URI</LocURI></Target></replace>")},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Windows Profile With Excluded Labels",
|
||||
profiles: []fleet.MDMProfileBatchPayload{
|
||||
{Name: "windowsProfile", LabelsExcludeAny: []string{"a"}, Contents: []byte("<replace><Target><LocURI>Custom/URI</LocURI></Target></replace>")},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Windows Profile With Included Labels",
|
||||
profiles: []fleet.MDMProfileBatchPayload{
|
||||
{Name: "windowsProfile", LabelsIncludeAll: []string{"a"}, Contents: []byte("<replace><Target><LocURI>Custom/URI</LocURI></Target></replace>")},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Windows Profile With Mixed Labels",
|
||||
profiles: []fleet.MDMProfileBatchPayload{
|
||||
{Name: "windowsProfile", Labels: []string{"z"}, LabelsIncludeAll: []string{"a"}, Contents: []byte("<replace><Target><LocURI>Custom/URI</LocURI></Target></replace>")},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Too large profile",
|
||||
profiles: []fleet.MDMProfileBatchPayload{
|
||||
|
|
|
|||
|
|
@ -116,6 +116,8 @@ github.com/fleetdm/fleet/v4/server/fleet/MDM MacOSSettings fleet.MacOSSettings
|
|||
github.com/fleetdm/fleet/v4/server/fleet/MacOSSettings CustomSettings []fleet.MDMProfileSpec
|
||||
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 LabelsExcludeAny []string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MacOSSettings DeprecatedEnableDiskEncryption *bool
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDM MacOSSetup fleet.MacOSSetup
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MacOSSetup BootstrapPackage optjson.String
|
||||
|
|
|
|||
|
|
@ -1,2 +1,4 @@
|
|||
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 LabelsExcludeAny []string
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ github.com/fleetdm/fleet/v4/server/fleet/TeamMDM MacOSSettings fleet.MacOSSettin
|
|||
github.com/fleetdm/fleet/v4/server/fleet/MacOSSettings CustomSettings []fleet.MDMProfileSpec
|
||||
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 LabelsExcludeAny []string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MacOSSettings DeprecatedEnableDiskEncryption *bool
|
||||
github.com/fleetdm/fleet/v4/server/fleet/TeamMDM MacOSSetup fleet.MacOSSetup
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MacOSSetup BootstrapPackage optjson.String
|
||||
|
|
|
|||
Loading…
Reference in a new issue