mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 17:08:53 +00:00
Custom OS settings: "include any label" option for custom target (#23802)
This commit is contained in:
commit
b8c9816e53
47 changed files with 1799 additions and 450 deletions
2
changes/22575-ui-for-include-any-labels
Normal file
2
changes/22575-ui-for-include-any-labels
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
- add UI for allowing users to install custom profiles on hosts that include any of the defined
|
||||
labels
|
||||
1
changes/22576-labels-include-any-gitops
Normal file
1
changes/22576-labels-include-any-gitops
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Add support for labels_include_any to gitops
|
||||
1
changes/22578-db-schema
Normal file
1
changes/22578-db-schema
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Adds DB support for "include any" label profile deployment
|
||||
1
changes/22581-cron-updates
Normal file
1
changes/22581-cron-updates
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Adds support for "include any" label/profile relationships to the profile reconciliation machinery.
|
||||
|
|
@ -2079,11 +2079,13 @@ func TestGitOpsCustomSettings(t *testing.T) {
|
|||
}{
|
||||
{"testdata/gitops/global_macos_windows_custom_settings_valid.yml", ""},
|
||||
{"testdata/gitops/global_macos_custom_settings_valid_deprecated.yml", ""},
|
||||
{"testdata/gitops/global_windows_custom_settings_invalid_label_mix.yml", `For each profile, only one of "labels_exclude_any", "labels_include_all" or "labels" can be included`},
|
||||
{"testdata/gitops/global_windows_custom_settings_invalid_label_mix.yml", `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included`},
|
||||
{"testdata/gitops/global_windows_custom_settings_invalid_label_mix_2.yml", `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included`},
|
||||
{"testdata/gitops/global_windows_custom_settings_unknown_label.yml", `some or all the labels provided don't exist`},
|
||||
{"testdata/gitops/team_macos_windows_custom_settings_valid.yml", ""},
|
||||
{"testdata/gitops/team_macos_custom_settings_valid_deprecated.yml", ""},
|
||||
{"testdata/gitops/team_macos_windows_custom_settings_invalid_labels_mix.yml", `For each profile, only one of "labels_exclude_any", "labels_include_all" or "labels" can be included.`},
|
||||
{"testdata/gitops/team_macos_windows_custom_settings_invalid_labels_mix.yml", `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included`},
|
||||
{"testdata/gitops/team_macos_windows_custom_settings_invalid_labels_mix_2.yml", `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included`},
|
||||
{"testdata/gitops/team_macos_windows_custom_settings_unknown_label.yml", `some or all the labels provided don't exist`},
|
||||
}
|
||||
for _, c := range cases {
|
||||
|
|
|
|||
96
cmd/fleetctl/testdata/gitops/global_windows_custom_settings_invalid_label_mix_2.yml
vendored
Normal file
96
cmd/fleetctl/testdata/gitops/global_windows_custom_settings_invalid_label_mix_2.yml
vendored
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
controls:
|
||||
windows_settings:
|
||||
custom_settings:
|
||||
- path: ./lib/windows-screenlock.xml
|
||||
labels_include_all:
|
||||
- B
|
||||
labels_include_any:
|
||||
- C
|
||||
scripts:
|
||||
enable_disk_encryption: false
|
||||
macos_migration:
|
||||
enable: false
|
||||
mode: ""
|
||||
webhook_url: ""
|
||||
macos_setup:
|
||||
bootstrap_package: null
|
||||
enable_end_user_authentication: false
|
||||
macos_setup_assistant: null
|
||||
macos_updates:
|
||||
deadline: null
|
||||
minimum_version: null
|
||||
windows_enabled_and_configured: true
|
||||
windows_updates:
|
||||
deadline_days: null
|
||||
grace_period_days: null
|
||||
queries:
|
||||
policies:
|
||||
agent_options:
|
||||
command_line_flags:
|
||||
distributed_denylist_duration: 0
|
||||
config:
|
||||
options:
|
||||
disable_distributed: false
|
||||
distributed_interval: 10
|
||||
distributed_plugin: tls
|
||||
distributed_tls_max_attempts: 3
|
||||
logger_tls_endpoint: /api/v1/osquery/log
|
||||
pack_delimiter: /
|
||||
org_settings:
|
||||
server_settings:
|
||||
deferred_save_host: false
|
||||
enable_analytics: true
|
||||
live_query_disabled: false
|
||||
query_report_cap: 2000
|
||||
query_reports_disabled: false
|
||||
scripts_disabled: false
|
||||
server_url: $FLEET_SERVER_URL
|
||||
ai_features_disabled: true
|
||||
org_info:
|
||||
contact_url: https://fleetdm.com/company/contact
|
||||
org_logo_url: ""
|
||||
org_logo_url_light_background: ""
|
||||
org_name: $ORG_NAME
|
||||
smtp_settings:
|
||||
authentication_method: authmethod_plain
|
||||
authentication_type: authtype_username_password
|
||||
configured: false
|
||||
domain: ""
|
||||
enable_smtp: false
|
||||
enable_ssl_tls: true
|
||||
enable_start_tls: true
|
||||
password: ""
|
||||
port: 587
|
||||
sender_address: ""
|
||||
server: ""
|
||||
user_name: ""
|
||||
verify_ssl_certs: true
|
||||
sso_settings:
|
||||
enable_jit_provisioning: false
|
||||
enable_jit_role_sync: false
|
||||
enable_sso: true
|
||||
enable_sso_idp_login: false
|
||||
entity_id: https://saml.example.com/entityid
|
||||
idp_image_url: ""
|
||||
idp_name: MockSAML
|
||||
issuer_uri: ""
|
||||
metadata: ""
|
||||
metadata_url: https://mocksaml.com/api/saml/metadata
|
||||
integrations:
|
||||
mdm:
|
||||
webhook_settings:
|
||||
fleet_desktop:
|
||||
transparency_url: https://fleetdm.com/transparency
|
||||
host_expiry_settings:
|
||||
host_expiry_enabled: false
|
||||
activity_expiry_settings:
|
||||
activity_expiry_enabled: true
|
||||
activity_expiry_window: 60
|
||||
features:
|
||||
enable_host_users: true
|
||||
enable_software_inventory: true
|
||||
vulnerability_settings:
|
||||
databases_path: ""
|
||||
secrets:
|
||||
- secret: ABC
|
||||
software:
|
||||
29
cmd/fleetctl/testdata/gitops/team_macos_windows_custom_settings_invalid_labels_mix_2.yml
vendored
Normal file
29
cmd/fleetctl/testdata/gitops/team_macos_windows_custom_settings_invalid_labels_mix_2.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_any:
|
||||
- A
|
||||
labels:
|
||||
- B
|
||||
windows_settings:
|
||||
custom_settings:
|
||||
- path: ./lib/windows-screenlock.xml
|
||||
labels_include_any:
|
||||
- A
|
||||
labels_exclude_any:
|
||||
- C
|
||||
policies:
|
||||
queries:
|
||||
software:
|
||||
|
|
@ -1435,14 +1435,14 @@ func (svc *Service) editTeamFromSpec(
|
|||
func validateTeamCustomSettings(invalid *fleet.InvalidArgumentError, prefix string, customSettings []fleet.MDMProfileSpec) {
|
||||
for i, prof := range customSettings {
|
||||
count := 0
|
||||
for _, b := range []bool{len(prof.Labels) > 0, len(prof.LabelsIncludeAll) > 0, len(prof.LabelsExcludeAny) > 0} {
|
||||
for _, b := range []bool{len(prof.Labels) > 0, len(prof.LabelsIncludeAll) > 0, len(prof.LabelsIncludeAny) > 0, len(prof.LabelsExcludeAny) > 0} {
|
||||
if b {
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count > 1 {
|
||||
invalid.Append(fmt.Sprintf("%s_settings.custom_settings", prefix),
|
||||
fmt.Sprintf(`Couldn't edit %s_settings.custom_settings. For each profile, only one of "labels_exclude_any", "labels_include_all" or "labels" can be included.`, prefix))
|
||||
fmt.Sprintf(`Couldn't edit %s_settings.custom_settings. For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`, prefix))
|
||||
}
|
||||
if len(prof.Labels) > 0 {
|
||||
customSettings[i].LabelsIncludeAll = customSettings[i].Labels
|
||||
|
|
|
|||
|
|
@ -111,6 +111,7 @@ export interface IMdmProfile {
|
|||
updated_at: string;
|
||||
checksum: string | null; // null for windows profiles
|
||||
labels_include_all?: IProfileLabel[];
|
||||
labels_include_any?: IProfileLabel[];
|
||||
labels_exclude_any?: IProfileLabel[];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import Pagination from "pages/ManageControlsPage/components/Pagination";
|
|||
import UploadList from "../../../components/UploadList";
|
||||
|
||||
import AddProfileCard from "./components/ProfileUploader/components/AddProfileCard";
|
||||
import AddProfileModal from "./components/ProfileUploader/components/AddProfileModal/AddProfileModal";
|
||||
import AddProfileModal from "./components/ProfileUploader/components/AddProfileModal";
|
||||
import DeleteProfileModal from "./components/DeleteProfileModal/DeleteProfileModal";
|
||||
import ProfileLabelsModal from "./components/ProfileLabelsModal/ProfileLabelsModal";
|
||||
import ProfileListItem from "./components/ProfileListItem";
|
||||
|
|
@ -136,7 +136,7 @@ const CustomSettings = ({
|
|||
}
|
||||
|
||||
if (!profiles?.length) {
|
||||
return null;
|
||||
return <AddProfileCard setShowModal={setShowAddProfileModal} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -144,11 +144,11 @@ const CustomSettings = ({
|
|||
<UploadList
|
||||
keyAttribute="profile_uuid"
|
||||
listItems={profiles}
|
||||
HeadingComponent={() =>
|
||||
ProfileListHeading({
|
||||
onClickAddProfile: () => setShowAddProfileModal(true),
|
||||
})
|
||||
}
|
||||
HeadingComponent={() => (
|
||||
<ProfileListHeading
|
||||
onClickAddProfile={() => setShowAddProfileModal(true)}
|
||||
/>
|
||||
)}
|
||||
ListItemComponent={({ listItem }) => (
|
||||
<ProfileListItem
|
||||
isPremium={!!isPremiumTier}
|
||||
|
|
@ -171,6 +171,7 @@ const CustomSettings = ({
|
|||
|
||||
const hasLabels =
|
||||
!!profileLabelsModalData?.labels_include_all?.length ||
|
||||
!!profileLabelsModalData?.labels_include_any?.length ||
|
||||
!!profileLabelsModalData?.labels_exclude_any?.length;
|
||||
|
||||
return (
|
||||
|
|
@ -184,13 +185,7 @@ const CustomSettings = ({
|
|||
url="https://fleetdm.com/learn-more-about/custom-os-settings"
|
||||
/>
|
||||
</p>
|
||||
{renderProfileList()}
|
||||
{!isLoadingProfiles && !isErrorProfiles && !profiles?.length && (
|
||||
<AddProfileCard
|
||||
baseClass="add-profile"
|
||||
setShowModal={setShowAddProfileModal}
|
||||
/>
|
||||
)}
|
||||
<>{renderProfileList()}</>
|
||||
{showAddProfileModal && (
|
||||
<AddProfileModal
|
||||
currentTeamId={currentTeamId}
|
||||
|
|
@ -209,7 +204,6 @@ const CustomSettings = ({
|
|||
)}
|
||||
{isPremiumTier && hasLabels && (
|
||||
<ProfileLabelsModal
|
||||
baseClass={baseClass}
|
||||
profile={profileLabelsModalData}
|
||||
setModalData={setProfileLabelsModalData}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,69 +1,15 @@
|
|||
.custom-settings {
|
||||
.section-header {
|
||||
margin: 0;
|
||||
padding: 0 0 12px 0;
|
||||
|
||||
h2 {
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-size: $x-small;
|
||||
margin: $pad-large 0;
|
||||
}
|
||||
|
||||
&__profiles-header {
|
||||
padding: $pad-medium $pad-large;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: $x-small;
|
||||
font-weight: $bold;
|
||||
border-bottom: 1px solid $ui-fleet-black-10;
|
||||
}
|
||||
|
||||
&__profile-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__pagination-controls {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin: $pad-large 0;
|
||||
}
|
||||
|
||||
&__file-uploader {
|
||||
margin-top: $pad-xxlarge;
|
||||
}
|
||||
|
||||
&__labels-list {
|
||||
border-radius: 6px;
|
||||
border: 1px solid $ui-fleet-black-10;
|
||||
|
||||
&--label {
|
||||
display: flex;
|
||||
height: 41px;
|
||||
padding: 0 $pad-large;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid $ui-fleet-black-10;
|
||||
|
||||
.warning {
|
||||
display: flex;
|
||||
padding: 0;
|
||||
gap: $pad-small;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-list {
|
||||
&__list {
|
||||
.list-item__label-count {
|
||||
|
|
@ -84,99 +30,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-profile {
|
||||
&__card--content-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $pad-medium;
|
||||
padding: 28.5px 0;
|
||||
}
|
||||
|
||||
&__profile-graphic {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $pad-small;
|
||||
|
||||
&--message {
|
||||
text-align: center;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&__button-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: $pad-medium;
|
||||
}
|
||||
|
||||
&__target {
|
||||
margin: $pad-large 0 $pad-small 0;
|
||||
}
|
||||
|
||||
&__description {
|
||||
margin: $pad-medium 0;
|
||||
}
|
||||
|
||||
&__no-labels {
|
||||
display: flex;
|
||||
height: 187px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $pad-small;
|
||||
justify-content: center;
|
||||
|
||||
span {
|
||||
color: $ui-fleet-black-75;
|
||||
}
|
||||
}
|
||||
|
||||
&__checkboxes {
|
||||
display: flex;
|
||||
max-height: 187px;
|
||||
flex-direction: column;
|
||||
border-radius: $border-radius;
|
||||
border: 1px solid $ui-fleet-black-10;
|
||||
overflow-y: auto;
|
||||
|
||||
.loading-spinner {
|
||||
margin: 69.5px auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
width: 100%;
|
||||
padding: $pad-small $pad-medium;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid $ui-fleet-black-10;
|
||||
}
|
||||
|
||||
.form-field--checkbox {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__label-name {
|
||||
padding-left: $pad-large;
|
||||
}
|
||||
|
||||
.fleet-checkbox {
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&__label {
|
||||
width: 490px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,35 +6,7 @@ import InfoBanner from "components/InfoBanner";
|
|||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import Icon from "components/Icon";
|
||||
|
||||
interface IModalDescriptionProps {
|
||||
baseClass: string;
|
||||
profileName: string;
|
||||
targetType: "includeAll" | "excludeAny";
|
||||
}
|
||||
|
||||
const ModalDescription = ({
|
||||
baseClass,
|
||||
profileName,
|
||||
targetType,
|
||||
}: IModalDescriptionProps) => {
|
||||
const targetTypeText =
|
||||
targetType === "includeAll" ? (
|
||||
<>
|
||||
have <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 baseClass = "profile-labels-modal";
|
||||
|
||||
const BrokenLabelWarning = () => (
|
||||
<InfoBanner color="yellow">
|
||||
|
|
@ -51,16 +23,10 @@ const BrokenLabelWarning = () => (
|
|||
</InfoBanner>
|
||||
);
|
||||
|
||||
const LabelsList = ({
|
||||
baseClass,
|
||||
labels,
|
||||
}: {
|
||||
baseClass: string;
|
||||
labels: IProfileLabel[];
|
||||
}) => (
|
||||
<div className={`${baseClass}__labels-list`}>
|
||||
const LabelsList = ({ labels }: { labels: IProfileLabel[] }) => (
|
||||
<ul className={`${baseClass}__labels-list`}>
|
||||
{labels.map((label) => (
|
||||
<div key={label.name} className={`${baseClass}__labels-list--label`}>
|
||||
<li key={label.name} className={`${baseClass}__labels-list--label`}>
|
||||
{label.name}
|
||||
{label.broken && (
|
||||
<span className={`${baseClass}__labels-list--label warning`}>
|
||||
|
|
@ -68,19 +34,17 @@ const LabelsList = ({
|
|||
Label deleted
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
</ul>
|
||||
);
|
||||
|
||||
interface IProfileLabelsModalProps {
|
||||
baseClass: string;
|
||||
profile: IMdmProfile | null;
|
||||
setModalData: React.Dispatch<React.SetStateAction<IMdmProfile | null>>;
|
||||
}
|
||||
|
||||
const ProfileLabelsModal = ({
|
||||
baseClass,
|
||||
profile,
|
||||
setModalData,
|
||||
}: IProfileLabelsModalProps) => {
|
||||
|
|
@ -88,30 +52,53 @@ const ProfileLabelsModal = ({
|
|||
return null;
|
||||
}
|
||||
|
||||
const { name, labels_include_all, labels_exclude_any } = profile;
|
||||
const labels = labels_include_all || labels_exclude_any;
|
||||
const {
|
||||
name,
|
||||
labels_include_all,
|
||||
labels_include_any,
|
||||
labels_exclude_any,
|
||||
} = profile;
|
||||
const labels = labels_include_all || labels_include_any || labels_exclude_any;
|
||||
|
||||
if (!labels?.length) {
|
||||
// caller ensures this never happens
|
||||
return null;
|
||||
}
|
||||
|
||||
const renderlabelDescription = () => {
|
||||
let targetTypeText = <></>;
|
||||
if (labels_include_all) {
|
||||
targetTypeText = <b>have all</b>;
|
||||
} else if (labels_include_any) {
|
||||
targetTypeText = <b>have any</b>;
|
||||
} else {
|
||||
targetTypeText = <b>don't have any</b>;
|
||||
}
|
||||
|
||||
return (
|
||||
<p className={`${baseClass}__description`}>
|
||||
<b>{name}</b> profile only applies to hosts that {targetTypeText} of
|
||||
these labels:
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal title="Custom target" onExit={() => setModalData(null)}>
|
||||
<div className={`${baseClass}__modal-content-wrap`}>
|
||||
<Modal
|
||||
className={baseClass}
|
||||
title="Custom target"
|
||||
onExit={() => setModalData(null)}
|
||||
>
|
||||
<>
|
||||
{labels.some((label) => label.broken) && <BrokenLabelWarning />}
|
||||
<ModalDescription
|
||||
baseClass={baseClass}
|
||||
profileName={name}
|
||||
targetType={labels_include_all ? "includeAll" : "excludeAny"}
|
||||
/>
|
||||
<LabelsList baseClass={baseClass} labels={labels} />
|
||||
<>{renderlabelDescription()}</>
|
||||
<LabelsList labels={labels} />
|
||||
<div className="modal-cta-wrap">
|
||||
<Button variant="brand" onClick={() => setModalData(null)}>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
.profile-labels-modal {
|
||||
&__description {
|
||||
font-size: $x-small;
|
||||
margin: $pad-large 0 $pad-medium;
|
||||
}
|
||||
|
||||
&__labels-list {
|
||||
border-radius: 6px;
|
||||
border: 1px solid $ui-fleet-black-10;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
&--label {
|
||||
display: flex;
|
||||
height: 41px;
|
||||
padding: 0 $pad-large;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid $ui-fleet-black-10;
|
||||
|
||||
.warning {
|
||||
display: flex;
|
||||
padding: 0;
|
||||
gap: $pad-small;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -87,6 +87,7 @@ const ProfileListItem = ({
|
|||
const {
|
||||
created_at,
|
||||
labels_include_all,
|
||||
labels_include_any,
|
||||
labels_exclude_any,
|
||||
name,
|
||||
platform,
|
||||
|
|
@ -102,7 +103,7 @@ const ProfileListItem = ({
|
|||
FileSaver.saveAs(file);
|
||||
};
|
||||
|
||||
const labels = labels_include_all || labels_exclude_any;
|
||||
const labels = labels_include_all || labels_include_any || labels_exclude_any;
|
||||
|
||||
const renderLabelInfo = () => {
|
||||
if (!isPremium || labels === undefined || labels.length === 0) {
|
||||
|
|
|
|||
|
|
@ -2,16 +2,16 @@ import React from "react";
|
|||
|
||||
import Card from "components/Card";
|
||||
import Button from "components/buttons/Button";
|
||||
import ProfileGraphic from "./AddProfileGraphic";
|
||||
import ProfileGraphic from "../AddProfileGraphic";
|
||||
|
||||
const AddProfileCard = ({
|
||||
baseClass,
|
||||
setShowModal,
|
||||
}: {
|
||||
baseClass: string;
|
||||
const baseClass = "add-profile-card";
|
||||
|
||||
interface IAddProfileCardProps {
|
||||
setShowModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}) => (
|
||||
<Card color="gray" className={`${baseClass}__card`}>
|
||||
}
|
||||
|
||||
const AddProfileCard = ({ setShowModal }: IAddProfileCardProps) => (
|
||||
<Card color="gray" className={baseClass}>
|
||||
<div className={`${baseClass}__card--content-wrap`}>
|
||||
<ProfileGraphic baseClass={baseClass} showMessage />
|
||||
<Button
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
.add-profile-card {
|
||||
&__card--content-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $pad-medium;
|
||||
padding: 28.5px 0;
|
||||
}
|
||||
|
||||
&__profile-graphic {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $pad-small;
|
||||
|
||||
&--message {
|
||||
text-align: center;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./AddProfileCard";
|
||||
|
|
@ -32,6 +32,7 @@ import {
|
|||
CUSTOM_TARGET_OPTIONS,
|
||||
CustomTargetOption,
|
||||
generateLabelKey,
|
||||
getDescriptionText,
|
||||
listNamesFromSelectedLabels,
|
||||
} from "./helpers";
|
||||
|
||||
|
|
@ -152,18 +153,6 @@ const LabelChooser = ({
|
|||
[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} />;
|
||||
|
|
@ -206,7 +195,9 @@ const LabelChooser = ({
|
|||
searchable={false}
|
||||
onChange={onSelectCustomTargetOption}
|
||||
/>
|
||||
<div className={`${baseClass}__description`}>{descriptionText}</div>
|
||||
<div className={`${baseClass}__description`}>
|
||||
{getDescriptionText(customTargetOption)}
|
||||
</div>
|
||||
<div className={`${baseClass}__checkboxes`}>{renderLabels()}</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,11 +5,22 @@ import { IDropdownOption } from "interfaces/dropdownOption";
|
|||
export const CUSTOM_TARGET_OPTIONS: IDropdownOption[] = [
|
||||
{
|
||||
value: "labelsIncludeAll",
|
||||
label: "Include all ",
|
||||
label: "Include all",
|
||||
helpText: (
|
||||
<>
|
||||
Profile will only be applied to hosts that have <b>all</b> of these
|
||||
labels{" "}
|
||||
Profile will only be applied to hosts that <b>have all</b> of these
|
||||
labels.
|
||||
</>
|
||||
),
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
value: "labelsIncludeAny",
|
||||
label: "Include any",
|
||||
helpText: (
|
||||
<>
|
||||
Profile will only be applied to hosts that <b>have any</b> of these
|
||||
labels.
|
||||
</>
|
||||
),
|
||||
disabled: false,
|
||||
|
|
@ -19,8 +30,8 @@ export const CUSTOM_TARGET_OPTIONS: IDropdownOption[] = [
|
|||
label: "Exclude any",
|
||||
helpText: (
|
||||
<>
|
||||
Profile will be applied to hosts that don't have <b>any</b> of
|
||||
these labels{" "}
|
||||
Profile will only be applied to hosts that <b>don't have any</b> of
|
||||
these labels.
|
||||
</>
|
||||
),
|
||||
disabled: false,
|
||||
|
|
@ -36,7 +47,10 @@ export const listNamesFromSelectedLabels = (dict: Record<string, boolean>) => {
|
|||
}, [] as string[]);
|
||||
};
|
||||
|
||||
export type CustomTargetOption = "labelsIncludeAll" | "labelsExcludeAny";
|
||||
export type CustomTargetOption =
|
||||
| "labelsIncludeAll"
|
||||
| "labelsIncldeAny"
|
||||
| "labelsExcludeAny";
|
||||
|
||||
export const generateLabelKey = (
|
||||
target: string,
|
||||
|
|
@ -51,3 +65,8 @@ export const generateLabelKey = (
|
|||
[customTargetOption]: listNamesFromSelectedLabels(selectedLabels),
|
||||
};
|
||||
};
|
||||
|
||||
export const getDescriptionText = (value: string) => {
|
||||
return CUSTOM_TARGET_OPTIONS.find((option) => option.value === value)
|
||||
?.helpText;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ export interface IUploadProfileApiParams {
|
|||
file: File;
|
||||
teamId?: number;
|
||||
labelsIncludeAll?: string[];
|
||||
labelsIncludeAny?: string[];
|
||||
labelsExcludeAny?: string[];
|
||||
}
|
||||
|
||||
|
|
@ -132,6 +133,7 @@ const mdmService = {
|
|||
file,
|
||||
teamId,
|
||||
labelsIncludeAll,
|
||||
labelsIncludeAny,
|
||||
labelsExcludeAny,
|
||||
}: IUploadProfileApiParams) => {
|
||||
const { MDM_PROFILES } = endpoints;
|
||||
|
|
@ -143,11 +145,18 @@ const mdmService = {
|
|||
formData.append("team_id", teamId.toString());
|
||||
}
|
||||
|
||||
if (labelsIncludeAll || labelsExcludeAny) {
|
||||
const labels = labelsIncludeAll || labelsExcludeAny;
|
||||
const labelKey = labelsIncludeAll
|
||||
? "labels_include_all"
|
||||
: "labels_exclude_any";
|
||||
if (labelsIncludeAll || labelsIncludeAny || labelsExcludeAny) {
|
||||
const labels = labelsIncludeAll || labelsIncludeAny || labelsExcludeAny;
|
||||
|
||||
let labelKey = "";
|
||||
if (labelsIncludeAll) {
|
||||
labelKey = "labels_include_all";
|
||||
} else if (labelsIncludeAny) {
|
||||
labelKey = "labels_include_any";
|
||||
} else {
|
||||
labelKey = "labels_exclude_any";
|
||||
}
|
||||
|
||||
labels?.forEach((label) => {
|
||||
formData.append(labelKey, label);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -79,14 +79,23 @@ INSERT INTO
|
|||
// filled in.
|
||||
profileID, _ = res.LastInsertId()
|
||||
|
||||
labels := make([]fleet.ConfigurationProfileLabel, 0, len(cp.LabelsIncludeAll)+len(cp.LabelsExcludeAny))
|
||||
labels := make([]fleet.ConfigurationProfileLabel, 0, len(cp.LabelsIncludeAll)+len(cp.LabelsIncludeAny)+len(cp.LabelsExcludeAny))
|
||||
for i := range cp.LabelsIncludeAll {
|
||||
cp.LabelsIncludeAll[i].ProfileUUID = profUUID
|
||||
cp.LabelsIncludeAll[i].Exclude = false
|
||||
cp.LabelsIncludeAll[i].RequireAll = true
|
||||
labels = append(labels, cp.LabelsIncludeAll[i])
|
||||
}
|
||||
for i := range cp.LabelsIncludeAny {
|
||||
cp.LabelsIncludeAny[i].ProfileUUID = profUUID
|
||||
cp.LabelsIncludeAny[i].Exclude = false
|
||||
cp.LabelsIncludeAny[i].RequireAll = false
|
||||
labels = append(labels, cp.LabelsIncludeAny[i])
|
||||
}
|
||||
for i := range cp.LabelsExcludeAny {
|
||||
cp.LabelsExcludeAny[i].ProfileUUID = profUUID
|
||||
cp.LabelsExcludeAny[i].Exclude = true
|
||||
cp.LabelsExcludeAny[i].RequireAll = false
|
||||
labels = append(labels, cp.LabelsExcludeAny[i])
|
||||
}
|
||||
if _, err := batchSetProfileLabelAssociationsDB(ctx, tx, labels, "darwin"); err != nil {
|
||||
|
|
@ -238,9 +247,19 @@ WHERE
|
|||
return nil, err
|
||||
}
|
||||
for _, lbl := range labels {
|
||||
if lbl.Exclude {
|
||||
switch {
|
||||
case lbl.Exclude && lbl.RequireAll:
|
||||
// this should never happen so log it for debugging
|
||||
level.Debug(ds.logger).Log("msg", "unsupported profile label: cannot be both exclude and require all",
|
||||
"profile_uuid", lbl.ProfileUUID,
|
||||
"label_name", lbl.LabelName,
|
||||
)
|
||||
case lbl.Exclude && !lbl.RequireAll:
|
||||
res.LabelsExcludeAny = append(res.LabelsExcludeAny, lbl)
|
||||
} else {
|
||||
case !lbl.Exclude && !lbl.RequireAll:
|
||||
res.LabelsIncludeAny = append(res.LabelsIncludeAny, lbl)
|
||||
default:
|
||||
// default include all
|
||||
res.LabelsIncludeAll = append(res.LabelsIncludeAll, lbl)
|
||||
}
|
||||
}
|
||||
|
|
@ -280,9 +299,19 @@ WHERE
|
|||
return nil, err
|
||||
}
|
||||
for _, lbl := range labels {
|
||||
if lbl.Exclude {
|
||||
switch {
|
||||
case lbl.Exclude && lbl.RequireAll:
|
||||
// this should never happen so log it for debugging
|
||||
level.Debug(ds.logger).Log("msg", "unsupported profile label: cannot be both exclude and require all",
|
||||
"profile_uuid", lbl.ProfileUUID,
|
||||
"label_name", lbl.LabelName,
|
||||
)
|
||||
case lbl.Exclude && !lbl.RequireAll:
|
||||
res.LabelsExcludeAny = append(res.LabelsExcludeAny, lbl)
|
||||
} else {
|
||||
case !lbl.Exclude && !lbl.RequireAll:
|
||||
res.LabelsIncludeAny = append(res.LabelsIncludeAny, lbl)
|
||||
default:
|
||||
// default include all
|
||||
res.LabelsIncludeAll = append(res.LabelsIncludeAll, lbl)
|
||||
}
|
||||
}
|
||||
|
|
@ -1819,16 +1848,28 @@ ON DUPLICATE KEY UPDATE
|
|||
|
||||
for _, label := range incomingProf.LabelsIncludeAll {
|
||||
label.ProfileUUID = newlyInsertedProf.ProfileUUID
|
||||
label.Exclude = false
|
||||
label.RequireAll = true
|
||||
incomingLabels = append(incomingLabels, label)
|
||||
}
|
||||
for _, label := range incomingProf.LabelsIncludeAny {
|
||||
label.ProfileUUID = newlyInsertedProf.ProfileUUID
|
||||
label.Exclude = false
|
||||
label.RequireAll = false
|
||||
incomingLabels = append(incomingLabels, label)
|
||||
}
|
||||
for _, label := range incomingProf.LabelsExcludeAny {
|
||||
label.ProfileUUID = newlyInsertedProf.ProfileUUID
|
||||
label.Exclude = true
|
||||
label.RequireAll = false
|
||||
incomingLabels = append(incomingLabels, label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: At what point are we deleting label associations for existing profiles (e.g. if the user
|
||||
// removes all labels from a profile in gitops, shouldn't we remove the old associations)?
|
||||
|
||||
// insert label associations
|
||||
var updatedLabels bool
|
||||
if updatedLabels, err = batchSetProfileLabelAssociationsDB(ctx, tx, incomingLabels,
|
||||
|
|
@ -1949,7 +1990,7 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB(
|
|||
( hmap.profile_uuid IS NULL AND hmap.host_uuid IS NULL ) OR
|
||||
-- profiles in A and B but with operation type "remove"
|
||||
( hmap.host_uuid IS NOT NULL AND ( hmap.operation_type = ? OR hmap.operation_type IS NULL ) )
|
||||
`, fmt.Sprintf(appleMDMProfilesDesiredStateQuery, "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)"))
|
||||
`, fmt.Sprintf(appleMDMProfilesDesiredStateQuery, "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)"))
|
||||
|
||||
// batches of 10K hosts because h.uuid appears three times in the
|
||||
// query, and the max number of prepared statements is 65K, this was
|
||||
|
|
@ -1971,7 +2012,7 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB(
|
|||
|
||||
batchUUIDs := uuids[start:end]
|
||||
|
||||
stmt, args, err := sqlx.In(toInstallStmt, batchUUIDs, batchUUIDs, batchUUIDs, fleet.MDMOperationTypeRemove)
|
||||
stmt, args, err := sqlx.In(toInstallStmt, batchUUIDs, batchUUIDs, batchUUIDs, batchUUIDs, fleet.MDMOperationTypeRemove)
|
||||
if err != nil {
|
||||
return false, ctxerr.Wrapf(ctx, err, "building statement to select profiles to install, batch %d of %d", i,
|
||||
selectProfilesTotalBatches)
|
||||
|
|
@ -2018,7 +2059,7 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB(
|
|||
mcpl.apple_profile_uuid = hmap.profile_uuid AND
|
||||
mcpl.label_id IS NULL
|
||||
)
|
||||
`, fmt.Sprintf(appleMDMProfilesDesiredStateQuery, "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)"))
|
||||
`, fmt.Sprintf(appleMDMProfilesDesiredStateQuery, "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)"))
|
||||
|
||||
var currentProfiles []*fleet.MDMAppleProfilePayload
|
||||
for i := 0; i < selectProfilesTotalBatches; i++ {
|
||||
|
|
@ -2030,7 +2071,7 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB(
|
|||
|
||||
batchUUIDs := uuids[start:end]
|
||||
|
||||
stmt, args, err := sqlx.In(toRemoveStmt, batchUUIDs, batchUUIDs, batchUUIDs, batchUUIDs, fleet.MDMOperationTypeRemove)
|
||||
stmt, args, err := sqlx.In(toRemoveStmt, batchUUIDs, batchUUIDs, batchUUIDs, batchUUIDs, batchUUIDs, fleet.MDMOperationTypeRemove)
|
||||
if err != nil {
|
||||
return false, ctxerr.Wrap(ctx, err, "building profiles to remove statement")
|
||||
}
|
||||
|
|
@ -2360,7 +2401,7 @@ func generateDesiredStateQuery(entityType string) string {
|
|||
JOIN nano_enrollments ne
|
||||
ON ne.device_id = h.uuid
|
||||
JOIN ${mdmEntityLabelsTable} mel
|
||||
ON mel.${appleEntityUUIDColumn} = mae.${entityUUIDColumn} AND mel.exclude = 0
|
||||
ON mel.${appleEntityUUIDColumn} = mae.${entityUUIDColumn} AND mel.exclude = 0 AND mel.require_all = 1
|
||||
LEFT OUTER JOIN label_membership lm
|
||||
ON lm.label_id = mel.label_id AND lm.host_id = h.id
|
||||
WHERE
|
||||
|
|
@ -2400,7 +2441,7 @@ func generateDesiredStateQuery(entityType string) string {
|
|||
JOIN nano_enrollments ne
|
||||
ON ne.device_id = h.uuid
|
||||
JOIN ${mdmEntityLabelsTable} mel
|
||||
ON mel.${appleEntityUUIDColumn} = mae.${entityUUIDColumn} AND mel.exclude = 1
|
||||
ON mel.${appleEntityUUIDColumn} = mae.${entityUUIDColumn} AND mel.exclude = 1 AND mel.require_all = 0
|
||||
LEFT OUTER JOIN labels lbl
|
||||
ON lbl.id = mel.label_id
|
||||
LEFT OUTER JOIN label_membership lm
|
||||
|
|
@ -2415,6 +2456,42 @@ func generateDesiredStateQuery(entityType string) string {
|
|||
HAVING
|
||||
-- considers only the profiles with labels, without any broken label, with results reported after all labels were created and with the host not in any label
|
||||
${countEntityLabelsColumn} > 0 AND ${countEntityLabelsColumn} = count_non_broken_labels AND ${countEntityLabelsColumn} = count_host_updated_after_labels AND count_host_labels = 0
|
||||
|
||||
UNION
|
||||
|
||||
-- label-based entities where the host is a member of any the labels (include-any).
|
||||
-- by design, "include" labels cannot match if they are broken (the host cannot be
|
||||
-- a member of a deleted label).
|
||||
SELECT
|
||||
mae.${entityUUIDColumn},
|
||||
h.uuid as host_uuid,
|
||||
h.platform as host_platform,
|
||||
mae.identifier as ${entityIdentifierColumn},
|
||||
mae.name as ${entityNameColumn},
|
||||
mae.checksum as checksum,
|
||||
COUNT(*) as ${countEntityLabelsColumn},
|
||||
COUNT(mel.label_id) as count_non_broken_labels,
|
||||
COUNT(lm.label_id) as count_host_labels,
|
||||
0 as count_host_updated_after_labels
|
||||
FROM
|
||||
${mdmAppleEntityTable} mae
|
||||
JOIN hosts h
|
||||
ON h.team_id = mae.team_id OR (h.team_id IS NULL AND mae.team_id = 0)
|
||||
JOIN nano_enrollments ne
|
||||
ON ne.device_id = h.uuid
|
||||
JOIN ${mdmEntityLabelsTable} mel
|
||||
ON mel.${appleEntityUUIDColumn} = mae.${entityUUIDColumn} AND mel.exclude = 0 AND mel.require_all = 0
|
||||
LEFT OUTER JOIN label_membership lm
|
||||
ON lm.label_id = mel.label_id AND lm.host_id = h.id
|
||||
WHERE
|
||||
(h.platform = 'darwin' OR h.platform = 'ios' OR h.platform = 'ipados') AND
|
||||
ne.enabled = 1 AND
|
||||
ne.type = 'Device' AND
|
||||
( %s )
|
||||
GROUP BY
|
||||
mae.${entityUUIDColumn}, h.uuid, h.platform, mae.identifier, mae.name, mae.checksum
|
||||
HAVING
|
||||
${countEntityLabelsColumn} > 0 AND count_host_labels >= 1
|
||||
`, func(s string) string { return dynamicNames[s] })
|
||||
}
|
||||
|
||||
|
|
@ -2473,7 +2550,7 @@ func generateEntitiesToInstallQuery(entityType string) string {
|
|||
( hmae.host_uuid IS NOT NULL AND ( hmae.operation_type = ? OR hmae.operation_type IS NULL ) ) OR
|
||||
-- entities in A and B with operation type "install" and NULL status
|
||||
( hmae.host_uuid IS NOT NULL AND hmae.operation_type = ? AND hmae.status IS NULL )
|
||||
`, func(s string) string { return dynamicNames[s] }), fmt.Sprintf(generateDesiredStateQuery(entityType), "TRUE", "TRUE", "TRUE"))
|
||||
`, func(s string) string { return dynamicNames[s] }), fmt.Sprintf(generateDesiredStateQuery(entityType), "TRUE", "TRUE", "TRUE", "TRUE"))
|
||||
}
|
||||
|
||||
// generateEntitiesToRemoveQuery is a set difference between:
|
||||
|
|
@ -2526,7 +2603,7 @@ func generateEntitiesToRemoveQuery(entityType string) string {
|
|||
mcpl.${appleEntityUUIDColumn} = hmae.${entityUUIDColumn} AND
|
||||
mcpl.label_id IS NULL
|
||||
)
|
||||
`, func(s string) string { return dynamicNames[s] }), fmt.Sprintf(generateDesiredStateQuery(entityType), "TRUE", "TRUE", "TRUE"))
|
||||
`, func(s string) string { return dynamicNames[s] }), fmt.Sprintf(generateDesiredStateQuery(entityType), "TRUE", "TRUE", "TRUE", "TRUE"))
|
||||
}
|
||||
|
||||
func (ds *Datastore) ListMDMAppleProfilesToInstall(ctx context.Context) ([]*fleet.MDMAppleProfilePayload, error) {
|
||||
|
|
@ -3837,7 +3914,8 @@ func (ds *Datastore) updateHostDEPAssignProfileResponses(ctx context.Context, pa
|
|||
}
|
||||
|
||||
func updateHostDEPAssignProfileResponses(ctx context.Context, tx sqlx.ExtContext, logger log.Logger, profileUUID string, serials []string,
|
||||
status string, abmTokenID *uint) error {
|
||||
status string, abmTokenID *uint,
|
||||
) error {
|
||||
if len(serials) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -4243,11 +4321,20 @@ WHERE
|
|||
|
||||
for _, label := range incomingDecl.LabelsIncludeAll {
|
||||
label.ProfileUUID = newlyInsertedDecl.DeclarationUUID
|
||||
label.Exclude = false
|
||||
label.RequireAll = true
|
||||
incomingLabels = append(incomingLabels, label)
|
||||
}
|
||||
for _, label := range incomingDecl.LabelsIncludeAny {
|
||||
label.ProfileUUID = newlyInsertedDecl.DeclarationUUID
|
||||
label.Exclude = false
|
||||
label.RequireAll = false
|
||||
incomingLabels = append(incomingLabels, label)
|
||||
}
|
||||
for _, label := range incomingDecl.LabelsExcludeAny {
|
||||
label.ProfileUUID = newlyInsertedDecl.DeclarationUUID
|
||||
label.Exclude = true
|
||||
label.RequireAll = false
|
||||
incomingLabels = append(incomingLabels, label)
|
||||
}
|
||||
}
|
||||
|
|
@ -4349,14 +4436,23 @@ func (ds *Datastore) insertOrUpsertMDMAppleDeclaration(ctx context.Context, insO
|
|||
}
|
||||
|
||||
labels := make([]fleet.ConfigurationProfileLabel, 0,
|
||||
len(declaration.LabelsIncludeAll)+len(declaration.LabelsExcludeAny))
|
||||
len(declaration.LabelsIncludeAll)+len(declaration.LabelsIncludeAny)+len(declaration.LabelsExcludeAny))
|
||||
for i := range declaration.LabelsIncludeAll {
|
||||
declaration.LabelsIncludeAll[i].ProfileUUID = declUUID
|
||||
declaration.LabelsIncludeAll[i].Exclude = false
|
||||
declaration.LabelsIncludeAll[i].RequireAll = true
|
||||
labels = append(labels, declaration.LabelsIncludeAll[i])
|
||||
}
|
||||
for i := range declaration.LabelsIncludeAny {
|
||||
declaration.LabelsIncludeAny[i].ProfileUUID = declUUID
|
||||
declaration.LabelsIncludeAny[i].Exclude = false
|
||||
declaration.LabelsIncludeAny[i].RequireAll = false
|
||||
labels = append(labels, declaration.LabelsIncludeAny[i])
|
||||
}
|
||||
for i := range declaration.LabelsExcludeAny {
|
||||
declaration.LabelsExcludeAny[i].ProfileUUID = declUUID
|
||||
declaration.LabelsExcludeAny[i].Exclude = true
|
||||
declaration.LabelsExcludeAny[i].RequireAll = false
|
||||
labels = append(labels, declaration.LabelsExcludeAny[i])
|
||||
}
|
||||
if _, err := batchSetDeclarationLabelAssociationsDB(ctx, tx, labels); err != nil {
|
||||
|
|
@ -4391,16 +4487,17 @@ func batchSetDeclarationLabelAssociationsDB(ctx context.Context, tx sqlx.ExtCont
|
|||
|
||||
upsertStmt := `
|
||||
INSERT INTO mdm_declaration_labels
|
||||
(apple_declaration_uuid, label_id, label_name, exclude)
|
||||
(apple_declaration_uuid, label_id, label_name, exclude, require_all)
|
||||
VALUES
|
||||
%s
|
||||
ON DUPLICATE KEY UPDATE
|
||||
label_id = VALUES(label_id),
|
||||
exclude = VALUES(exclude)
|
||||
exclude = VALUES(exclude),
|
||||
require_all = VALUES(require_all)
|
||||
`
|
||||
|
||||
selectStmt := `
|
||||
SELECT apple_declaration_uuid as profile_uuid, label_name, label_id, exclude FROM mdm_declaration_labels
|
||||
SELECT apple_declaration_uuid as profile_uuid, label_name, label_id, exclude, require_all FROM mdm_declaration_labels
|
||||
WHERE (apple_declaration_uuid, label_name) IN (%s)
|
||||
`
|
||||
|
||||
|
|
@ -4420,10 +4517,10 @@ func batchSetDeclarationLabelAssociationsDB(ctx context.Context, tx sqlx.ExtCont
|
|||
insertBuilder.WriteString(",")
|
||||
selectOrDeleteBuilder.WriteString(",")
|
||||
}
|
||||
insertBuilder.WriteString("(?, ?, ?, ?)")
|
||||
insertBuilder.WriteString("(?, ?, ?, ?, ?)")
|
||||
selectOrDeleteBuilder.WriteString("(?, ?)")
|
||||
selectParams = append(selectParams, pl.ProfileUUID, pl.LabelName)
|
||||
insertParams = append(insertParams, pl.ProfileUUID, pl.LabelID, pl.LabelName, pl.Exclude)
|
||||
insertParams = append(insertParams, pl.ProfileUUID, pl.LabelID, pl.LabelName, pl.Exclude, pl.RequireAll)
|
||||
deleteParams = append(deleteParams, pl.ProfileUUID, pl.LabelID)
|
||||
|
||||
setProfileUUIDs[pl.ProfileUUID] = struct{}{}
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ func TestMDMApple(t *testing.T) {
|
|||
{"HostMDMCommands", testHostMDMCommands},
|
||||
{"IngestMDMAppleDeviceFromOTAEnrollment", testIngestMDMAppleDeviceFromOTAEnrollment},
|
||||
{"MDMManagedCertificates", testMDMManagedCertificates},
|
||||
{"TestMDMAppleProfileLabels", testMDMAppleProfileLabels},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
|
|
@ -1287,8 +1288,8 @@ func configProfileBytesForTest(name, identifier, uuid string) []byte {
|
|||
`, name, identifier, uuid))
|
||||
}
|
||||
|
||||
// if the label name starts with "exclude-", the label is considered an "exclude-any", otherwise
|
||||
// it is an "include-all".
|
||||
// If the label name starts with "exclude-", the label is considered an "exclude-any". If it starts
|
||||
// with "include-any", it is considered an "include-any". Otherwise it is an "include-all".
|
||||
func configProfileForTest(t *testing.T, name, identifier, uuid string, labels ...*fleet.Label) *fleet.MDMAppleConfigProfile {
|
||||
prof := configProfileBytesForTest(name, identifier, uuid)
|
||||
cp, err := fleet.NewMDMAppleConfigProfile(prof, nil)
|
||||
|
|
@ -1297,9 +1298,12 @@ func configProfileForTest(t *testing.T, name, identifier, uuid string, labels ..
|
|||
cp.Checksum = sum[:]
|
||||
|
||||
for _, lbl := range labels {
|
||||
if strings.HasPrefix(lbl.Name, "exclude-") {
|
||||
switch {
|
||||
case strings.HasPrefix(lbl.Name, "exclude-"):
|
||||
cp.LabelsExcludeAny = append(cp.LabelsExcludeAny, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID})
|
||||
} else {
|
||||
case strings.HasPrefix(lbl.Name, "include-any-"):
|
||||
cp.LabelsIncludeAny = append(cp.LabelsIncludeAny, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID})
|
||||
default:
|
||||
cp.LabelsIncludeAll = append(cp.LabelsIncludeAll, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID})
|
||||
}
|
||||
}
|
||||
|
|
@ -1396,10 +1400,10 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) {
|
|||
`)
|
||||
require.NoError(t, err)
|
||||
|
||||
// if there are no hosts, then no profiles need to be installed
|
||||
profiles, err := ds.ListMDMAppleProfilesToInstall(ctx)
|
||||
// if there are no hosts, then no profilesToInstall need to be installed
|
||||
profilesToInstall, err := ds.ListMDMAppleProfilesToInstall(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, profiles)
|
||||
require.Empty(t, profilesToInstall)
|
||||
|
||||
host1, err := ds.NewHost(ctx, &fleet.Host{
|
||||
Hostname: "test-host1-name",
|
||||
|
|
@ -1437,13 +1441,13 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// global profiles to install on the newly added host
|
||||
profiles, err = ds.ListMDMAppleProfilesToInstall(ctx)
|
||||
profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx)
|
||||
require.NoError(t, err)
|
||||
matchProfiles([]*fleet.MDMAppleProfilePayload{
|
||||
{ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"},
|
||||
{ProfileUUID: globalPfs[1].ProfileUUID, ProfileIdentifier: globalPfs[1].Identifier, ProfileName: globalPfs[1].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"},
|
||||
{ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"},
|
||||
}, profiles)
|
||||
}, profilesToInstall)
|
||||
|
||||
// add another host, it belongs to a team
|
||||
team, err := ds.NewTeam(ctx, &fleet.Team{Name: "test team"})
|
||||
|
|
@ -1459,9 +1463,9 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
nanoEnroll(t, ds, host2, false)
|
||||
|
||||
// still the same profiles to assign as there are no profiles for team 1
|
||||
profiles, err = ds.ListMDMAppleProfilesToInstall(ctx)
|
||||
profiles, err := ds.ListMDMAppleProfilesToInstall(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
matchProfiles([]*fleet.MDMAppleProfilePayload{
|
||||
{ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"},
|
||||
{ProfileUUID: globalPfs[1].ProfileUUID, ProfileIdentifier: globalPfs[1].Identifier, ProfileName: globalPfs[1].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"},
|
||||
|
|
@ -1484,7 +1488,7 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) {
|
|||
require.Len(t, teamPfs, 2)
|
||||
|
||||
// new profiles, this time for the new host belonging to team 1
|
||||
profiles, err = ds.ListMDMAppleProfilesToInstall(ctx)
|
||||
profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx)
|
||||
require.NoError(t, err)
|
||||
matchProfiles([]*fleet.MDMAppleProfilePayload{
|
||||
{ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"},
|
||||
|
|
@ -1492,7 +1496,7 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) {
|
|||
{ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"},
|
||||
{ProfileUUID: teamPfs[0].ProfileUUID, ProfileIdentifier: teamPfs[0].Identifier, ProfileName: teamPfs[0].Name, HostUUID: "test-uuid-2", HostPlatform: "darwin"},
|
||||
{ProfileUUID: teamPfs[1].ProfileUUID, ProfileIdentifier: teamPfs[1].Identifier, ProfileName: teamPfs[1].Name, HostUUID: "test-uuid-2", HostPlatform: "darwin"},
|
||||
}, profiles)
|
||||
}, profilesToInstall)
|
||||
|
||||
// add another global host
|
||||
host3, err := ds.NewHost(ctx, &fleet.Host{
|
||||
|
|
@ -1507,7 +1511,7 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) {
|
|||
nanoEnroll(t, ds, host3, false)
|
||||
|
||||
// more profiles, this time for both global hosts and the team
|
||||
profiles, err = ds.ListMDMAppleProfilesToInstall(ctx)
|
||||
profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx)
|
||||
require.NoError(t, err)
|
||||
matchProfiles([]*fleet.MDMAppleProfilePayload{
|
||||
{ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"},
|
||||
|
|
@ -1518,7 +1522,7 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) {
|
|||
{ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-3", HostPlatform: "darwin"},
|
||||
{ProfileUUID: globalPfs[1].ProfileUUID, ProfileIdentifier: globalPfs[1].Identifier, ProfileName: globalPfs[1].Name, HostUUID: "test-uuid-3", HostPlatform: "darwin"},
|
||||
{ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-3", HostPlatform: "darwin"},
|
||||
}, profiles)
|
||||
}, profilesToInstall)
|
||||
|
||||
// cron runs and updates the status
|
||||
err = ds.BulkUpsertMDMAppleHostProfiles(
|
||||
|
|
@ -1608,9 +1612,9 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// no profiles left to install
|
||||
profiles, err = ds.ListMDMAppleProfilesToInstall(ctx)
|
||||
profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, profiles)
|
||||
require.Empty(t, profilesToInstall)
|
||||
|
||||
// no profiles to remove yet
|
||||
toRemove, err := ds.ListMDMAppleProfilesToRemove(ctx)
|
||||
|
|
@ -1627,9 +1631,9 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, apple_mdm.VerifyHostMDMProfiles(ctx, ds, host3, profilesByIdentifier(verified)))
|
||||
|
||||
// still no profiles to install
|
||||
profiles, err = ds.ListMDMAppleProfilesToInstall(ctx)
|
||||
profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, profiles)
|
||||
require.Empty(t, profilesToInstall)
|
||||
|
||||
// still no profiles to remove
|
||||
toRemove, err = ds.ListMDMAppleProfilesToRemove(ctx)
|
||||
|
|
@ -1641,12 +1645,12 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// profiles to be added for host1 are now related to the team
|
||||
profiles, err = ds.ListMDMAppleProfilesToInstall(ctx)
|
||||
profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx)
|
||||
require.NoError(t, err)
|
||||
matchProfiles([]*fleet.MDMAppleProfilePayload{
|
||||
{ProfileUUID: teamPfs[0].ProfileUUID, ProfileIdentifier: teamPfs[0].Identifier, ProfileName: teamPfs[0].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"},
|
||||
{ProfileUUID: teamPfs[1].ProfileUUID, ProfileIdentifier: teamPfs[1].Identifier, ProfileName: teamPfs[1].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"},
|
||||
}, profiles)
|
||||
}, profilesToInstall)
|
||||
|
||||
// profiles to be removed includes host1's old profiles
|
||||
toRemove, err = ds.ListMDMAppleProfilesToRemove(ctx)
|
||||
|
|
@ -7228,3 +7232,221 @@ func testMDMManagedCertificates(t *testing.T, ds *Datastore) {
|
|||
})
|
||||
require.ErrorIs(t, err, sql.ErrNoRows)
|
||||
}
|
||||
|
||||
func testMDMAppleProfileLabels(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
|
||||
matchProfiles := func(want, got []*fleet.MDMAppleProfilePayload) {
|
||||
// match only the fields we care about
|
||||
for _, p := range got {
|
||||
p.Checksum = nil
|
||||
}
|
||||
require.ElementsMatch(t, want, got)
|
||||
}
|
||||
|
||||
globProf1, err := ds.NewMDMAppleConfigProfile(ctx, *configProfileForTest(t, "N1", "I1", "z"))
|
||||
require.NoError(t, err)
|
||||
globProf2, err := ds.NewMDMAppleConfigProfile(ctx, *configProfileForTest(t, "N2", "I2", "x"))
|
||||
require.NoError(t, err)
|
||||
|
||||
globalPfs, err := ds.ListMDMAppleConfigProfiles(ctx, ptr.Uint(0))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, globalPfs, 2)
|
||||
|
||||
// if there are no hosts, then no profilesToInstall need to be installed
|
||||
profilesToInstall, err := ds.ListMDMAppleProfilesToInstall(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, profilesToInstall)
|
||||
|
||||
host1, err := ds.NewHost(ctx, &fleet.Host{
|
||||
Hostname: "test-host1-name",
|
||||
OsqueryHostID: ptr.String("1337"),
|
||||
NodeKey: ptr.String("1337"),
|
||||
UUID: "test-uuid-1",
|
||||
TeamID: nil,
|
||||
Platform: "darwin",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// add a user enrollment for this device, nothing else should be modified
|
||||
nanoEnroll(t, ds, host1, true)
|
||||
|
||||
// non-macOS hosts shouldn't modify any of the results below
|
||||
_, err = ds.NewHost(ctx, &fleet.Host{
|
||||
Hostname: "test-windows-host",
|
||||
OsqueryHostID: ptr.String("4824"),
|
||||
NodeKey: ptr.String("4824"),
|
||||
UUID: "test-windows-host",
|
||||
TeamID: nil,
|
||||
Platform: "windows",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// a macOS host that's not MDM enrolled into Fleet shouldn't
|
||||
// modify any of the results below
|
||||
_, err = ds.NewHost(ctx, &fleet.Host{
|
||||
Hostname: "test-non-mdm-host",
|
||||
OsqueryHostID: ptr.String("4825"),
|
||||
NodeKey: ptr.String("4825"),
|
||||
UUID: "test-non-mdm-host",
|
||||
TeamID: nil,
|
||||
Platform: "darwin",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// global profiles to install on the newly added host
|
||||
profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx)
|
||||
require.NoError(t, err)
|
||||
matchProfiles([]*fleet.MDMAppleProfilePayload{
|
||||
{ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"},
|
||||
{ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"},
|
||||
}, profilesToInstall)
|
||||
|
||||
hostLabel, err := ds.NewHost(ctx, &fleet.Host{
|
||||
Hostname: "test-host-name-label",
|
||||
OsqueryHostID: ptr.String("1337_label"),
|
||||
NodeKey: ptr.String("1337_label"),
|
||||
UUID: "test-uuid-1-label",
|
||||
TeamID: nil,
|
||||
Platform: "darwin",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// add a user enrollment for this device, nothing else should be modified
|
||||
nanoEnroll(t, ds, hostLabel, true)
|
||||
|
||||
// include-any labels
|
||||
l1, err := ds.NewLabel(ctx, &fleet.Label{Name: "include-any-1", Query: "select 1"})
|
||||
require.NoError(t, err)
|
||||
|
||||
l2, err := ds.NewLabel(ctx, &fleet.Label{Name: "include-any-2", Query: "select 1"})
|
||||
require.NoError(t, err)
|
||||
|
||||
l3, err := ds.NewLabel(ctx, &fleet.Label{Name: "include-any-3", Query: "select 1"})
|
||||
require.NoError(t, err)
|
||||
|
||||
// include-all labels
|
||||
l4, err := ds.NewLabel(ctx, &fleet.Label{Name: "include-all-4", Query: "select 1"})
|
||||
require.NoError(t, err)
|
||||
|
||||
l5, err := ds.NewLabel(ctx, &fleet.Label{Name: "include-all-5", Query: "select 1"})
|
||||
require.NoError(t, err)
|
||||
|
||||
// exclude-any labels
|
||||
l6, err := ds.NewLabel(ctx, &fleet.Label{Name: "exclude-any-6", Query: "select 1"})
|
||||
require.NoError(t, err)
|
||||
|
||||
l7, err := ds.NewLabel(ctx, &fleet.Label{Name: "exclude-any-7", Query: "select 1"})
|
||||
require.NoError(t, err)
|
||||
|
||||
profIncludeAny, err := ds.NewMDMAppleConfigProfile(ctx, *configProfileForTest(t, "prof-include-any", "prof-include-any", "prof-include-any", l1, l2, l3))
|
||||
require.NoError(t, err)
|
||||
profIncludeAll, err := ds.NewMDMAppleConfigProfile(ctx, *configProfileForTest(t, "prof-include-all", "prof-include-all", "prof-include-all", l4, l5))
|
||||
require.NoError(t, err)
|
||||
profExcludeAny, err := ds.NewMDMAppleConfigProfile(ctx, *configProfileForTest(t, "prof-exclude-any", "prof-exclude-any", "prof-exclude-any", l6, l7))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Update hosts' labels updated at timestamp so that the exclude any profile shows up
|
||||
hostLabel.LabelUpdatedAt = time.Now()
|
||||
err = ds.UpdateHost(ctx, hostLabel)
|
||||
require.NoError(t, err)
|
||||
|
||||
host1.LabelUpdatedAt = time.Now()
|
||||
err = ds.UpdateHost(ctx, host1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// hostLabel is a member of l1, l4, l5
|
||||
err = ds.AsyncBatchInsertLabelMembership(ctx, [][2]uint{{l1.ID, hostLabel.ID}, {l4.ID, hostLabel.ID}, {l5.ID, hostLabel.ID}})
|
||||
require.NoError(t, err)
|
||||
|
||||
globalPfs, err = ds.ListMDMAppleConfigProfiles(ctx, ptr.Uint(0))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, globalPfs, 5)
|
||||
|
||||
// still the same profiles to assign (plus the one for hostLabel) as there are no profiles for team 1
|
||||
profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx)
|
||||
|
||||
require.NoError(t, err)
|
||||
matchProfiles([]*fleet.MDMAppleProfilePayload{
|
||||
{ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: host1.UUID, HostPlatform: "darwin"},
|
||||
{ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: host1.UUID, HostPlatform: "darwin"},
|
||||
{ProfileUUID: profExcludeAny.ProfileUUID, ProfileIdentifier: profExcludeAny.Identifier, ProfileName: profExcludeAny.Name, HostUUID: host1.UUID, HostPlatform: "darwin"},
|
||||
|
||||
{ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"},
|
||||
{ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"},
|
||||
{ProfileUUID: profIncludeAny.ProfileUUID, ProfileIdentifier: profIncludeAny.Identifier, ProfileName: profIncludeAny.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"},
|
||||
{ProfileUUID: profIncludeAll.ProfileUUID, ProfileIdentifier: profIncludeAll.Identifier, ProfileName: profIncludeAll.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"},
|
||||
{ProfileUUID: profExcludeAny.ProfileUUID, ProfileIdentifier: profExcludeAny.Identifier, ProfileName: profExcludeAny.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"},
|
||||
}, profilesToInstall)
|
||||
|
||||
// Remove the l1<->hostLabel relationship, but add l2<->hostLabel. The profile should still show
|
||||
// up since it's "include any"
|
||||
err = ds.AsyncBatchDeleteLabelMembership(ctx, [][2]uint{{l1.ID, hostLabel.ID}})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.AsyncBatchInsertLabelMembership(ctx, [][2]uint{{l2.ID, hostLabel.ID}})
|
||||
require.NoError(t, err)
|
||||
|
||||
profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx)
|
||||
|
||||
require.NoError(t, err)
|
||||
matchProfiles([]*fleet.MDMAppleProfilePayload{
|
||||
{ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: host1.UUID, HostPlatform: "darwin"},
|
||||
{ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: host1.UUID, HostPlatform: "darwin"},
|
||||
{ProfileUUID: profExcludeAny.ProfileUUID, ProfileIdentifier: profExcludeAny.Identifier, ProfileName: profExcludeAny.Name, HostUUID: host1.UUID, HostPlatform: "darwin"},
|
||||
|
||||
{ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"},
|
||||
{ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"},
|
||||
{ProfileUUID: profIncludeAny.ProfileUUID, ProfileIdentifier: profIncludeAny.Identifier, ProfileName: profIncludeAny.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"},
|
||||
{ProfileUUID: profIncludeAll.ProfileUUID, ProfileIdentifier: profIncludeAll.Identifier, ProfileName: profIncludeAll.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"},
|
||||
{ProfileUUID: profExcludeAny.ProfileUUID, ProfileIdentifier: profExcludeAny.Identifier, ProfileName: profExcludeAny.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"},
|
||||
}, profilesToInstall)
|
||||
|
||||
// Remove the l2<->hostLabel relationship. The profie should no longer show up since it's
|
||||
// include-any
|
||||
err = ds.AsyncBatchDeleteLabelMembership(ctx, [][2]uint{{l2.ID, hostLabel.ID}})
|
||||
require.NoError(t, err)
|
||||
profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
matchProfiles([]*fleet.MDMAppleProfilePayload{
|
||||
{ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: host1.UUID, HostPlatform: "darwin"},
|
||||
{ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: host1.UUID, HostPlatform: "darwin"},
|
||||
{ProfileUUID: profExcludeAny.ProfileUUID, ProfileIdentifier: profExcludeAny.Identifier, ProfileName: profExcludeAny.Name, HostUUID: host1.UUID, HostPlatform: "darwin"},
|
||||
|
||||
{ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"},
|
||||
{ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"},
|
||||
{ProfileUUID: profIncludeAll.ProfileUUID, ProfileIdentifier: profIncludeAll.Identifier, ProfileName: profIncludeAll.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"},
|
||||
{ProfileUUID: profExcludeAny.ProfileUUID, ProfileIdentifier: profExcludeAny.Identifier, ProfileName: profExcludeAny.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"},
|
||||
}, profilesToInstall)
|
||||
|
||||
// Remove the l4<->hostLabel relationship. Since the profile is "include-all", it should no longer show
|
||||
// up even though the l5<->hostLabel connection is still there.
|
||||
err = ds.AsyncBatchDeleteLabelMembership(ctx, [][2]uint{{l4.ID, hostLabel.ID}})
|
||||
require.NoError(t, err)
|
||||
profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
matchProfiles([]*fleet.MDMAppleProfilePayload{
|
||||
{ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: host1.UUID, HostPlatform: "darwin"},
|
||||
{ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: host1.UUID, HostPlatform: "darwin"},
|
||||
{ProfileUUID: profExcludeAny.ProfileUUID, ProfileIdentifier: profExcludeAny.Identifier, ProfileName: profExcludeAny.Name, HostUUID: host1.UUID, HostPlatform: "darwin"},
|
||||
|
||||
{ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"},
|
||||
{ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"},
|
||||
{ProfileUUID: profExcludeAny.ProfileUUID, ProfileIdentifier: profExcludeAny.Identifier, ProfileName: profExcludeAny.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"},
|
||||
}, profilesToInstall)
|
||||
|
||||
// Add a l6<->host relationship. The exclude-any profile should no longer be assigned to hostLabel.
|
||||
err = ds.AsyncBatchInsertLabelMembership(ctx, [][2]uint{{l6.ID, hostLabel.ID}})
|
||||
require.NoError(t, err)
|
||||
profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
matchProfiles([]*fleet.MDMAppleProfilePayload{
|
||||
{ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: host1.UUID, HostPlatform: "darwin"},
|
||||
{ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: host1.UUID, HostPlatform: "darwin"},
|
||||
{ProfileUUID: profExcludeAny.ProfileUUID, ProfileIdentifier: profExcludeAny.Identifier, ProfileName: profExcludeAny.Name, HostUUID: host1.UUID, HostPlatform: "darwin"},
|
||||
|
||||
{ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"},
|
||||
{ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin"},
|
||||
}, profilesToInstall)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,7 +124,8 @@ func (ds *Datastore) getMDMCommand(ctx context.Context, q sqlx.QueryerContext, c
|
|||
|
||||
func (ds *Datastore) BatchSetMDMProfiles(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile,
|
||||
winProfiles []*fleet.MDMWindowsConfigProfile, macDeclarations []*fleet.MDMAppleDeclaration) (updates fleet.MDMProfilesUpdates,
|
||||
err error) {
|
||||
err error,
|
||||
) {
|
||||
err = ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
||||
var err error
|
||||
if updates.WindowsConfigProfile, err = ds.batchSetMDMWindowsProfilesDB(ctx, tx, tmID, winProfiles); err != nil {
|
||||
|
|
@ -273,9 +274,19 @@ FROM (
|
|||
}
|
||||
for _, label := range labels {
|
||||
if prof, ok := profMap[label.ProfileUUID]; ok {
|
||||
if label.Exclude {
|
||||
switch {
|
||||
case label.Exclude && label.RequireAll:
|
||||
// this should never happen so log it for debugging
|
||||
level.Debug(ds.logger).Log("msg", "unsupported profile label: cannot be both exclude and require all",
|
||||
"profile_uuid", label.ProfileUUID,
|
||||
"label_name", label.LabelName,
|
||||
)
|
||||
case label.Exclude && !label.RequireAll:
|
||||
prof.LabelsExcludeAny = append(prof.LabelsExcludeAny, label)
|
||||
} else {
|
||||
case !label.Exclude && !label.RequireAll:
|
||||
prof.LabelsIncludeAny = append(prof.LabelsIncludeAny, label)
|
||||
default:
|
||||
// default include all
|
||||
prof.LabelsIncludeAll = append(prof.LabelsIncludeAll, label)
|
||||
}
|
||||
}
|
||||
|
|
@ -292,7 +303,8 @@ SELECT
|
|||
label_name,
|
||||
COALESCE(label_id, 0) as label_id,
|
||||
IF(label_id IS NULL, 1, 0) as broken,
|
||||
exclude
|
||||
exclude,
|
||||
require_all
|
||||
FROM
|
||||
mdm_configuration_profile_labels mcpl
|
||||
WHERE
|
||||
|
|
@ -304,7 +316,8 @@ SELECT
|
|||
label_name,
|
||||
COALESCE(label_id, 0) as label_id,
|
||||
IF(label_id IS NULL, 1, 0) as broken,
|
||||
exclude
|
||||
exclude,
|
||||
require_all
|
||||
FROM
|
||||
mdm_declaration_labels mdl
|
||||
WHERE
|
||||
|
|
@ -996,6 +1009,8 @@ func batchSetProfileLabelAssociationsDB(
|
|||
platform string,
|
||||
) (updatedDB bool, err error) {
|
||||
if len(profileLabels) == 0 {
|
||||
// FIXME: At what point are we deleting all labels for a profile (e.g., the user might
|
||||
// remove all labels from an existing profile)?
|
||||
return false, nil
|
||||
}
|
||||
|
||||
|
|
@ -1025,16 +1040,17 @@ func batchSetProfileLabelAssociationsDB(
|
|||
|
||||
upsertStmt := `
|
||||
INSERT INTO mdm_configuration_profile_labels
|
||||
(%s_profile_uuid, label_id, label_name, exclude)
|
||||
(%s_profile_uuid, label_id, label_name, exclude, require_all)
|
||||
VALUES
|
||||
%s
|
||||
ON DUPLICATE KEY UPDATE
|
||||
label_id = VALUES(label_id),
|
||||
exclude = VALUES(exclude)
|
||||
exclude = VALUES(exclude),
|
||||
require_all = VALUES(require_all)
|
||||
`
|
||||
|
||||
selectStmt := `
|
||||
SELECT %s_profile_uuid as profile_uuid, label_id, label_name, exclude FROM mdm_configuration_profile_labels
|
||||
SELECT %s_profile_uuid as profile_uuid, label_id, label_name, exclude, require_all FROM mdm_configuration_profile_labels
|
||||
WHERE (%s_profile_uuid, label_name) IN (%s)
|
||||
`
|
||||
|
||||
|
|
@ -1054,10 +1070,10 @@ func batchSetProfileLabelAssociationsDB(
|
|||
insertBuilder.WriteString(",")
|
||||
selectOrDeleteBuilder.WriteString(",")
|
||||
}
|
||||
insertBuilder.WriteString("(?, ?, ?, ?)")
|
||||
insertBuilder.WriteString("(?, ?, ?, ?, ?)")
|
||||
selectOrDeleteBuilder.WriteString("(?, ?)")
|
||||
selectParams = append(selectParams, pl.ProfileUUID, pl.LabelName)
|
||||
insertParams = append(insertParams, pl.ProfileUUID, pl.LabelID, pl.LabelName, pl.Exclude)
|
||||
insertParams = append(insertParams, pl.ProfileUUID, pl.LabelID, pl.LabelName, pl.Exclude, pl.RequireAll)
|
||||
deleteParams = append(deleteParams, pl.ProfileUUID, pl.LabelID)
|
||||
|
||||
setProfileUUIDs[pl.ProfileUUID] = struct{}{}
|
||||
|
|
|
|||
|
|
@ -699,20 +699,20 @@ func testListMDMConfigProfiles(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, ds.DeleteLabel(ctx, labels[4].Name))
|
||||
profLabels := map[string][]fleet.ConfigurationProfileLabel{
|
||||
"C": {
|
||||
{LabelName: labels[0].Name, LabelID: labels[0].ID},
|
||||
{LabelName: labels[1].Name, LabelID: labels[1].ID},
|
||||
{LabelName: labels[0].Name, LabelID: labels[0].ID, RequireAll: true},
|
||||
{LabelName: labels[1].Name, LabelID: labels[1].ID, RequireAll: true},
|
||||
},
|
||||
"D": {
|
||||
{LabelName: labels[2].Name, LabelID: labels[2].ID},
|
||||
{LabelName: labels[3].Name, LabelID: 0, Broken: true},
|
||||
{LabelName: labels[2].Name, LabelID: labels[2].ID, RequireAll: true},
|
||||
{LabelName: labels[3].Name, LabelID: 0, Broken: true, RequireAll: true},
|
||||
},
|
||||
"E": {
|
||||
{LabelName: labels[4].Name, LabelID: 0, Broken: true},
|
||||
{LabelName: labels[5].Name, LabelID: labels[5].ID},
|
||||
{LabelName: labels[4].Name, LabelID: 0, Broken: true, RequireAll: true},
|
||||
{LabelName: labels[5].Name, LabelID: labels[5].ID, RequireAll: true},
|
||||
},
|
||||
"F": {
|
||||
{LabelName: labels[6].Name, LabelID: labels[6].ID},
|
||||
{LabelName: labels[7].Name, LabelID: labels[7].ID},
|
||||
{LabelName: labels[6].Name, LabelID: labels[6].ID, RequireAll: true},
|
||||
{LabelName: labels[7].Name, LabelID: labels[7].ID, RequireAll: true},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -745,9 +745,19 @@ WHERE
|
|||
return nil, err
|
||||
}
|
||||
for _, lbl := range labels {
|
||||
if lbl.Exclude {
|
||||
switch {
|
||||
case lbl.Exclude && lbl.RequireAll:
|
||||
// this should never happen so log it for debugging
|
||||
level.Debug(ds.logger).Log("msg", "unsupported profile label: cannot be both exclude and require all",
|
||||
"profile_uuid", lbl.ProfileUUID,
|
||||
"label_name", lbl.LabelName,
|
||||
)
|
||||
case lbl.Exclude && !lbl.RequireAll:
|
||||
res.LabelsExcludeAny = append(res.LabelsExcludeAny, lbl)
|
||||
} else {
|
||||
case !lbl.Exclude && !lbl.RequireAll:
|
||||
res.LabelsIncludeAny = append(res.LabelsIncludeAny, lbl)
|
||||
default:
|
||||
// default include all
|
||||
res.LabelsIncludeAll = append(res.LabelsIncludeAll, lbl)
|
||||
}
|
||||
}
|
||||
|
|
@ -1168,7 +1178,7 @@ const windowsMDMProfilesDesiredStateQuery = `
|
|||
JOIN mdm_windows_enrollments mwe
|
||||
ON mwe.host_uuid = h.uuid
|
||||
JOIN mdm_configuration_profile_labels mcpl
|
||||
ON mcpl.windows_profile_uuid = mwcp.profile_uuid AND mcpl.exclude = 0
|
||||
ON mcpl.windows_profile_uuid = mwcp.profile_uuid AND mcpl.exclude = 0 AND mcpl.require_all = 1
|
||||
LEFT OUTER JOIN label_membership lm
|
||||
ON lm.label_id = mcpl.label_id AND lm.host_id = h.id
|
||||
WHERE
|
||||
|
|
@ -1203,7 +1213,7 @@ const windowsMDMProfilesDesiredStateQuery = `
|
|||
JOIN mdm_windows_enrollments mwe
|
||||
ON mwe.host_uuid = h.uuid
|
||||
JOIN mdm_configuration_profile_labels mcpl
|
||||
ON mcpl.windows_profile_uuid = mwcp.profile_uuid AND mcpl.exclude = 1
|
||||
ON mcpl.windows_profile_uuid = mwcp.profile_uuid AND mcpl.exclude = 1 AND mcpl.require_all = 0
|
||||
LEFT OUTER JOIN labels lbl
|
||||
ON lbl.id = mcpl.label_id
|
||||
LEFT OUTER JOIN label_membership lm
|
||||
|
|
@ -1216,6 +1226,37 @@ const windowsMDMProfilesDesiredStateQuery = `
|
|||
HAVING
|
||||
-- considers only the profiles with labels, without any broken label, with results reported after all labels were created and with the host not in any label
|
||||
count_profile_labels > 0 AND count_profile_labels = count_non_broken_labels AND count_profile_labels = count_host_updated_after_labels AND count_host_labels = 0
|
||||
|
||||
UNION
|
||||
|
||||
-- label-based profiles where the host is a member of any of the labels (include-any).
|
||||
-- by design, "include" labels cannot match if they are broken (the host cannot be
|
||||
-- a member of a deleted label).
|
||||
SELECT
|
||||
mwcp.profile_uuid,
|
||||
mwcp.name,
|
||||
h.uuid as host_uuid,
|
||||
COUNT(*) as count_profile_labels,
|
||||
COUNT(mcpl.label_id) as count_non_broken_labels,
|
||||
COUNT(lm.label_id) as count_host_labels,
|
||||
0 as count_host_updated_after_labels
|
||||
FROM
|
||||
mdm_windows_configuration_profiles mwcp
|
||||
JOIN hosts h
|
||||
ON h.team_id = mwcp.team_id OR (h.team_id IS NULL AND mwcp.team_id = 0)
|
||||
JOIN mdm_windows_enrollments mwe
|
||||
ON mwe.host_uuid = h.uuid
|
||||
JOIN mdm_configuration_profile_labels mcpl
|
||||
ON mcpl.windows_profile_uuid = mwcp.profile_uuid AND mcpl.exclude = 0 AND mcpl.require_all = 0
|
||||
LEFT OUTER JOIN label_membership lm
|
||||
ON lm.label_id = mcpl.label_id AND lm.host_id = h.id
|
||||
WHERE
|
||||
h.platform = 'windows' AND
|
||||
( %s )
|
||||
GROUP BY
|
||||
mwcp.profile_uuid, mwcp.name, h.uuid
|
||||
HAVING
|
||||
count_profile_labels > 0 AND count_host_labels >= 1
|
||||
`
|
||||
|
||||
func (ds *Datastore) ListMDMWindowsProfilesToInstall(ctx context.Context) ([]*fleet.MDMWindowsProfilePayload, error) {
|
||||
|
|
@ -1282,9 +1323,9 @@ func listMDMWindowsProfilesToInstallDB(
|
|||
|
||||
var err error
|
||||
args := []any{fleet.MDMOperationTypeInstall}
|
||||
query = fmt.Sprintf(query, hostFilter, hostFilter, hostFilter)
|
||||
query = fmt.Sprintf(query, hostFilter, hostFilter, hostFilter, hostFilter)
|
||||
if len(hostUUIDs) > 0 {
|
||||
query, args, err = sqlx.In(query, hostUUIDs, hostUUIDs, hostUUIDs, fleet.MDMOperationTypeInstall)
|
||||
query, args, err = sqlx.In(query, hostUUIDs, hostUUIDs, hostUUIDs, hostUUIDs, fleet.MDMOperationTypeInstall)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "building sqlx.In")
|
||||
}
|
||||
|
|
@ -1362,7 +1403,7 @@ func listMDMWindowsProfilesToRemoveDB(
|
|||
mcpl.label_id IS NULL
|
||||
) AND
|
||||
(%s)
|
||||
`, fmt.Sprintf(windowsMDMProfilesDesiredStateQuery, "TRUE", "TRUE", "TRUE"), hostFilter)
|
||||
`, fmt.Sprintf(windowsMDMProfilesDesiredStateQuery, "TRUE", "TRUE", "TRUE", "TRUE"), hostFilter)
|
||||
|
||||
var err error
|
||||
var args []any
|
||||
|
|
@ -1581,13 +1622,22 @@ INSERT INTO
|
|||
}
|
||||
}
|
||||
|
||||
labels := make([]fleet.ConfigurationProfileLabel, 0, len(cp.LabelsIncludeAll)+len(cp.LabelsExcludeAny))
|
||||
labels := make([]fleet.ConfigurationProfileLabel, 0, len(cp.LabelsIncludeAll)+len(cp.LabelsIncludeAny)+len(cp.LabelsExcludeAny))
|
||||
for i := range cp.LabelsIncludeAll {
|
||||
cp.LabelsIncludeAll[i].ProfileUUID = profileUUID
|
||||
cp.LabelsIncludeAll[i].RequireAll = true
|
||||
cp.LabelsIncludeAll[i].Exclude = false
|
||||
labels = append(labels, cp.LabelsIncludeAll[i])
|
||||
}
|
||||
for i := range cp.LabelsIncludeAny {
|
||||
cp.LabelsIncludeAny[i].ProfileUUID = profileUUID
|
||||
cp.LabelsIncludeAny[i].RequireAll = false
|
||||
cp.LabelsIncludeAny[i].Exclude = false
|
||||
labels = append(labels, cp.LabelsIncludeAny[i])
|
||||
}
|
||||
for i := range cp.LabelsExcludeAny {
|
||||
cp.LabelsExcludeAny[i].ProfileUUID = profileUUID
|
||||
cp.LabelsExcludeAny[i].RequireAll = false
|
||||
cp.LabelsExcludeAny[i].Exclude = true
|
||||
labels = append(labels, cp.LabelsExcludeAny[i])
|
||||
}
|
||||
|
|
@ -1832,11 +1882,20 @@ ON DUPLICATE KEY UPDATE
|
|||
|
||||
for _, label := range incomingProf.LabelsIncludeAll {
|
||||
label.ProfileUUID = newlyInsertedProf.ProfileUUID
|
||||
label.Exclude = false
|
||||
label.RequireAll = true
|
||||
incomingLabels = append(incomingLabels, label)
|
||||
}
|
||||
for _, label := range incomingProf.LabelsIncludeAny {
|
||||
label.ProfileUUID = newlyInsertedProf.ProfileUUID
|
||||
label.Exclude = false
|
||||
label.RequireAll = false
|
||||
incomingLabels = append(incomingLabels, label)
|
||||
}
|
||||
for _, label := range incomingProf.LabelsExcludeAny {
|
||||
label.ProfileUUID = newlyInsertedProf.ProfileUUID
|
||||
label.Exclude = true
|
||||
label.RequireAll = false
|
||||
incomingLabels = append(incomingLabels, label)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ func TestMDMWindows(t *testing.T) {
|
|||
{"TestMDMWindowsDiskEncryption", testMDMWindowsDiskEncryption},
|
||||
{"TestMDMWindowsProfilesSummary", testMDMWindowsProfilesSummary},
|
||||
{"TestBatchSetMDMWindowsProfiles", testBatchSetMDMWindowsProfiles},
|
||||
{"TestMDMWindowsProfileLabels", testMDMWindowsProfileLabels},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
|
|
@ -2010,6 +2011,164 @@ func testSetOrReplaceMDMWindowsConfigProfile(t *testing.T, ds *Datastore) {
|
|||
expectWindowsProfiles(t, ds, nil, []*fleet.MDMWindowsConfigProfile{&cp2, &cp6})
|
||||
}
|
||||
|
||||
func testMDMWindowsProfileLabels(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
// Create a windows host
|
||||
u := uuid.New().String()
|
||||
host, err := ds.NewHost(ctx, &fleet.Host{
|
||||
DetailUpdatedAt: time.Now(),
|
||||
LabelUpdatedAt: time.Now(),
|
||||
PolicyUpdatedAt: time.Now(),
|
||||
SeenTime: time.Now(),
|
||||
NodeKey: &u,
|
||||
UUID: u,
|
||||
Hostname: u,
|
||||
Platform: "windows",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
windowsEnroll(t, ds, host)
|
||||
|
||||
// "include-any" labels
|
||||
l1, err := ds.NewLabel(ctx, &fleet.Label{
|
||||
Name: "include-any-label1",
|
||||
Description: "desc",
|
||||
Query: "select 1;",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
l2, err := ds.NewLabel(ctx, &fleet.Label{
|
||||
Name: "include-any-label2",
|
||||
Description: "desc",
|
||||
Query: "select 1;",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
l3, err := ds.NewLabel(ctx, &fleet.Label{
|
||||
Name: "include-any-label3",
|
||||
Description: "desc",
|
||||
Query: "select 1;",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// include-all labels
|
||||
l4, err := ds.NewLabel(ctx, &fleet.Label{
|
||||
Name: "include-all-label4",
|
||||
Description: "desc",
|
||||
Query: "select 1;",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
l5, err := ds.NewLabel(ctx, &fleet.Label{
|
||||
Name: "include-all-label5",
|
||||
Description: "desc",
|
||||
Query: "select 1;",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// exclude-all labels
|
||||
l6, err := ds.NewLabel(ctx, &fleet.Label{
|
||||
Name: "exclude-any-label6",
|
||||
Description: "desc",
|
||||
Query: "select 1;",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
l7, err := ds.NewLabel(ctx, &fleet.Label{
|
||||
Name: "exclude-any-label7",
|
||||
Description: "desc",
|
||||
Query: "select 1;",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a profile with "include-any" with l1
|
||||
includeAnyProf, err := ds.NewMDMWindowsConfigProfile(
|
||||
ctx,
|
||||
*windowsConfigProfileForTest(t, "prof-include-any", "./Foo/Bar", l1, l2, l3),
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, includeAnyProf.ProfileUUID)
|
||||
|
||||
// Create a profile with "include-all" with l4 and l5
|
||||
includeAllProf, err := ds.NewMDMWindowsConfigProfile(
|
||||
ctx,
|
||||
*windowsConfigProfileForTest(t, "prof-include-all", "./Foo/Bar", l4, l5),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, includeAllProf.ProfileUUID)
|
||||
|
||||
// Create a profile with "exclude-all" with l6 and l7
|
||||
excludeAllProf, err := ds.NewMDMWindowsConfigProfile(
|
||||
ctx,
|
||||
*windowsConfigProfileForTest(t, "prof-exclude-any", "./Foo/Bar", l6, l7),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Connect the host and l1, l4, l5
|
||||
err = ds.AsyncBatchInsertLabelMembership(ctx, [][2]uint{{l1.ID, host.ID}, {l4.ID, host.ID}, {l5.ID, host.ID}})
|
||||
require.NoError(t, err)
|
||||
|
||||
host.LabelUpdatedAt = time.Now()
|
||||
err = ds.UpdateHost(ctx, host)
|
||||
require.NoError(t, err)
|
||||
|
||||
// We should see all 3 profiles in the "to install" list
|
||||
profilesToInstall, err := ds.ListMDMWindowsProfilesToInstall(ctx)
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, []*fleet.MDMWindowsProfilePayload{
|
||||
{ProfileUUID: includeAllProf.ProfileUUID, ProfileName: includeAllProf.Name, HostUUID: host.UUID},
|
||||
{ProfileUUID: includeAnyProf.ProfileUUID, ProfileName: includeAnyProf.Name, HostUUID: host.UUID},
|
||||
{ProfileUUID: excludeAllProf.ProfileUUID, ProfileName: excludeAllProf.Name, HostUUID: host.UUID},
|
||||
}, profilesToInstall)
|
||||
|
||||
// Remove the l1<->host relationship, but add l2<->labelHost. The profile should still show
|
||||
// up since it's "include any"
|
||||
err = ds.AsyncBatchDeleteLabelMembership(ctx, [][2]uint{{l1.ID, host.ID}})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.AsyncBatchInsertLabelMembership(ctx, [][2]uint{{l2.ID, host.ID}})
|
||||
require.NoError(t, err)
|
||||
|
||||
profilesToInstall, err = ds.ListMDMWindowsProfilesToInstall(ctx)
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, []*fleet.MDMWindowsProfilePayload{
|
||||
{ProfileUUID: includeAllProf.ProfileUUID, ProfileName: includeAllProf.Name, HostUUID: host.UUID},
|
||||
{ProfileUUID: includeAnyProf.ProfileUUID, ProfileName: includeAnyProf.Name, HostUUID: host.UUID},
|
||||
{ProfileUUID: excludeAllProf.ProfileUUID, ProfileName: excludeAllProf.Name, HostUUID: host.UUID},
|
||||
}, profilesToInstall)
|
||||
|
||||
// Remove the l2<->host relationship. Since the profile is "include-any", it should no longer
|
||||
// show up
|
||||
err = ds.AsyncBatchDeleteLabelMembership(ctx, [][2]uint{{l2.ID, host.ID}})
|
||||
require.NoError(t, err)
|
||||
|
||||
profilesToInstall, err = ds.ListMDMWindowsProfilesToInstall(ctx)
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, []*fleet.MDMWindowsProfilePayload{
|
||||
{ProfileUUID: includeAllProf.ProfileUUID, ProfileName: includeAllProf.Name, HostUUID: host.UUID},
|
||||
{ProfileUUID: excludeAllProf.ProfileUUID, ProfileName: excludeAllProf.Name, HostUUID: host.UUID},
|
||||
}, profilesToInstall)
|
||||
|
||||
// Remove the l4<->host relationship. Since the profile is "include-all", it should no longer show
|
||||
// up even though the l5<->host connection is still there.
|
||||
err = ds.AsyncBatchDeleteLabelMembership(ctx, [][2]uint{{l4.ID, host.ID}})
|
||||
require.NoError(t, err)
|
||||
|
||||
profilesToInstall, err = ds.ListMDMWindowsProfilesToInstall(ctx)
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, []*fleet.MDMWindowsProfilePayload{
|
||||
{ProfileUUID: excludeAllProf.ProfileUUID, ProfileName: excludeAllProf.Name, HostUUID: host.UUID},
|
||||
}, profilesToInstall)
|
||||
|
||||
// Add a l6<->host relationship. The exclude-any profile should be gone now.
|
||||
err = ds.AsyncBatchInsertLabelMembership(ctx, [][2]uint{{l6.ID, host.ID}})
|
||||
require.NoError(t, err)
|
||||
|
||||
profilesToInstall, err = ds.ListMDMWindowsProfilesToInstall(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, profilesToInstall)
|
||||
}
|
||||
|
||||
func expectWindowsProfiles(
|
||||
t *testing.T,
|
||||
ds *Datastore,
|
||||
|
|
@ -2068,7 +2227,8 @@ func testBatchSetMDMWindowsProfiles(t *testing.T, ds *Datastore) {
|
|||
ctx := context.Background()
|
||||
|
||||
applyAndExpect := func(newSet []*fleet.MDMWindowsConfigProfile, tmID *uint, want []*fleet.MDMWindowsConfigProfile,
|
||||
wantUpdated bool) map[string]string {
|
||||
wantUpdated bool,
|
||||
) map[string]string {
|
||||
err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
||||
updatedDB, err := ds.batchSetMDMWindowsProfilesDB(ctx, tx, tmID, newSet)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -2197,9 +2357,12 @@ func windowsConfigProfileForTest(t *testing.T, name, locURI string, labels ...*f
|
|||
}
|
||||
|
||||
for _, lbl := range labels {
|
||||
if strings.HasPrefix(lbl.Name, "exclude-") {
|
||||
switch {
|
||||
case strings.HasPrefix(lbl.Name, "exclude-"):
|
||||
prof.LabelsExcludeAny = append(prof.LabelsExcludeAny, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID})
|
||||
} else {
|
||||
case strings.HasPrefix(lbl.Name, "include-any-"):
|
||||
prof.LabelsIncludeAny = append(prof.LabelsIncludeAny, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID})
|
||||
default:
|
||||
prof.LabelsIncludeAll = append(prof.LabelsIncludeAll, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
package tables
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func init() {
|
||||
MigrationClient.AddMigration(Up_20241030102721, Down_20241030102721)
|
||||
}
|
||||
|
||||
func Up_20241030102721(tx *sql.Tx) error {
|
||||
// Add columns
|
||||
_, err := tx.Exec(`ALTER TABLE mdm_configuration_profile_labels ADD COLUMN require_all BOOL NOT NULL DEFAULT false`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add require_all to mdm_configuration_profile_labels: %w", err)
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`ALTER TABLE mdm_declaration_labels ADD COLUMN require_all BOOL NOT NULL DEFAULT false`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add require_all to mdm_declaration_labels: %w", err)
|
||||
}
|
||||
|
||||
// Set require_all to true if exclude was false (this means that it represents an "include all"
|
||||
// label filter
|
||||
_, err = tx.Exec(`UPDATE mdm_configuration_profile_labels SET require_all = true WHERE exclude = false`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to migrate include all records in mdm_configuration_profile_labels: %w", err)
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`UPDATE mdm_declaration_labels SET require_all = true WHERE exclude = false`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to migrate include all records in mdm_declaration_labels: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Down_20241030102721(tx *sql.Tx) error {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package tables
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUp_20241030102721(t *testing.T) {
|
||||
db := applyUpToPrev(t)
|
||||
|
||||
// insert 2 profiles and 2 declarations
|
||||
execNoErr(t, db, `INSERT INTO mdm_apple_configuration_profiles (team_id, identifier, name, mobileconfig, checksum, profile_uuid) VALUES (0, 'A', 'nameA', '<plist></plist>', '', 'A')`)
|
||||
execNoErr(t, db, `INSERT INTO mdm_apple_configuration_profiles (team_id, identifier, name, mobileconfig, checksum, profile_uuid) VALUES (0, 'B', 'nameB', '<plist></plist>', '', 'B')`)
|
||||
|
||||
execNoErr(t, db, `INSERT INTO mdm_apple_declarations (declaration_uuid, identifier, name, raw_json, checksum, team_id) VALUES ('C', 'C', 'nameC', '{"foo": "bar"}', '', 0)`)
|
||||
execNoErr(t, db, `INSERT INTO mdm_apple_declarations (declaration_uuid, identifier, name, raw_json, checksum, team_id) VALUES ('D', 'D', 'nameD', '{"foo": "bar"}', '', 0)`)
|
||||
|
||||
// insert 2 profile labels associations: 1 that's exclude any and 1 that's include all
|
||||
cfgExcludeAnyID := execNoErrLastID(t, db, `INSERT INTO mdm_configuration_profile_labels (apple_profile_uuid, label_name, exclude) VALUES ('A', 'foo', true)`)
|
||||
cfgIncludeAllID := execNoErrLastID(t, db, `INSERT INTO mdm_configuration_profile_labels (apple_profile_uuid, label_name, exclude) VALUES ('B', 'bar', false)`)
|
||||
|
||||
declExcludeAnyID := execNoErrLastID(t, db, `INSERT INTO mdm_declaration_labels (apple_declaration_uuid, label_name, exclude) VALUES ('C', 'baz', true)`)
|
||||
declIncludeAllID := execNoErrLastID(t, db, `INSERT INTO mdm_declaration_labels (apple_declaration_uuid, label_name, exclude) VALUES ('C', 'boo', false)`)
|
||||
|
||||
// Apply current migration.
|
||||
applyNext(t, db)
|
||||
|
||||
var cps []struct {
|
||||
ID int64 `db:"id"`
|
||||
Exclude bool `db:"exclude"`
|
||||
AllLabels bool `db:"require_all"`
|
||||
}
|
||||
|
||||
err := sqlx.SelectContext(context.Background(), db, &cps, `SELECT id, exclude, require_all FROM mdm_configuration_profile_labels`)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, c := range cps {
|
||||
// the exclude any should be unchanged
|
||||
if c.ID == cfgExcludeAnyID {
|
||||
require.True(t, c.Exclude)
|
||||
require.False(t, c.AllLabels)
|
||||
}
|
||||
|
||||
// the include all should have require_all = true
|
||||
if c.ID == cfgIncludeAllID {
|
||||
require.False(t, c.Exclude)
|
||||
require.True(t, c.AllLabels)
|
||||
}
|
||||
}
|
||||
|
||||
err = sqlx.SelectContext(context.Background(), db, &cps, `SELECT id, exclude, require_all FROM mdm_declaration_labels`)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, c := range cps {
|
||||
// the exclude any should be unchanged
|
||||
if c.ID == declExcludeAnyID {
|
||||
require.True(t, c.Exclude)
|
||||
require.False(t, c.AllLabels)
|
||||
}
|
||||
|
||||
// the include all should have require_all = true
|
||||
if c.ID == declIncludeAllID {
|
||||
require.False(t, c.Exclude)
|
||||
require.True(t, c.AllLabels)
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -385,6 +385,7 @@ func (s *MacOSSettings) FromMap(m map[string]interface{}) (map[string]bool, erro
|
|||
spec.Labels = extractLabelField(m, "labels")
|
||||
spec.LabelsIncludeAll = extractLabelField(m, "labels_include_all")
|
||||
spec.LabelsExcludeAny = extractLabelField(m, "labels_exclude_any")
|
||||
spec.LabelsIncludeAny = extractLabelField(m, "labels_include_any")
|
||||
|
||||
csSpecs = append(csSpecs, spec)
|
||||
} else if m, ok := v.(string); ok { // for backwards compatibility with the old way to define profiles
|
||||
|
|
|
|||
|
|
@ -201,6 +201,7 @@ type MDMAppleConfigProfile struct {
|
|||
// Checksum is an MD5 hash of the Mobileconfig bytes
|
||||
Checksum []byte `db:"checksum" json:"checksum,omitempty"`
|
||||
LabelsIncludeAll []ConfigurationProfileLabel `db:"-" json:"labels_include_all,omitempty"`
|
||||
LabelsIncludeAny []ConfigurationProfileLabel `db:"-" json:"labels_include_any,omitempty"`
|
||||
LabelsExcludeAny []ConfigurationProfileLabel `db:"-" json:"labels_exclude_any,omitempty"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UploadedAt time.Time `db:"uploaded_at" json:"updated_at"` // NOTE: JSON field is still `updated_at` for historical reasons, would be an API breaking change
|
||||
|
|
@ -217,13 +218,18 @@ type MDMProfilesUpdates struct {
|
|||
// profiles and labels.
|
||||
//
|
||||
// NOTE: json representation of the fields is a bit awkward to match the
|
||||
// required API response, as this struct is returned within profile responses.
|
||||
// required API response, as this struct is returned within profile
|
||||
// responses.
|
||||
//
|
||||
// NOTE The fields in this struct other than LabelName and LabelID
|
||||
// MAY NOT BE SET CORRECTLY, dependong on where they're being ingested from.
|
||||
type ConfigurationProfileLabel struct {
|
||||
ProfileUUID string `db:"profile_uuid" json:"-"`
|
||||
LabelName string `db:"label_name" json:"name"`
|
||||
LabelID uint `db:"label_id" json:"id,omitempty"` // omitted if 0 (which is impossible if the label is not broken)
|
||||
Broken bool `db:"broken" json:"broken,omitempty"` // omitted (not rendered to JSON) if false
|
||||
Exclude bool `db:"exclude" json:"-"` // not rendered in JSON, used to store the profile in LabelsIncludeAll or LabelsExcludeAny on the parent profile
|
||||
Exclude bool `db:"exclude" json:"-"` // not rendered in JSON, used to store the profile in LabelsIncludeAll, LabelsIncludeAny, or LabelsExcludeAny on the parent profile
|
||||
RequireAll bool `db:"require_all" json:"-"` // not rendered in JSON, used to store the profile in LabelsIncludeAll, LabelsIncludeAny, or LabelsIncludeAny on the parent profile
|
||||
}
|
||||
|
||||
func NewMDMAppleConfigProfile(raw []byte, teamID *uint) (*MDMAppleConfigProfile, error) {
|
||||
|
|
@ -599,6 +605,7 @@ type MDMAppleDeclaration struct {
|
|||
|
||||
// labels associated with this Declaration
|
||||
LabelsIncludeAll []ConfigurationProfileLabel `db:"-" json:"labels_include_all,omitempty"`
|
||||
LabelsIncludeAny []ConfigurationProfileLabel `db:"-" json:"labels_include_any,omitempty"`
|
||||
LabelsExcludeAny []ConfigurationProfileLabel `db:"-" json:"labels_exclude_any,omitempty"`
|
||||
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
|
|
|
|||
|
|
@ -479,7 +479,6 @@ func TestMDMAppleHostDeclarationEqual(t *testing.T) {
|
|||
items[0].Status = nil
|
||||
items[1].Status = nil
|
||||
assert.True(t, items[0].Equal(items[1]))
|
||||
|
||||
}
|
||||
|
||||
func TestMDMAppleProfilePayloadEqual(t *testing.T) {
|
||||
|
|
@ -559,7 +558,6 @@ func TestMDMAppleProfilePayloadEqual(t *testing.T) {
|
|||
items[0].Checksum = nil
|
||||
items[1].Checksum = nil
|
||||
assert.True(t, items[0].Equal(items[1]))
|
||||
|
||||
}
|
||||
|
||||
func TestConfigurationProfileLabelEqual(t *testing.T) {
|
||||
|
|
@ -608,9 +606,10 @@ func TestConfigurationProfileLabelEqual(t *testing.T) {
|
|||
fieldsInEqualMethod++
|
||||
items[1].Exclude = items[0].Exclude
|
||||
fieldsInEqualMethod++
|
||||
items[1].RequireAll = items[0].RequireAll
|
||||
fieldsInEqualMethod++
|
||||
|
||||
assert.Equal(t, fieldsInEqualMethod, numberOfFields,
|
||||
"Does cmp.Equal for ConfigurationProfileLabel needs to be updated for new/updated field(s)?")
|
||||
assert.True(t, cmp.Equal(items[0], items[1]))
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -419,6 +419,7 @@ type MDMConfigProfilePayload struct {
|
|||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UploadedAt time.Time `json:"updated_at" db:"uploaded_at"` // NOTE: JSON field is still `updated_at` for historical reasons, would be an API breaking change
|
||||
LabelsIncludeAll []ConfigurationProfileLabel `json:"labels_include_all,omitempty" db:"-"`
|
||||
LabelsIncludeAny []ConfigurationProfileLabel `json:"labels_include_any,omitempty" db:"-"`
|
||||
LabelsExcludeAny []ConfigurationProfileLabel `json:"labels_exclude_any,omitempty" db:"-"`
|
||||
}
|
||||
|
||||
|
|
@ -432,6 +433,7 @@ type MDMProfileBatchPayload struct {
|
|||
// LabelsIncludeAll.
|
||||
Labels []string `json:"labels,omitempty"`
|
||||
LabelsIncludeAll []string `json:"labels_include_all,omitempty"`
|
||||
LabelsIncludeAny []string `json:"labels_include_any,omitempty"`
|
||||
LabelsExcludeAny []string `json:"labels_exclude_any,omitempty"`
|
||||
}
|
||||
|
||||
|
|
@ -448,6 +450,7 @@ func NewMDMConfigProfilePayloadFromWindows(cp *MDMWindowsConfigProfile) *MDMConf
|
|||
CreatedAt: cp.CreatedAt,
|
||||
UploadedAt: cp.UploadedAt,
|
||||
LabelsIncludeAll: cp.LabelsIncludeAll,
|
||||
LabelsIncludeAny: cp.LabelsIncludeAny,
|
||||
LabelsExcludeAny: cp.LabelsExcludeAny,
|
||||
}
|
||||
}
|
||||
|
|
@ -467,6 +470,7 @@ func NewMDMConfigProfilePayloadFromApple(cp *MDMAppleConfigProfile) *MDMConfigPr
|
|||
CreatedAt: cp.CreatedAt,
|
||||
UploadedAt: cp.UploadedAt,
|
||||
LabelsIncludeAll: cp.LabelsIncludeAll,
|
||||
LabelsIncludeAny: cp.LabelsIncludeAny,
|
||||
LabelsExcludeAny: cp.LabelsExcludeAny,
|
||||
}
|
||||
}
|
||||
|
|
@ -486,6 +490,7 @@ func NewMDMConfigProfilePayloadFromAppleDDM(decl *MDMAppleDeclaration) *MDMConfi
|
|||
CreatedAt: decl.CreatedAt,
|
||||
UploadedAt: decl.UploadedAt,
|
||||
LabelsIncludeAll: decl.LabelsIncludeAll,
|
||||
LabelsIncludeAny: decl.LabelsIncludeAny,
|
||||
LabelsExcludeAny: decl.LabelsExcludeAny,
|
||||
}
|
||||
}
|
||||
|
|
@ -504,6 +509,10 @@ type MDMProfileSpec struct {
|
|||
// of in order to receive the profile. It must be a member of all listed
|
||||
// labels.
|
||||
LabelsIncludeAll []string `json:"labels_include_all,omitempty"`
|
||||
// LabelsIncludeAny is a list of label names that the host must be a member
|
||||
// of in order to receive the profile. It may be a member of
|
||||
// any listed labels.
|
||||
LabelsIncludeAny []string `json:"labels_include_any,omitempty"`
|
||||
// LabelsExcludeAll is a list of label names that the host must not be a
|
||||
// member of in order to receive the profile. It must not be a member of any
|
||||
// of the listed labels.
|
||||
|
|
@ -516,26 +525,30 @@ func (p *MDMProfileSpec) UnmarshalJSON(data []byte) error {
|
|||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if lookAhead := bytes.TrimSpace(data); len(lookAhead) > 0 && lookAhead[0] == '"' {
|
||||
var backwardsCompat string
|
||||
if err := json.Unmarshal(data, &backwardsCompat); err != nil {
|
||||
return fmt.Errorf("unmarshal profile spec. Error using old format: %w", err)
|
||||
}
|
||||
p.Path = backwardsCompat
|
||||
|
||||
// FIXME: equivalent of no label condition, should clear all labels slice?
|
||||
// p.Labels = nil
|
||||
// p.LabelsIncludeAll = nil
|
||||
// p.LabelsIncludeAny = nil
|
||||
// p.LabelsExcludeAny = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// use an alias type to avoid recursively calling this function forever.
|
||||
type Alias MDMProfileSpec
|
||||
aliasData := struct {
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(p),
|
||||
}
|
||||
var aliasData Alias
|
||||
if err := json.Unmarshal(data, &aliasData); err != nil {
|
||||
return fmt.Errorf("unmarshal profile spec. Error using new format: %w", err)
|
||||
}
|
||||
// NOTE: we always want the newly unmarshaled profile spec to completely replace the old one
|
||||
// (rather than merging the new data into the old one).
|
||||
*p = MDMProfileSpec(aliasData)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -558,6 +571,10 @@ func (p *MDMProfileSpec) Copy() *MDMProfileSpec {
|
|||
clone.LabelsIncludeAll = make([]string, len(p.LabelsIncludeAll))
|
||||
copy(clone.LabelsIncludeAll, p.LabelsIncludeAll)
|
||||
}
|
||||
if len(p.LabelsIncludeAny) > 0 {
|
||||
clone.LabelsIncludeAny = make([]string, len(p.LabelsIncludeAny))
|
||||
copy(clone.LabelsIncludeAny, p.LabelsIncludeAny)
|
||||
}
|
||||
if len(p.LabelsExcludeAny) > 0 {
|
||||
clone.LabelsExcludeAny = make([]string, len(p.LabelsExcludeAny))
|
||||
copy(clone.LabelsExcludeAny, p.LabelsExcludeAny)
|
||||
|
|
@ -591,6 +608,10 @@ func MDMProfileSpecsMatch(a, b []MDMProfileSpec) bool {
|
|||
pathLabelIncludeCounts[v.Path] = labelCountMap(v.Labels)
|
||||
}
|
||||
}
|
||||
pathLabelsIncludeAnyCounts := make(map[string]map[string]int)
|
||||
for _, v := range a {
|
||||
pathLabelsIncludeAnyCounts[v.Path] = labelCountMap(v.LabelsIncludeAny)
|
||||
}
|
||||
pathLabelExcludeCounts := make(map[string]map[string]int)
|
||||
for _, v := range a {
|
||||
pathLabelExcludeCounts[v.Path] = labelCountMap(v.LabelsExcludeAny)
|
||||
|
|
@ -598,8 +619,9 @@ func MDMProfileSpecsMatch(a, b []MDMProfileSpec) bool {
|
|||
|
||||
for _, v := range b {
|
||||
includeLabels, okIncl := pathLabelIncludeCounts[v.Path]
|
||||
includeAnyLabels, okInclAny := pathLabelsIncludeAnyCounts[v.Path]
|
||||
excludeLabels, okExcl := pathLabelExcludeCounts[v.Path]
|
||||
if !okIncl || !okExcl {
|
||||
if !okIncl || !okExcl || !okInclAny {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -621,6 +643,19 @@ func MDMProfileSpecsMatch(a, b []MDMProfileSpec) bool {
|
|||
}
|
||||
}
|
||||
|
||||
bLabelIncludeAnyCounts := labelCountMap(v.LabelsIncludeAny)
|
||||
for label, count := range bLabelIncludeAnyCounts {
|
||||
if includeAnyLabels[label] != count {
|
||||
return false
|
||||
}
|
||||
includeAnyLabels[label] -= count
|
||||
}
|
||||
for _, count := range includeAnyLabels {
|
||||
if count != 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
bLabelExcludeCounts := labelCountMap(v.LabelsExcludeAny)
|
||||
for label, count := range bLabelExcludeCounts {
|
||||
if excludeLabels[label] != count {
|
||||
|
|
@ -635,12 +670,21 @@ func MDMProfileSpecsMatch(a, b []MDMProfileSpec) bool {
|
|||
}
|
||||
|
||||
delete(pathLabelIncludeCounts, v.Path)
|
||||
delete(pathLabelsIncludeAnyCounts, v.Path)
|
||||
delete(pathLabelExcludeCounts, v.Path)
|
||||
}
|
||||
|
||||
return len(pathLabelIncludeCounts) == 0 && len(pathLabelExcludeCounts) == 0
|
||||
return len(pathLabelIncludeCounts) == 0 && len(pathLabelsIncludeAnyCounts) == 0 && len(pathLabelExcludeCounts) == 0
|
||||
}
|
||||
|
||||
type MDMLabelsMode string
|
||||
|
||||
const (
|
||||
LabelsIncludeAll MDMLabelsMode = "labels_include_all"
|
||||
LabelsIncludeAny MDMLabelsMode = "labels_include_any"
|
||||
LabelsExcludeAny MDMLabelsMode = "labels_exclude_any"
|
||||
)
|
||||
|
||||
type MDMAssetName string
|
||||
|
||||
const (
|
||||
|
|
@ -741,9 +785,11 @@ func FilterMacOSOnlyProfilesFromIOSIPadOS(profiles []*MDMAppleProfilePayload) []
|
|||
}
|
||||
|
||||
// RefetchBaseCommandUUIDPrefix and below command prefixes are the prefixes used for MDM commands used to refetch information from iOS/iPadOS devices.
|
||||
const RefetchBaseCommandUUIDPrefix = "REFETCH-"
|
||||
const RefetchDeviceCommandUUIDPrefix = RefetchBaseCommandUUIDPrefix + "DEVICE-"
|
||||
const RefetchAppsCommandUUIDPrefix = RefetchBaseCommandUUIDPrefix + "APPS-"
|
||||
const (
|
||||
RefetchBaseCommandUUIDPrefix = "REFETCH-"
|
||||
RefetchDeviceCommandUUIDPrefix = RefetchBaseCommandUUIDPrefix + "DEVICE-"
|
||||
RefetchAppsCommandUUIDPrefix = RefetchBaseCommandUUIDPrefix + "APPS-"
|
||||
)
|
||||
|
||||
// VPPTokenInfo is the representation of the VPP token that we send out via API.
|
||||
type VPPTokenInfo struct {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package fleet_test
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
|
@ -122,72 +123,106 @@ func TestDEPClient(t *testing.T) {
|
|||
wantToksTermsFlags map[string]bool
|
||||
}{
|
||||
// use a valid token, appconfig should not be updated (already unflagged)
|
||||
{token: validToken, orgName: "org1", wantErr: false, readInvoked: true, writeTokInvoked: false,
|
||||
writeAppCfgInvoked: false, wantAppCfgTermsFlag: false, wantToksTermsFlags: map[string]bool{"org1": false, "org2": false}},
|
||||
{
|
||||
token: validToken, orgName: "org1", wantErr: false, readInvoked: true, writeTokInvoked: false,
|
||||
writeAppCfgInvoked: false, wantAppCfgTermsFlag: false, wantToksTermsFlags: map[string]bool{"org1": false, "org2": false},
|
||||
},
|
||||
|
||||
// use a valid token without org, nothing is checked
|
||||
{token: validToken, orgName: "", wantErr: false, readInvoked: false, writeTokInvoked: false,
|
||||
writeAppCfgInvoked: false, wantAppCfgTermsFlag: false, wantToksTermsFlags: map[string]bool{"org1": false, "org2": false}},
|
||||
{
|
||||
token: validToken, orgName: "", wantErr: false, readInvoked: false, writeTokInvoked: false,
|
||||
writeAppCfgInvoked: false, wantAppCfgTermsFlag: false, wantToksTermsFlags: map[string]bool{"org1": false, "org2": false},
|
||||
},
|
||||
|
||||
// use an invalid token without org, call fails but nothing is checked because this is an unsaved token
|
||||
{token: invalidToken, orgName: "", wantErr: true, readInvoked: false, writeTokInvoked: false,
|
||||
writeAppCfgInvoked: false, wantAppCfgTermsFlag: false, wantToksTermsFlags: map[string]bool{"org1": false, "org2": false}},
|
||||
{
|
||||
token: invalidToken, orgName: "", wantErr: true, readInvoked: false, writeTokInvoked: false,
|
||||
writeAppCfgInvoked: false, wantAppCfgTermsFlag: false, wantToksTermsFlags: map[string]bool{"org1": false, "org2": false},
|
||||
},
|
||||
|
||||
// use an invalid token, appconfig should not even be read (not a terms error)
|
||||
{token: invalidToken, orgName: "org1", wantErr: true, readInvoked: false, writeTokInvoked: false,
|
||||
writeAppCfgInvoked: false, wantAppCfgTermsFlag: false, wantToksTermsFlags: map[string]bool{"org1": false, "org2": false}},
|
||||
{
|
||||
token: invalidToken, orgName: "org1", wantErr: true, readInvoked: false, writeTokInvoked: false,
|
||||
writeAppCfgInvoked: false, wantAppCfgTermsFlag: false, wantToksTermsFlags: map[string]bool{"org1": false, "org2": false},
|
||||
},
|
||||
|
||||
// terms changed for org1 during the auth request
|
||||
{token: termsChangedToken, orgName: "org1", wantErr: true, readInvoked: true, writeTokInvoked: true,
|
||||
writeAppCfgInvoked: true, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": true, "org2": false}},
|
||||
{
|
||||
token: termsChangedToken, orgName: "org1", wantErr: true, readInvoked: true, writeTokInvoked: true,
|
||||
writeAppCfgInvoked: true, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": true, "org2": false},
|
||||
},
|
||||
|
||||
// use of an invalid token does not update the flag
|
||||
{token: invalidToken, orgName: "org1", wantErr: true, readInvoked: false, writeTokInvoked: false,
|
||||
writeAppCfgInvoked: false, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": true, "org2": false}},
|
||||
{
|
||||
token: invalidToken, orgName: "org1", wantErr: true, readInvoked: false, writeTokInvoked: false,
|
||||
writeAppCfgInvoked: false, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": true, "org2": false},
|
||||
},
|
||||
|
||||
// use of a valid token for org1 resets the flags
|
||||
{token: validToken, orgName: "org1", wantErr: false, readInvoked: true, writeTokInvoked: true,
|
||||
writeAppCfgInvoked: true, wantAppCfgTermsFlag: false, wantToksTermsFlags: map[string]bool{"org1": false, "org2": false}},
|
||||
{
|
||||
token: validToken, orgName: "org1", wantErr: false, readInvoked: true, writeTokInvoked: true,
|
||||
writeAppCfgInvoked: true, wantAppCfgTermsFlag: false, wantToksTermsFlags: map[string]bool{"org1": false, "org2": false},
|
||||
},
|
||||
|
||||
// use of a valid token again with org2 does not update anything
|
||||
{token: validToken, orgName: "org2", wantErr: false, readInvoked: true, writeTokInvoked: false,
|
||||
writeAppCfgInvoked: false, wantAppCfgTermsFlag: false, wantToksTermsFlags: map[string]bool{"org1": false, "org2": false}},
|
||||
{
|
||||
token: validToken, orgName: "org2", wantErr: false, readInvoked: true, writeTokInvoked: false,
|
||||
writeAppCfgInvoked: false, wantAppCfgTermsFlag: false, wantToksTermsFlags: map[string]bool{"org1": false, "org2": false},
|
||||
},
|
||||
|
||||
// terms changed for org2 during the actual account request, after auth
|
||||
{token: termsChangedAfterAuthToken, orgName: "org2", wantErr: true, readInvoked: true, writeTokInvoked: true,
|
||||
writeAppCfgInvoked: true, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": false, "org2": true}},
|
||||
{
|
||||
token: termsChangedAfterAuthToken, orgName: "org2", wantErr: true, readInvoked: true, writeTokInvoked: true,
|
||||
writeAppCfgInvoked: true, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": false, "org2": true},
|
||||
},
|
||||
|
||||
// again terms changed after auth for org2, doesn't update appConfig
|
||||
{token: termsChangedAfterAuthToken, orgName: "org2", wantErr: true, readInvoked: true, writeTokInvoked: true,
|
||||
writeAppCfgInvoked: false, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": false, "org2": true}},
|
||||
{
|
||||
token: termsChangedAfterAuthToken, orgName: "org2", wantErr: true, readInvoked: true, writeTokInvoked: true,
|
||||
writeAppCfgInvoked: false, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": false, "org2": true},
|
||||
},
|
||||
|
||||
// terms changed during auth for org2, doesn't update appConfig
|
||||
{token: termsChangedToken, orgName: "org2", wantErr: true, readInvoked: true, writeTokInvoked: true,
|
||||
writeAppCfgInvoked: false, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": false, "org2": true}},
|
||||
{
|
||||
token: termsChangedToken, orgName: "org2", wantErr: true, readInvoked: true, writeTokInvoked: true,
|
||||
writeAppCfgInvoked: false, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": false, "org2": true},
|
||||
},
|
||||
|
||||
// terms changed during auth for org1, now both tokens have the flag, doesn't update appConfig
|
||||
{token: termsChangedToken, orgName: "org1", wantErr: true, readInvoked: true, writeTokInvoked: true,
|
||||
writeAppCfgInvoked: false, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": true, "org2": true}},
|
||||
{
|
||||
token: termsChangedToken, orgName: "org1", wantErr: true, readInvoked: true, writeTokInvoked: true,
|
||||
writeAppCfgInvoked: false, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": true, "org2": true},
|
||||
},
|
||||
|
||||
// use a valid token without org, nothing is checked
|
||||
{token: validToken, orgName: "", wantErr: false, readInvoked: false, writeTokInvoked: false,
|
||||
writeAppCfgInvoked: false, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": true, "org2": true}},
|
||||
{
|
||||
token: validToken, orgName: "", wantErr: false, readInvoked: false, writeTokInvoked: false,
|
||||
writeAppCfgInvoked: false, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": true, "org2": true},
|
||||
},
|
||||
|
||||
// use an invalid token without org, call fails but nothing is checked because this is an unsaved token
|
||||
{token: invalidToken, orgName: "", wantErr: true, readInvoked: false, writeTokInvoked: false,
|
||||
writeAppCfgInvoked: false, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": true, "org2": true}},
|
||||
{
|
||||
token: invalidToken, orgName: "", wantErr: true, readInvoked: false, writeTokInvoked: false,
|
||||
writeAppCfgInvoked: false, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": true, "org2": true},
|
||||
},
|
||||
|
||||
// valid token for org1, resets that token's flag but not appConfig
|
||||
{token: validToken, orgName: "org1", wantErr: false, readInvoked: true, writeTokInvoked: true,
|
||||
writeAppCfgInvoked: false, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": false, "org2": true}},
|
||||
{
|
||||
token: validToken, orgName: "org1", wantErr: false, readInvoked: true, writeTokInvoked: true,
|
||||
writeAppCfgInvoked: false, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": false, "org2": true},
|
||||
},
|
||||
|
||||
// valid token again for org1, still no write to appConfig
|
||||
{token: validToken, orgName: "org1", wantErr: false, readInvoked: true, writeTokInvoked: true,
|
||||
writeAppCfgInvoked: false, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": false, "org2": true}},
|
||||
{
|
||||
token: validToken, orgName: "org1", wantErr: false, readInvoked: true, writeTokInvoked: true,
|
||||
writeAppCfgInvoked: false, wantAppCfgTermsFlag: true, wantToksTermsFlags: map[string]bool{"org1": false, "org2": true},
|
||||
},
|
||||
|
||||
// valid token again for org2, this time resets appConfig
|
||||
{token: validToken, orgName: "org2", wantErr: false, readInvoked: true, writeTokInvoked: true,
|
||||
writeAppCfgInvoked: true, wantAppCfgTermsFlag: false, wantToksTermsFlags: map[string]bool{"org1": false, "org2": false}},
|
||||
{
|
||||
token: validToken, orgName: "org2", wantErr: false, readInvoked: true, writeTokInvoked: true,
|
||||
writeAppCfgInvoked: true, wantAppCfgTermsFlag: false, wantToksTermsFlags: map[string]bool{"org1": false, "org2": false},
|
||||
},
|
||||
}
|
||||
|
||||
// order of calls is important, and test must not be parallelized as it would
|
||||
|
|
@ -333,6 +368,67 @@ func TestMDMProfileSpecUnmarshalJSON(t *testing.T) {
|
|||
require.Equal(t, "oldpath", p.Path)
|
||||
require.Empty(t, p.Labels)
|
||||
})
|
||||
|
||||
t.Run("changing labels", func(t *testing.T) {
|
||||
// When updating AppConfig, we unmarshal the incoming JSON into the existing AppConfig
|
||||
// struct, see
|
||||
// https://github.com/fleetdm/fleet/blob/d1144df1318b50482cbd9eb996b863443975f138/server/service/appconfig.go#L334-L335
|
||||
//
|
||||
// But we found there were issues unmarshaling the slice of profile specs where if a key is present in an old
|
||||
// element but not in the new element (e.g. element[0] of the old slice and element[0] of the
|
||||
// new slice), both keys were preserved. This test is designed to cover that issue, which
|
||||
// was addressed in the unmarshal function, see
|
||||
// https://github.com/fleetdm/fleet/blob/1042702def54f095335d8b42ed5fdcc90468fa0d/server/fleet/mdm.go#L551-L552
|
||||
|
||||
storedConfig := fleet.AppConfig{
|
||||
OrgInfo: fleet.OrgInfo{
|
||||
OrgName: "Test",
|
||||
},
|
||||
MDM: fleet.MDM{
|
||||
MacOSSettings: fleet.MacOSSettings{
|
||||
CustomSettings: []fleet.MDMProfileSpec{
|
||||
{
|
||||
Path: "some-profile-2",
|
||||
LabelsExcludeAny: []string{"bar"},
|
||||
},
|
||||
{
|
||||
Path: "some-profile-1",
|
||||
LabelsIncludeAll: []string{"foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
incomingConfig := fleet.AppConfig{
|
||||
OrgInfo: fleet.OrgInfo{
|
||||
OrgName: "Test",
|
||||
},
|
||||
MDM: fleet.MDM{
|
||||
MacOSSettings: fleet.MacOSSettings{
|
||||
CustomSettings: []fleet.MDMProfileSpec{
|
||||
{
|
||||
Path: "some-profile-1",
|
||||
LabelsIncludeAll: []string{"foo"},
|
||||
},
|
||||
{
|
||||
Path: "some-profile-2",
|
||||
LabelsIncludeAny: []string{"bar"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
b, err := json.Marshal(incomingConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = json.Unmarshal(b, &storedConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, storedConfig.MDM.MacOSSettings.CustomSettings, incomingConfig.MDM.MacOSSettings.CustomSettings)
|
||||
require.Nil(t, storedConfig.MDM.MacOSSettings.CustomSettings[0].LabelsExcludeAny) // old key should be removed
|
||||
require.Nil(t, storedConfig.MDM.MacOSSettings.CustomSettings[1].LabelsIncludeAll) // old key should be removed
|
||||
})
|
||||
}
|
||||
|
||||
func TestMDMProfileSpecsMatch(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -759,9 +759,9 @@ type Service interface {
|
|||
GetHostDEPAssignment(ctx context.Context, host *Host) (*HostDEPAssignment, error)
|
||||
|
||||
// NewMDMAppleConfigProfile creates a new configuration profile for the specified team.
|
||||
NewMDMAppleConfigProfile(ctx context.Context, teamID uint, r io.Reader, labels []string, labelsExcludeMode bool) (*MDMAppleConfigProfile, error)
|
||||
NewMDMAppleConfigProfile(ctx context.Context, teamID uint, r io.Reader, labels []string, labelsMembershipMode MDMLabelsMode) (*MDMAppleConfigProfile, error)
|
||||
// NewMDMAppleConfigProfileWithPayload creates a new declaration for the specified team.
|
||||
NewMDMAppleDeclaration(ctx context.Context, teamID uint, r io.Reader, labels []string, name string, labelsExcludeMode bool) (*MDMAppleDeclaration, error)
|
||||
NewMDMAppleDeclaration(ctx context.Context, teamID uint, r io.Reader, labels []string, name string, labelsMembershipMode MDMLabelsMode) (*MDMAppleDeclaration, error)
|
||||
|
||||
// GetMDMAppleConfigProfileByDeprecatedID retrieves the specified Apple
|
||||
// configuration profile via its numeric ID. This method is deprecated and
|
||||
|
|
@ -1033,7 +1033,7 @@ type Service interface {
|
|||
|
||||
// NewMDMWindowsConfigProfile creates a new Windows configuration profile for
|
||||
// the specified team.
|
||||
NewMDMWindowsConfigProfile(ctx context.Context, teamID uint, profileName string, r io.Reader, labels []string, labelsExcludeMode bool) (*MDMWindowsConfigProfile, error)
|
||||
NewMDMWindowsConfigProfile(ctx context.Context, teamID uint, profileName string, r io.Reader, labels []string, labelsMembershipMode MDMLabelsMode) (*MDMWindowsConfigProfile, error)
|
||||
|
||||
// NewMDMUnsupportedConfigProfile is called when a profile with an
|
||||
// unsupported extension is uploaded.
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ type MDMWindowsConfigProfile struct {
|
|||
Name string `db:"name" json:"name"`
|
||||
SyncML []byte `db:"syncml" json:"-"`
|
||||
LabelsIncludeAll []ConfigurationProfileLabel `db:"-" json:"labels_include_all,omitempty"`
|
||||
LabelsIncludeAny []ConfigurationProfileLabel `db:"-" json:"labels_include_any,omitempty"`
|
||||
LabelsExcludeAny []ConfigurationProfileLabel `db:"-" json:"labels_exclude_any,omitempty"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UploadedAt time.Time `db:"uploaded_at" json:"updated_at"` // NOTE: JSON field is still `updated_at` for historical reasons, would be an API breaking change
|
||||
|
|
|
|||
|
|
@ -986,14 +986,19 @@ func (svc *Service) validateMDM(
|
|||
checkCustomSettings := func(prefix string, customSettings []fleet.MDMProfileSpec) {
|
||||
for i, prof := range customSettings {
|
||||
count := 0
|
||||
for _, b := range []bool{len(prof.Labels) > 0, len(prof.LabelsIncludeAll) > 0, len(prof.LabelsExcludeAny) > 0} {
|
||||
for _, b := range []bool{
|
||||
len(prof.Labels) > 0,
|
||||
len(prof.LabelsIncludeAll) > 0,
|
||||
len(prof.LabelsIncludeAny) > 0,
|
||||
len(prof.LabelsExcludeAny) > 0,
|
||||
} {
|
||||
if b {
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count > 1 {
|
||||
invalid.Append(fmt.Sprintf("%s_settings.custom_settings", prefix),
|
||||
fmt.Sprintf(`Couldn't edit %s_settings.custom_settings. For each profile, only one of "labels_exclude_any", "labels_include_all" or "labels" can be included.`, prefix))
|
||||
fmt.Sprintf(`Couldn't edit %s_settings.custom_settings. For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`, prefix))
|
||||
}
|
||||
if len(prof.Labels) > 0 {
|
||||
customSettings[i].LabelsIncludeAll = customSettings[i].Labels
|
||||
|
|
|
|||
|
|
@ -1511,7 +1511,8 @@ func TestModifyAppConfigForNDESSCEPProxy(t *testing.T) {
|
|||
svc, ctx = newTestServiceWithConfig(t, ds, fleetConfig, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}})
|
||||
ctx = viewer.NewContext(ctx, viewer.Viewer{User: admin})
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte,
|
||||
createdAt time.Time) error {
|
||||
createdAt time.Time,
|
||||
) error {
|
||||
assert.IsType(t, fleet.ActivityAddedNDESSCEPProxy{}, activity)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1560,7 +1561,8 @@ func TestModifyAppConfigForNDESSCEPProxy(t *testing.T) {
|
|||
scepURL = "https://new.com/mscep/mscep.dll"
|
||||
jsonPayload = fmt.Sprintf(jsonPayloadBase, scepURL, adminURL, username, "")
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte,
|
||||
createdAt time.Time) error {
|
||||
createdAt time.Time,
|
||||
) error {
|
||||
assert.IsType(t, fleet.ActivityEditedNDESSCEPProxy{}, activity)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1644,7 +1646,8 @@ func TestModifyAppConfigForNDESSCEPProxy(t *testing.T) {
|
|||
// Second, real run.
|
||||
appConfig.Integrations.NDESSCEPProxy.Valid = true
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte,
|
||||
createdAt time.Time) error {
|
||||
createdAt time.Time,
|
||||
) error {
|
||||
assert.IsType(t, fleet.ActivityDeletedNDESSCEPProxy{}, activity)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1682,5 +1685,4 @@ func TestModifyAppConfigForNDESSCEPProxy(t *testing.T) {
|
|||
ctx = viewer.NewContext(ctx, viewer.Viewer{User: admin})
|
||||
_, err = svc.ModifyAppConfig(ctx, []byte(jsonPayload), fleet.ApplySpecOptions{})
|
||||
assert.ErrorContains(t, err, "private key")
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -342,7 +342,7 @@ func newMDMAppleConfigProfileEndpoint(ctx context.Context, request interface{},
|
|||
}
|
||||
defer ff.Close()
|
||||
// providing an empty set of labels since this endpoint is only maintained for backwards compat
|
||||
cp, err := svc.NewMDMAppleConfigProfile(ctx, req.TeamID, ff, nil, false)
|
||||
cp, err := svc.NewMDMAppleConfigProfile(ctx, req.TeamID, ff, nil, fleet.LabelsIncludeAll)
|
||||
if err != nil {
|
||||
return &newMDMAppleConfigProfileResponse{Err: err}, nil
|
||||
}
|
||||
|
|
@ -351,7 +351,7 @@ func newMDMAppleConfigProfileEndpoint(ctx context.Context, request interface{},
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) NewMDMAppleConfigProfile(ctx context.Context, teamID uint, r io.Reader, labels []string, labelsExcludeMode bool) (*fleet.MDMAppleConfigProfile, error) {
|
||||
func (svc *Service) NewMDMAppleConfigProfile(ctx context.Context, teamID uint, r io.Reader, labels []string, labelsMembershipMode fleet.MDMLabelsMode) (*fleet.MDMAppleConfigProfile, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.MDMConfigProfileAuthz{TeamID: &teamID}, fleet.ActionWrite); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
|
|
@ -395,10 +395,15 @@ func (svc *Service) NewMDMAppleConfigProfile(ctx context.Context, teamID uint, r
|
|||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "validating labels")
|
||||
}
|
||||
if labelsExcludeMode {
|
||||
cp.LabelsExcludeAny = labelMap
|
||||
} else {
|
||||
switch labelsMembershipMode {
|
||||
case fleet.LabelsIncludeAll:
|
||||
cp.LabelsIncludeAll = labelMap
|
||||
case fleet.LabelsIncludeAny:
|
||||
cp.LabelsIncludeAny = labelMap
|
||||
case fleet.LabelsExcludeAny:
|
||||
cp.LabelsExcludeAny = labelMap
|
||||
default:
|
||||
// TODO what happens if mode is not set?s
|
||||
}
|
||||
err = validateConfigProfileFleetVariables(string(cp.Mobileconfig))
|
||||
if err != nil {
|
||||
|
|
@ -456,7 +461,7 @@ func validateConfigProfileFleetVariables(contents string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (svc *Service) NewMDMAppleDeclaration(ctx context.Context, teamID uint, r io.Reader, labels []string, name string, labelsExcludeMode bool) (*fleet.MDMAppleDeclaration, error) {
|
||||
func (svc *Service) NewMDMAppleDeclaration(ctx context.Context, teamID uint, r io.Reader, labels []string, name string, labelsMembershipMode fleet.MDMLabelsMode) (*fleet.MDMAppleDeclaration, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.MDMConfigProfileAuthz{TeamID: &teamID}, fleet.ActionWrite); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
|
|
@ -514,9 +519,13 @@ func (svc *Service) NewMDMAppleDeclaration(ctx context.Context, teamID uint, r i
|
|||
|
||||
d := fleet.NewMDMAppleDeclaration(data, tmID, name, rawDecl.Type, rawDecl.Identifier)
|
||||
|
||||
if labelsExcludeMode {
|
||||
switch labelsMembershipMode {
|
||||
case fleet.LabelsIncludeAny:
|
||||
d.LabelsIncludeAny = validatedLabels
|
||||
case fleet.LabelsExcludeAny:
|
||||
d.LabelsExcludeAny = validatedLabels
|
||||
} else {
|
||||
default:
|
||||
// default to include all
|
||||
d.LabelsIncludeAll = validatedLabels
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -216,7 +216,8 @@ func setupAppleMDMService(t *testing.T, license *fleet.LicenseInfo) (fleet.Servi
|
|||
certPEM := tokenpki.PEMCertificate(crt.Raw)
|
||||
keyPEM := tokenpki.PEMRSAPrivateKey(key)
|
||||
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName,
|
||||
_ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
|
||||
_ sqlx.QueryerContext,
|
||||
) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
|
||||
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
|
||||
fleet.MDMAssetAPNSCert: {Value: apnsCert},
|
||||
fleet.MDMAssetAPNSKey: {Value: apnsKey},
|
||||
|
|
@ -672,11 +673,11 @@ func TestMDMAppleConfigProfileAuthz(t *testing.T) {
|
|||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// test authz create new profile (no team)
|
||||
_, err := svc.NewMDMAppleConfigProfile(ctx, 0, bytes.NewReader(mcBytes), nil, false)
|
||||
_, err := svc.NewMDMAppleConfigProfile(ctx, 0, bytes.NewReader(mcBytes), nil, fleet.LabelsIncludeAll)
|
||||
checkShouldFail(err, tt.shouldFailGlobal)
|
||||
|
||||
// test authz create new profile (team 1)
|
||||
_, err = svc.NewMDMAppleConfigProfile(ctx, 1, bytes.NewReader(mcBytes), nil, false)
|
||||
_, err = svc.NewMDMAppleConfigProfile(ctx, 1, bytes.NewReader(mcBytes), nil, fleet.LabelsIncludeAll)
|
||||
checkShouldFail(err, tt.shouldFailTeam)
|
||||
|
||||
// test authz list profiles (no team)
|
||||
|
|
@ -740,7 +741,7 @@ func TestNewMDMAppleConfigProfile(t *testing.T) {
|
|||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
|
||||
cp, err := svc.NewMDMAppleConfigProfile(ctx, 0, r, nil, false)
|
||||
cp, err := svc.NewMDMAppleConfigProfile(ctx, 0, r, nil, fleet.LabelsIncludeAll)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "Foo", cp.Name)
|
||||
assert.Equal(t, identifier, cp.Identifier)
|
||||
|
|
@ -749,7 +750,7 @@ func TestNewMDMAppleConfigProfile(t *testing.T) {
|
|||
// Unsupported Fleet variable
|
||||
mcBytes = mcBytesForTest("Foo", identifier, "UUID${FLEET_VAR_BOZO}")
|
||||
r = bytes.NewReader(mcBytes)
|
||||
_, err = svc.NewMDMAppleConfigProfile(ctx, 0, r, nil, false)
|
||||
_, err = svc.NewMDMAppleConfigProfile(ctx, 0, r, nil, fleet.LabelsIncludeAll)
|
||||
assert.ErrorContains(t, err, "Fleet variable")
|
||||
}
|
||||
|
||||
|
|
@ -781,7 +782,7 @@ func TestNewMDMAppleDeclaration(t *testing.T) {
|
|||
|
||||
// Unsupported Fleet variable
|
||||
b := declBytesForTest("D1", "d1content $FLEET_VAR_BOZO")
|
||||
_, err := svc.NewMDMAppleDeclaration(ctx, 0, bytes.NewReader(b), nil, "name", false)
|
||||
_, err := svc.NewMDMAppleDeclaration(ctx, 0, bytes.NewReader(b), nil, "name", fleet.LabelsIncludeAll)
|
||||
assert.ErrorContains(t, err, "Fleet variable")
|
||||
|
||||
ds.NewMDMAppleDeclarationFunc = func(ctx context.Context, d *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
|
||||
|
|
@ -797,7 +798,7 @@ func TestNewMDMAppleDeclaration(t *testing.T) {
|
|||
|
||||
// Good declaration
|
||||
b = declBytesForTest("D1", "d1content")
|
||||
d, err := svc.NewMDMAppleDeclaration(ctx, 0, bytes.NewReader(b), nil, "name", false)
|
||||
d, err := svc.NewMDMAppleDeclaration(ctx, 0, bytes.NewReader(b), nil, "name", fleet.LabelsIncludeAll)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, d)
|
||||
}
|
||||
|
|
@ -2236,7 +2237,8 @@ func TestMDMAppleReconcileAppleProfiles(t *testing.T) {
|
|||
AppleSCEPKey: "./testdata/server.key",
|
||||
}
|
||||
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName,
|
||||
_ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
|
||||
_ sqlx.QueryerContext,
|
||||
) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
|
||||
_, pemCert, pemKey, err := mdmConfig.AppleSCEP()
|
||||
require.NoError(t, err)
|
||||
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
|
||||
|
|
@ -2344,7 +2346,8 @@ func TestMDMAppleReconcileAppleProfiles(t *testing.T) {
|
|||
return false, nil
|
||||
}
|
||||
mdmStorage.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName,
|
||||
_ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
|
||||
_ sqlx.QueryerContext,
|
||||
) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
|
||||
certPEM, err := os.ReadFile("./testdata/server.pem")
|
||||
require.NoError(t, err)
|
||||
keyPEM, err := os.ReadFile("./testdata/server.key")
|
||||
|
|
@ -2860,7 +2863,8 @@ func TestPreprocessProfileContents(t *testing.T) {
|
|||
|
||||
ndesPassword := "test-password"
|
||||
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context,
|
||||
assetNames []fleet.MDMAssetName, _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
|
||||
assetNames []fleet.MDMAssetName, _ sqlx.QueryerContext,
|
||||
) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
|
||||
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
|
||||
fleet.MDMAssetNDESPassword: {Value: []byte(ndesPassword)},
|
||||
}, nil
|
||||
|
|
@ -3051,8 +3055,10 @@ func TestPreprocessProfileContents(t *testing.T) {
|
|||
"p3": []byte("no variables"),
|
||||
}
|
||||
addProfileToInstall := func(hostUUID, profileUUID, profileIdentifier string) {
|
||||
hostProfilesToInstallMap[hostProfileUUID{HostUUID: hostUUID,
|
||||
ProfileUUID: profileUUID}] = &fleet.MDMAppleBulkUpsertHostProfilePayload{
|
||||
hostProfilesToInstallMap[hostProfileUUID{
|
||||
HostUUID: hostUUID,
|
||||
ProfileUUID: profileUUID,
|
||||
}] = &fleet.MDMAppleBulkUpsertHostProfilePayload{
|
||||
ProfileUUID: profileUUID,
|
||||
ProfileIdentifier: profileIdentifier,
|
||||
HostUUID: hostUUID,
|
||||
|
|
@ -3060,7 +3066,6 @@ func TestPreprocessProfileContents(t *testing.T) {
|
|||
Status: &fleet.MDMDeliveryPending,
|
||||
CommandUUID: cmdUUID,
|
||||
}
|
||||
|
||||
}
|
||||
addProfileToInstall(hostUUID, "p1", "com.add.profile")
|
||||
addProfileToInstall("host-2", "p1", "com.add.profile")
|
||||
|
|
@ -3612,7 +3617,8 @@ func setupTest(t *testing.T) (context.Context, kitlog.Logger, *mock.Store, *conf
|
|||
_, pemCert, pemKey, err := mdmConfig.AppleSCEP()
|
||||
require.NoError(t, err)
|
||||
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName,
|
||||
_ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
|
||||
_ sqlx.QueryerContext,
|
||||
) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
|
||||
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
|
||||
fleet.MDMAssetCACert: {Value: pemCert},
|
||||
fleet.MDMAssetCAKey: {Value: pemKey},
|
||||
|
|
@ -3621,7 +3627,8 @@ func setupTest(t *testing.T) (context.Context, kitlog.Logger, *mock.Store, *conf
|
|||
}, nil
|
||||
}
|
||||
mdmStorage.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName,
|
||||
_ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
|
||||
_ sqlx.QueryerContext,
|
||||
) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
|
||||
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
|
||||
fleet.MDMAssetCACert: {Value: pemCert},
|
||||
fleet.MDMAssetCAKey: {Value: pemKey},
|
||||
|
|
|
|||
|
|
@ -381,6 +381,7 @@ func getProfilesContents(baseDir string, macProfiles []fleet.MDMProfileSpec, win
|
|||
Contents: fileContents,
|
||||
Labels: profile.Labels,
|
||||
LabelsIncludeAll: profile.LabelsIncludeAll,
|
||||
LabelsIncludeAny: profile.LabelsIncludeAny,
|
||||
LabelsExcludeAny: profile.LabelsExcludeAny,
|
||||
})
|
||||
|
||||
|
|
@ -1107,6 +1108,7 @@ func extractAppCfgCustomSettings(appCfg interface{}, platformKey string) []fleet
|
|||
// validations are done later on in the Fleet API endpoint.
|
||||
profSpec.Labels = extractLabelField(m, "labels")
|
||||
profSpec.LabelsIncludeAll = extractLabelField(m, "labels_include_all")
|
||||
profSpec.LabelsIncludeAny = extractLabelField(m, "labels_include_any")
|
||||
profSpec.LabelsExcludeAny = extractLabelField(m, "labels_exclude_any")
|
||||
|
||||
if profSpec.Path != "" {
|
||||
|
|
|
|||
|
|
@ -1789,7 +1789,7 @@ func (s *integrationMDMTestSuite) TestAppConfigMDMCustomSettings() {
|
|||
}
|
||||
}`), http.StatusUnprocessableEntity)
|
||||
msg := extractServerErrorText(res.Body)
|
||||
require.Contains(t, msg, `For each profile, only one of "labels_exclude_any", "labels_include_all" or "labels" can be included.`)
|
||||
require.Contains(t, msg, `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`)
|
||||
|
||||
res = s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
|
||||
"mdm": {
|
||||
|
|
@ -1801,7 +1801,31 @@ func (s *integrationMDMTestSuite) TestAppConfigMDMCustomSettings() {
|
|||
}
|
||||
}`), http.StatusUnprocessableEntity)
|
||||
msg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, msg, `For each profile, only one of "labels_exclude_any", "labels_include_all" or "labels" can be included.`)
|
||||
require.Contains(t, msg, `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`)
|
||||
|
||||
res = s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
|
||||
"mdm": {
|
||||
"windows_settings": {
|
||||
"custom_settings": [
|
||||
{"path": "foo", "labels_include_any": ["a"], "labels_exclude_any": ["b"]}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`), http.StatusUnprocessableEntity)
|
||||
msg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, msg, `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`)
|
||||
|
||||
res = s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
|
||||
"mdm": {
|
||||
"windows_settings": {
|
||||
"custom_settings": [
|
||||
{"path": "foo", "labels": ["a"], "labels_include_any": ["b"]}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`), http.StatusUnprocessableEntity)
|
||||
msg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, msg, `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`)
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) TestApplyTeamsMDMAppleProfiles() {
|
||||
|
|
@ -1920,7 +1944,7 @@ func (s *integrationMDMTestSuite) TestApplyTeamsMDMAppleProfiles() {
|
|||
}}}
|
||||
res = s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusUnprocessableEntity)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
assert.Contains(t, errMsg, `For each profile, only one of "labels_exclude_any", "labels_include_all" or "labels" can be included.`)
|
||||
assert.Contains(t, errMsg, `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`)
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) TestBatchSetMDMAppleProfiles() {
|
||||
|
|
@ -2626,15 +2650,20 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() {
|
|||
// NOTE: label names starting with "-" are sent as "labels_excluding_any"
|
||||
// (and the leading "-" is removed from the name). Names starting with
|
||||
// "!" are sent as the deprecated "labels" field (and the "!" is removed).
|
||||
// Names starting with a "~" prefix are sent as "labels_include_any"
|
||||
// (and the leading "~" is removed.
|
||||
addLabelsFields := func(labelNames []string) map[string][]string {
|
||||
var deprLabels, inclLabels, exclLabels []string
|
||||
var deprLabels, inclAllLabels, inclAnyLabels, exclLabels []string
|
||||
for _, lbl := range labelNames {
|
||||
if strings.HasPrefix(lbl, "-") { //nolint:gocritic // ignore ifElseChain
|
||||
switch {
|
||||
case strings.HasPrefix(lbl, "~"):
|
||||
inclAnyLabels = append(inclAnyLabels, strings.TrimPrefix(lbl, "~"))
|
||||
case strings.HasPrefix(lbl, "-"):
|
||||
exclLabels = append(exclLabels, strings.TrimPrefix(lbl, "-"))
|
||||
} else if strings.HasPrefix(lbl, "!") {
|
||||
case strings.HasPrefix(lbl, "!"):
|
||||
deprLabels = append(deprLabels, strings.TrimPrefix(lbl, "!"))
|
||||
} else {
|
||||
inclLabels = append(inclLabels, lbl)
|
||||
default:
|
||||
inclAllLabels = append(inclAllLabels, lbl)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2642,12 +2671,15 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() {
|
|||
if len(deprLabels) > 0 {
|
||||
fields["labels"] = deprLabels
|
||||
}
|
||||
if len(inclLabels) > 0 {
|
||||
fields["labels_include_all"] = inclLabels
|
||||
if len(inclAllLabels) > 0 {
|
||||
fields["labels_include_all"] = inclAllLabels
|
||||
}
|
||||
if len(exclLabels) > 0 {
|
||||
fields["labels_exclude_any"] = exclLabels
|
||||
}
|
||||
if len(inclAnyLabels) > 0 {
|
||||
fields["labels_include_any"] = inclAnyLabels
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
|
|
@ -2850,16 +2882,21 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() {
|
|||
assertWindowsProfile("win-profile-with-labels.xml", "./Test", 0, []string{"does-not-exist", "bar"}, http.StatusBadRequest, "some or all the labels provided don't exist")
|
||||
|
||||
// profiles with invalid mix of labels
|
||||
assertAppleProfile("apple-invalid-profile-with-labels.mobileconfig", "apple-invalid-profile-with-labels", "ident-with-labels", 0, []string{"foo", "!bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all" or "labels" can be included.`)
|
||||
assertAppleDeclaration("apple-invalid-decl-with-labels.json", "ident-decl-with-labels", 0, []string{"foo", "-bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all" or "labels" can be included.`)
|
||||
assertWindowsProfile("win-invalid-profile-with-labels.xml", "./Test", 0, []string{"-foo", "!bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all" or "labels" can be included.`)
|
||||
assertAppleProfile("apple-invalid-profile-with-labels.mobileconfig", "apple-invalid-profile-with-labels", "ident-with-labels", 0, []string{"foo", "!bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`)
|
||||
assertAppleProfile("apple-invalid-profile-with-labels.mobileconfig", "apple-invalid-profile-with-labels", "ident-with-labels", 0, []string{"foo", "~bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`)
|
||||
assertAppleDeclaration("apple-invalid-decl-with-labels.json", "ident-decl-with-labels", 0, []string{"foo", "-bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`)
|
||||
assertAppleDeclaration("apple-invalid-decl-with-labels.json", "ident-decl-with-labels", 0, []string{"foo", "~bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`)
|
||||
assertWindowsProfile("win-invalid-profile-with-labels.xml", "./Test", 0, []string{"-foo", "!bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`)
|
||||
assertWindowsProfile("win-invalid-profile-with-labels.xml", "./Test", 0, []string{"-foo", "~bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`)
|
||||
|
||||
// profiles with valid labels
|
||||
uuidAppleWithLabel := assertAppleProfile("apple-profile-with-labels.mobileconfig", "apple-profile-with-labels", "ident-with-labels", 0, []string{"!foo"}, http.StatusOK, "")
|
||||
uuidAppleWithInclAnyLabel := assertAppleProfile("apple-profile-with-incl-any-labels.mobileconfig", "apple-profile-with-incl-any-labels", "ident-with-incl-any-labels", 0, []string{"~foo", "~bar"}, http.StatusOK, "")
|
||||
uuidAppleDDMWithLabel := createAppleDeclaration("apple-decl-with-labels", "ident-decl-with-labels", 0, []string{"foo"})
|
||||
uuidWindowsWithLabel := assertWindowsProfile("win-profile-with-labels.xml", "./Test", 0, []string{"-foo", "-bar"}, http.StatusOK, "")
|
||||
uuidAppleDDMTeamWithLabel := createAppleDeclaration("apple-team-decl-with-labels", "ident-team-decl-with-labels", testTeam.ID, []string{"-foo"})
|
||||
uuidWindowsTeamWithLabel := assertWindowsProfile("win-team-profile-with-labels.xml", "./Test", testTeam.ID, []string{"foo", "bar"}, http.StatusOK, "")
|
||||
uuidWindowsTeamWithInclAnyLabel := assertWindowsProfile("win-team-profile-with-incl-any-labels.xml", "./Test", testTeam.ID, []string{"foo", "bar"}, http.StatusOK, "")
|
||||
|
||||
// Windows invalid content
|
||||
body, headers := generateNewProfileMultipartRequest(t, "win.xml", []byte("\x00\x01\x02"), s.token, nil)
|
||||
|
|
@ -2899,6 +2936,13 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() {
|
|||
{LabelID: labelFoo.ID, LabelName: labelFoo.Name},
|
||||
},
|
||||
},
|
||||
{
|
||||
ProfileUUID: uuidAppleWithInclAnyLabel, Platform: "darwin", Name: "apple-profile-with-incl-any-labels", Identifier: "ident-with-incl-any-labels", TeamID: nil,
|
||||
LabelsIncludeAny: []fleet.ConfigurationProfileLabel{
|
||||
{LabelID: labelBar.ID, LabelName: labelBar.Name},
|
||||
{LabelID: labelFoo.ID, LabelName: labelFoo.Name},
|
||||
},
|
||||
},
|
||||
{
|
||||
ProfileUUID: uuidWindowsWithLabel, Platform: "windows", Name: "win-profile-with-labels", TeamID: nil,
|
||||
LabelsExcludeAny: []fleet.ConfigurationProfileLabel{
|
||||
|
|
@ -2919,6 +2963,13 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() {
|
|||
{LabelID: labelFoo.ID, LabelName: labelFoo.Name},
|
||||
},
|
||||
},
|
||||
{
|
||||
ProfileUUID: uuidWindowsTeamWithInclAnyLabel, Platform: "windows", Name: "win-team-profile-with-incl-any-labels", TeamID: &testTeam.ID,
|
||||
LabelsIncludeAll: []fleet.ConfigurationProfileLabel{
|
||||
{LabelID: labelBar.ID, LabelName: labelBar.Name},
|
||||
{LabelID: labelFoo.ID, LabelName: labelFoo.Name},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, prof := range expectedProfiles {
|
||||
var getResp getMDMConfigProfileResponse
|
||||
|
|
@ -2939,6 +2990,9 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() {
|
|||
sort.Slice(getResp.LabelsExcludeAny, func(i, j int) bool {
|
||||
return getResp.LabelsExcludeAny[i].LabelName < getResp.LabelsExcludeAny[j].LabelName
|
||||
})
|
||||
sort.Slice(getResp.LabelsIncludeAny, func(i, j int) bool {
|
||||
return getResp.LabelsIncludeAny[i].LabelName < getResp.LabelsIncludeAny[j].LabelName
|
||||
})
|
||||
require.Equal(t, prof, *getResp.MDMConfigProfilePayload)
|
||||
|
||||
resp := s.Do("GET", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", prof.ProfileUUID), nil, http.StatusOK, "alt", "media")
|
||||
|
|
@ -3080,6 +3134,8 @@ func (s *integrationMDMTestSuite) TestListMDMConfigProfiles() {
|
|||
require.NoError(t, err)
|
||||
lblBar, err := s.ds.NewLabel(ctx, &fleet.Label{Name: "bar", Query: "select 1"})
|
||||
require.NoError(t, err)
|
||||
lblBaz, err := s.ds.NewLabel(ctx, &fleet.Label{Name: "baz", Query: "select 1"})
|
||||
require.NoError(t, err)
|
||||
|
||||
// create a couple profiles (Win and mac) for team 2, and none for team 3
|
||||
tprof, err := fleet.NewMDMAppleConfigProfile(mcBytesForTest("tF", "tF.identifier", "tF.uuid"), nil)
|
||||
|
|
@ -3107,19 +3163,33 @@ func (s *integrationMDMTestSuite) TestListMDMConfigProfiles() {
|
|||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// make tm2ProfH a "include-any" label-based profile
|
||||
tm2ProfH, err := s.ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{
|
||||
Name: "tH",
|
||||
TeamID: &tm2.ID,
|
||||
SyncML: []byte(`<Add></Add>`),
|
||||
LabelsIncludeAny: []fleet.ConfigurationProfileLabel{
|
||||
{LabelID: lblBar.ID, LabelName: lblBar.Name},
|
||||
{LabelID: lblBaz.ID, LabelName: lblBaz.Name},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// break lblFoo by deleting it
|
||||
require.NoError(t, s.ds.DeleteLabel(ctx, lblFoo.Name))
|
||||
|
||||
// test that all fields are correctly returned with team 2
|
||||
var listResp listMDMConfigProfilesResponse
|
||||
s.DoJSON("GET", "/api/latest/fleet/configuration_profiles", nil, http.StatusOK, &listResp, "team_id", fmt.Sprint(tm2.ID))
|
||||
require.Len(t, listResp.Profiles, 2)
|
||||
require.Len(t, listResp.Profiles, 3)
|
||||
require.NotZero(t, listResp.Profiles[0].CreatedAt)
|
||||
require.NotZero(t, listResp.Profiles[0].UploadedAt)
|
||||
require.NotZero(t, listResp.Profiles[1].CreatedAt)
|
||||
require.NotZero(t, listResp.Profiles[1].UploadedAt)
|
||||
listResp.Profiles[0].CreatedAt, listResp.Profiles[0].UploadedAt = time.Time{}, time.Time{}
|
||||
listResp.Profiles[1].CreatedAt, listResp.Profiles[1].UploadedAt = time.Time{}, time.Time{}
|
||||
listResp.Profiles[2].CreatedAt, listResp.Profiles[2].UploadedAt = time.Time{}, time.Time{}
|
||||
require.Equal(t, &fleet.MDMConfigProfilePayload{
|
||||
ProfileUUID: tm2ProfF.ProfileUUID,
|
||||
TeamID: tm2ProfF.TeamID,
|
||||
|
|
@ -3144,6 +3214,17 @@ func (s *integrationMDMTestSuite) TestListMDMConfigProfiles() {
|
|||
{LabelID: 0, LabelName: lblFoo.Name, Broken: true},
|
||||
},
|
||||
}, listResp.Profiles[1])
|
||||
require.Equal(t, &fleet.MDMConfigProfilePayload{
|
||||
ProfileUUID: tm2ProfH.ProfileUUID,
|
||||
TeamID: tm2ProfH.TeamID,
|
||||
Name: tm2ProfH.Name,
|
||||
Platform: "windows",
|
||||
// labels are ordered by name
|
||||
LabelsIncludeAny: []fleet.ConfigurationProfileLabel{
|
||||
{LabelID: lblBar.ID, LabelName: lblBar.Name},
|
||||
{LabelID: lblBaz.ID, LabelName: lblBaz.Name},
|
||||
},
|
||||
}, listResp.Profiles[2])
|
||||
|
||||
// get the specific include-all label-based profile returns the information
|
||||
var getProfResp getMDMConfigProfileResponse
|
||||
|
|
@ -3179,6 +3260,21 @@ func (s *integrationMDMTestSuite) TestListMDMConfigProfiles() {
|
|||
},
|
||||
}, getProfResp.MDMConfigProfilePayload)
|
||||
|
||||
// get the specific include-any label-based profile returns the information
|
||||
getProfResp = getMDMConfigProfileResponse{}
|
||||
s.DoJSON("GET", "/api/latest/fleet/mdm/profiles/"+tm2ProfH.ProfileUUID, nil, http.StatusOK, &getProfResp)
|
||||
getProfResp.CreatedAt, getProfResp.UploadedAt = time.Time{}, time.Time{}
|
||||
require.Equal(t, &fleet.MDMConfigProfilePayload{
|
||||
ProfileUUID: tm2ProfH.ProfileUUID,
|
||||
TeamID: tm2ProfH.TeamID,
|
||||
Name: tm2ProfH.Name,
|
||||
Platform: "windows",
|
||||
// labels are ordered by name
|
||||
LabelsIncludeAny: []fleet.ConfigurationProfileLabel{
|
||||
{LabelID: lblBar.ID, LabelName: lblBar.Name},
|
||||
{LabelID: lblBaz.ID, LabelName: lblBaz.Name},
|
||||
},
|
||||
}, getProfResp.MDMConfigProfilePayload)
|
||||
// list for a non-existing team returns 404
|
||||
s.DoJSON("GET", "/api/latest/fleet/configuration_profiles", nil, http.StatusNotFound, &listResp, "team_id", "99999")
|
||||
|
||||
|
|
@ -3228,7 +3324,7 @@ func (s *integrationMDMTestSuite) TestListMDMConfigProfiles() {
|
|||
{
|
||||
queries: []string{"per_page", "3"},
|
||||
teamID: &tm2.ID,
|
||||
wantNames: []string{"tF", "tG"},
|
||||
wantNames: []string{"tF", "tG", "tH"},
|
||||
wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: false},
|
||||
},
|
||||
{
|
||||
|
|
@ -3783,7 +3879,7 @@ func (s *integrationMDMTestSuite) TestApplyTeamsMDMWindowsProfiles() {
|
|||
}
|
||||
`), http.StatusUnprocessableEntity)
|
||||
errMsg := extractServerErrorText(res.Body)
|
||||
assert.Contains(t, errMsg, `For each profile, only one of "labels_exclude_any", "labels_include_all" or "labels" can be included.`)
|
||||
assert.Contains(t, errMsg, `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`)
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) TestBatchSetMDMProfiles() {
|
||||
|
|
@ -3984,7 +4080,7 @@ func (s *integrationMDMTestSuite) TestBatchSetMDMProfiles() {
|
|||
{Name: "N1", Contents: mobileconfigForTest("N1", "I1"), Labels: []string{lbl1.Name}, LabelsExcludeAny: []string{lbl2.Name}},
|
||||
}}, http.StatusUnprocessableEntity)
|
||||
msg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, msg, `For each profile, only one of "labels_exclude_any", "labels_include_all" or "labels" can be included.`)
|
||||
require.Contains(t, msg, `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`)
|
||||
|
||||
// successful batch-set
|
||||
s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
|
||||
|
|
@ -4885,6 +4981,134 @@ func (s *integrationMDMTestSuite) TestHostMDMProfilesExcludeLabels() {
|
|||
})
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) TestMDMProfilesIncludeAnyLabels() {
|
||||
t := s.T()
|
||||
ctx := context.Background()
|
||||
|
||||
triggerReconcileProfiles := func() {
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
// this will only mark them as "pending", as the response to confirm
|
||||
// profile deployment is asynchronous, so we simulate it here by
|
||||
// updating any "pending" (not NULL) profiles to "verifying"
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
if _, err := q.ExecContext(ctx, `UPDATE host_mdm_apple_profiles SET status = ? WHERE status = ?`, fleet.OSSettingsVerifying, fleet.OSSettingsPending); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := q.ExecContext(ctx, `UPDATE host_mdm_apple_declarations SET status = ? WHERE status = ?`, fleet.OSSettingsVerifying, fleet.OSSettingsPending); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := q.ExecContext(ctx, `UPDATE host_mdm_windows_profiles SET status = ? WHERE status = ?`, fleet.OSSettingsVerifying, fleet.OSSettingsPending); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// run the crons immediately, will create the Fleet-controlled profiles that
|
||||
// will then be expected to be applied (e.g. com.fleetdm.fleetd.config and
|
||||
// com.fleetdm.caroot)
|
||||
// first create the no-team enroll secret (required to create the fleet profiles)
|
||||
var applyResp applyEnrollSecretSpecResponse
|
||||
s.DoJSON("POST", "/api/latest/fleet/spec/enroll_secret",
|
||||
applyEnrollSecretSpecRequest{
|
||||
Spec: &fleet.EnrollSecretSpec{Secrets: []*fleet.EnrollSecret{{Secret: "super-global-secret"}}},
|
||||
}, http.StatusOK, &applyResp)
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
|
||||
// create an Apple and a Windows host
|
||||
appleHost, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t)
|
||||
windowsHost, _ := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)
|
||||
|
||||
// create a few labels, we'll use the first five for "exclude any" profiles and the remaining for "include any"
|
||||
labels := make([]*fleet.Label, 10)
|
||||
for i := 0; i < len(labels); i++ {
|
||||
label, err := s.ds.NewLabel(ctx, &fleet.Label{Name: fmt.Sprintf("label-%d", i), Query: "select 1;"})
|
||||
require.NoError(t, err)
|
||||
labels[i] = label
|
||||
}
|
||||
// simulate reporting label results for those hosts
|
||||
appleHost.LabelUpdatedAt = time.Now()
|
||||
windowsHost.LabelUpdatedAt = time.Now()
|
||||
err := s.ds.UpdateHost(ctx, appleHost)
|
||||
require.NoError(t, err)
|
||||
err = s.ds.UpdateHost(ctx, windowsHost)
|
||||
require.NoError(t, err)
|
||||
|
||||
// set up some Apple profiles and declarations and Windows profiles
|
||||
s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
|
||||
{Name: "A1", Contents: mobileconfigForTest("A1", "A1"), LabelsIncludeAny: []string{labels[0].Name, labels[1].Name}},
|
||||
{Name: "W2", Contents: syncMLForTest("./Foo/W2"), LabelsIncludeAny: []string{labels[2].Name, labels[3].Name}},
|
||||
{Name: "D3", Contents: declarationForTest("D3"), LabelsIncludeAny: []string{labels[4].Name}},
|
||||
}}, http.StatusNoContent)
|
||||
|
||||
// hosts are not members of any label yet, so running the cron applies no labels
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
|
||||
appleHost: {
|
||||
{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
|
||||
{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
|
||||
},
|
||||
})
|
||||
s.assertHostWindowsConfigProfiles(map[*fleet.Host][]fleet.HostMDMWindowsProfile{
|
||||
windowsHost: {},
|
||||
})
|
||||
|
||||
// make hosts members of labels [1], [2], [3] and [4], meaning that each of the "include any"
|
||||
// labels will now match at least one host
|
||||
err = s.ds.AsyncBatchInsertLabelMembership(ctx, [][2]uint{
|
||||
{labels[0].ID, appleHost.ID},
|
||||
{labels[1].ID, appleHost.ID},
|
||||
{labels[2].ID, appleHost.ID},
|
||||
{labels[3].ID, appleHost.ID},
|
||||
{labels[4].ID, appleHost.ID},
|
||||
{labels[1].ID, windowsHost.ID},
|
||||
{labels[2].ID, windowsHost.ID},
|
||||
{labels[3].ID, windowsHost.ID},
|
||||
{labels[4].ID, windowsHost.ID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
triggerReconcileProfiles()
|
||||
s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
|
||||
appleHost: {
|
||||
{Identifier: "A1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
|
||||
{Identifier: "D3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
|
||||
{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
|
||||
{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
|
||||
},
|
||||
})
|
||||
s.assertHostWindowsConfigProfiles(map[*fleet.Host][]fleet.HostMDMWindowsProfile{
|
||||
windowsHost: {
|
||||
{Name: "W2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
|
||||
},
|
||||
})
|
||||
|
||||
// remove membership of labels [2] for Windows, and [1] and [4] for Apple, meaning
|
||||
// that D3 will be removed on Apple, A1 will remain on Apple because the host is still a member
|
||||
// of [0], and W2 will remain on Windows because the host is still a member of [3]
|
||||
err = s.ds.AsyncBatchDeleteLabelMembership(ctx, [][2]uint{
|
||||
{labels[1].ID, appleHost.ID},
|
||||
{labels[4].ID, appleHost.ID},
|
||||
{labels[2].ID, windowsHost.ID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
|
||||
appleHost: {
|
||||
{Identifier: "A1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
|
||||
{Identifier: "D3", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
|
||||
{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
|
||||
{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
|
||||
},
|
||||
})
|
||||
s.assertHostWindowsConfigProfiles(map[*fleet.Host][]fleet.HostMDMWindowsProfile{
|
||||
windowsHost: {
|
||||
{Name: "W2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) TestOTAProfile() {
|
||||
t := s.T()
|
||||
ctx := context.Background()
|
||||
|
|
|
|||
|
|
@ -1209,6 +1209,7 @@ type newMDMConfigProfileRequest struct {
|
|||
TeamID uint
|
||||
Profile *multipart.FileHeader
|
||||
LabelsIncludeAll []string
|
||||
LabelsIncludeAny []string
|
||||
LabelsExcludeAny []string
|
||||
}
|
||||
|
||||
|
|
@ -1248,21 +1249,22 @@ func (newMDMConfigProfileRequest) DecodeRequest(ctx context.Context, r *http.Req
|
|||
}
|
||||
|
||||
// add labels
|
||||
var existsIncl, existsExcl, existsDepr bool
|
||||
var existsInclAll, existsInclAny, existsExclAny, existsDepr bool
|
||||
var deprecatedLabels []string
|
||||
decoded.LabelsIncludeAll, existsIncl = r.MultipartForm.Value["labels_include_all"]
|
||||
decoded.LabelsExcludeAny, existsExcl = r.MultipartForm.Value["labels_exclude_any"]
|
||||
decoded.LabelsIncludeAll, existsInclAll = r.MultipartForm.Value[string(fleet.LabelsIncludeAll)]
|
||||
decoded.LabelsIncludeAny, existsInclAny = r.MultipartForm.Value[string(fleet.LabelsIncludeAny)]
|
||||
decoded.LabelsExcludeAny, existsExclAny = r.MultipartForm.Value[string(fleet.LabelsExcludeAny)]
|
||||
deprecatedLabels, existsDepr = r.MultipartForm.Value["labels"]
|
||||
|
||||
// validate that only one of the labels type is provided
|
||||
var count int
|
||||
for _, b := range []bool{existsIncl, existsExcl, existsDepr} {
|
||||
for _, b := range []bool{existsInclAll, existsInclAny, existsExclAny, existsDepr} {
|
||||
if b {
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count > 1 {
|
||||
return nil, &fleet.BadRequestError{Message: `Only one of "labels_exclude_any", "labels_include_all" or "labels" can be included.`}
|
||||
return nil, &fleet.BadRequestError{Message: `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`}
|
||||
}
|
||||
if existsDepr {
|
||||
decoded.LabelsIncludeAll = deprecatedLabels
|
||||
|
|
@ -1292,17 +1294,25 @@ func newMDMConfigProfileEndpoint(ctx context.Context, request interface{}, svc f
|
|||
isMobileConfig := strings.EqualFold(fileExt, ".mobileconfig")
|
||||
isJSON := strings.EqualFold(fileExt, ".json")
|
||||
|
||||
labels := req.LabelsIncludeAll
|
||||
excludeMode := false
|
||||
if len(req.LabelsExcludeAny) > 0 {
|
||||
var labels []string
|
||||
var labelsMode fleet.MDMLabelsMode
|
||||
switch {
|
||||
case len(req.LabelsIncludeAny) > 0:
|
||||
labels = req.LabelsIncludeAny
|
||||
labelsMode = fleet.LabelsIncludeAny
|
||||
case len(req.LabelsExcludeAny) > 0:
|
||||
labels = req.LabelsExcludeAny
|
||||
excludeMode = true
|
||||
labelsMode = fleet.LabelsExcludeAny
|
||||
default:
|
||||
// default include all
|
||||
labels = req.LabelsIncludeAll
|
||||
labelsMode = fleet.LabelsIncludeAll
|
||||
}
|
||||
|
||||
if isMobileConfig || isJSON {
|
||||
// Then it's an Apple configuration file
|
||||
if isJSON {
|
||||
decl, err := svc.NewMDMAppleDeclaration(ctx, req.TeamID, ff, labels, profileName, excludeMode)
|
||||
decl, err := svc.NewMDMAppleDeclaration(ctx, req.TeamID, ff, labels, profileName, labelsMode)
|
||||
if err != nil {
|
||||
return &newMDMConfigProfileResponse{Err: err}, nil
|
||||
}
|
||||
|
|
@ -1313,7 +1323,7 @@ func newMDMConfigProfileEndpoint(ctx context.Context, request interface{}, svc f
|
|||
|
||||
}
|
||||
|
||||
cp, err := svc.NewMDMAppleConfigProfile(ctx, req.TeamID, ff, labels, excludeMode)
|
||||
cp, err := svc.NewMDMAppleConfigProfile(ctx, req.TeamID, ff, labels, labelsMode)
|
||||
if err != nil {
|
||||
return &newMDMConfigProfileResponse{Err: err}, nil
|
||||
}
|
||||
|
|
@ -1323,7 +1333,7 @@ func newMDMConfigProfileEndpoint(ctx context.Context, request interface{}, svc f
|
|||
}
|
||||
|
||||
if isWindows := strings.EqualFold(fileExt, ".xml"); isWindows {
|
||||
cp, err := svc.NewMDMWindowsConfigProfile(ctx, req.TeamID, profileName, ff, labels, excludeMode)
|
||||
cp, err := svc.NewMDMWindowsConfigProfile(ctx, req.TeamID, profileName, ff, labels, labelsMode)
|
||||
if err != nil {
|
||||
return &newMDMConfigProfileResponse{Err: err}, nil
|
||||
}
|
||||
|
|
@ -1347,7 +1357,7 @@ func (svc *Service) NewMDMUnsupportedConfigProfile(ctx context.Context, teamID u
|
|||
return &fleet.BadRequestError{Message: "Couldn't add profile. The file should be a .mobileconfig, XML, or JSON file."}
|
||||
}
|
||||
|
||||
func (svc *Service) NewMDMWindowsConfigProfile(ctx context.Context, teamID uint, profileName string, r io.Reader, labels []string, labelsExcludeMode bool) (*fleet.MDMWindowsConfigProfile, error) {
|
||||
func (svc *Service) NewMDMWindowsConfigProfile(ctx context.Context, teamID uint, profileName string, r io.Reader, labels []string, labelsMembershipMode fleet.MDMLabelsMode) (*fleet.MDMWindowsConfigProfile, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.MDMConfigProfileAuthz{TeamID: &teamID}, fleet.ActionWrite); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
|
|
@ -1396,11 +1406,16 @@ func (svc *Service) NewMDMWindowsConfigProfile(ctx context.Context, teamID uint,
|
|||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "validating labels")
|
||||
}
|
||||
if labelsExcludeMode {
|
||||
switch labelsMembershipMode {
|
||||
case fleet.LabelsIncludeAny:
|
||||
cp.LabelsIncludeAny = labelMap
|
||||
case fleet.LabelsExcludeAny:
|
||||
cp.LabelsExcludeAny = labelMap
|
||||
} else {
|
||||
default:
|
||||
// default include all
|
||||
cp.LabelsIncludeAll = labelMap
|
||||
}
|
||||
|
||||
err = validateWindowsProfileFleetVariables(string(cp.SyncML))
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "validating Windows profile")
|
||||
|
|
@ -1577,7 +1592,7 @@ func (svc *Service) BatchSetMDMProfiles(
|
|||
|
||||
labels := []string{}
|
||||
for i := range profiles {
|
||||
// from this point on (after this condition), only LabelsIncludeAll or
|
||||
// from this point on (after this condition), only LabelsIncludeAll, LabelsIncludeAny or
|
||||
// LabelsExcludeAny need to be checked.
|
||||
if len(profiles[i].Labels) > 0 {
|
||||
// must update the struct in the slice directly, because we don't have a
|
||||
|
|
@ -1586,6 +1601,7 @@ func (svc *Service) BatchSetMDMProfiles(
|
|||
profiles[i].Labels = nil
|
||||
}
|
||||
labels = append(labels, profiles[i].LabelsIncludeAll...)
|
||||
labels = append(labels, profiles[i].LabelsIncludeAny...)
|
||||
labels = append(labels, profiles[i].LabelsExcludeAny...)
|
||||
}
|
||||
labelMap, err := svc.batchValidateProfileLabels(ctx, labels)
|
||||
|
|
@ -1837,12 +1853,22 @@ func getAppleProfiles(
|
|||
|
||||
mdmDecl := fleet.NewMDMAppleDeclaration(prof.Contents, tmID, prof.Name, rawDecl.Type, rawDecl.Identifier)
|
||||
for _, labelName := range prof.LabelsIncludeAll {
|
||||
if lbl, ok := labelMap[labelName]; ok {
|
||||
declLabel := fleet.ConfigurationProfileLabel{
|
||||
LabelName: lbl.LabelName,
|
||||
LabelID: lbl.LabelID,
|
||||
RequireAll: true,
|
||||
}
|
||||
mdmDecl.LabelsIncludeAll = append(mdmDecl.LabelsIncludeAll, declLabel)
|
||||
}
|
||||
}
|
||||
for _, labelName := range prof.LabelsIncludeAny {
|
||||
if lbl, ok := labelMap[labelName]; ok {
|
||||
declLabel := fleet.ConfigurationProfileLabel{
|
||||
LabelName: lbl.LabelName,
|
||||
LabelID: lbl.LabelID,
|
||||
}
|
||||
mdmDecl.LabelsIncludeAll = append(mdmDecl.LabelsIncludeAll, declLabel)
|
||||
mdmDecl.LabelsIncludeAny = append(mdmDecl.LabelsIncludeAny, declLabel)
|
||||
}
|
||||
}
|
||||
for _, labelName := range prof.LabelsExcludeAny {
|
||||
|
|
@ -1889,12 +1915,31 @@ func getAppleProfiles(
|
|||
|
||||
for _, labelName := range prof.LabelsIncludeAll {
|
||||
if lbl, ok := labelMap[labelName]; ok {
|
||||
mdmProf.LabelsIncludeAll = append(mdmProf.LabelsIncludeAll, lbl)
|
||||
mdmLabel := fleet.ConfigurationProfileLabel{
|
||||
LabelName: lbl.LabelName,
|
||||
LabelID: lbl.LabelID,
|
||||
RequireAll: true,
|
||||
}
|
||||
mdmProf.LabelsIncludeAll = append(mdmProf.LabelsIncludeAll, mdmLabel)
|
||||
}
|
||||
}
|
||||
for _, labelName := range prof.LabelsIncludeAny {
|
||||
if lbl, ok := labelMap[labelName]; ok {
|
||||
mdmLabel := fleet.ConfigurationProfileLabel{
|
||||
LabelName: lbl.LabelName,
|
||||
LabelID: lbl.LabelID,
|
||||
}
|
||||
mdmProf.LabelsIncludeAny = append(mdmProf.LabelsIncludeAny, mdmLabel)
|
||||
}
|
||||
}
|
||||
for _, labelName := range prof.LabelsExcludeAny {
|
||||
if lbl, ok := labelMap[labelName]; ok {
|
||||
mdmProf.LabelsExcludeAny = append(mdmProf.LabelsExcludeAny, lbl)
|
||||
mdmLabel := fleet.ConfigurationProfileLabel{
|
||||
LabelName: lbl.LabelName,
|
||||
LabelID: lbl.LabelID,
|
||||
Exclude: true,
|
||||
}
|
||||
mdmProf.LabelsExcludeAny = append(mdmProf.LabelsExcludeAny, mdmLabel)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1965,12 +2010,31 @@ func getWindowsProfiles(
|
|||
}
|
||||
for _, labelName := range profile.LabelsIncludeAll {
|
||||
if lbl, ok := labelMap[labelName]; ok {
|
||||
mdmProf.LabelsIncludeAll = append(mdmProf.LabelsIncludeAll, lbl)
|
||||
mdmLabel := fleet.ConfigurationProfileLabel{
|
||||
LabelName: lbl.LabelName,
|
||||
LabelID: lbl.LabelID,
|
||||
RequireAll: true,
|
||||
}
|
||||
mdmProf.LabelsIncludeAll = append(mdmProf.LabelsIncludeAll, mdmLabel)
|
||||
}
|
||||
}
|
||||
for _, labelName := range profile.LabelsIncludeAny {
|
||||
if lbl, ok := labelMap[labelName]; ok {
|
||||
mdmLabel := fleet.ConfigurationProfileLabel{
|
||||
LabelName: lbl.LabelName,
|
||||
LabelID: lbl.LabelID,
|
||||
}
|
||||
mdmProf.LabelsIncludeAny = append(mdmProf.LabelsIncludeAny, mdmLabel)
|
||||
}
|
||||
}
|
||||
for _, labelName := range profile.LabelsExcludeAny {
|
||||
if lbl, ok := labelMap[labelName]; ok {
|
||||
mdmProf.LabelsExcludeAny = append(mdmProf.LabelsExcludeAny, lbl)
|
||||
mdmLabel := fleet.ConfigurationProfileLabel{
|
||||
LabelName: lbl.LabelName,
|
||||
LabelID: lbl.LabelID,
|
||||
Exclude: true,
|
||||
}
|
||||
mdmProf.LabelsExcludeAny = append(mdmProf.LabelsExcludeAny, mdmLabel)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2004,6 +2068,7 @@ func validateProfiles(profiles []fleet.MDMProfileBatchPayload) error {
|
|||
var count int
|
||||
for _, b := range []bool{
|
||||
len(profile.LabelsIncludeAll) > 0,
|
||||
len(profile.LabelsIncludeAny) > 0,
|
||||
len(profile.LabelsExcludeAny) > 0,
|
||||
len(profile.Labels) > 0,
|
||||
} {
|
||||
|
|
@ -2012,7 +2077,7 @@ func validateProfiles(profiles []fleet.MDMProfileBatchPayload) error {
|
|||
}
|
||||
}
|
||||
if count > 1 {
|
||||
return fleet.NewInvalidArgumentError("mdm", `Couldn't edit custom_settings. For each profile, only one of "labels_exclude_any", "labels_include_all" or "labels" can be included.`)
|
||||
return fleet.NewInvalidArgumentError("mdm", `Couldn't edit custom_settings. For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`)
|
||||
}
|
||||
|
||||
if len(profile.Contents) > 1024*1024 {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import (
|
|||
nanodep_client "github.com/fleetdm/fleet/v4/server/mdm/nanodep/client"
|
||||
nanodep_mock "github.com/fleetdm/fleet/v4/server/mock/nanodep"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/authz"
|
||||
"github.com/fleetdm/fleet/v4/server/config"
|
||||
|
|
@ -1127,11 +1128,11 @@ func TestMDMWindowsConfigProfileAuthz(t *testing.T) {
|
|||
checkShouldFail(t, err, tt.shouldFailTeamRead)
|
||||
|
||||
// test authz create new profile (no team)
|
||||
_, err = svc.NewMDMWindowsConfigProfile(ctx, 0, "prof", strings.NewReader(winProfContent), nil, false)
|
||||
_, err = svc.NewMDMWindowsConfigProfile(ctx, 0, "prof", strings.NewReader(winProfContent), nil, fleet.LabelsIncludeAll)
|
||||
checkShouldFail(t, err, tt.shouldFailGlobalWrite)
|
||||
|
||||
// test authz create new profile (team 1)
|
||||
_, err = svc.NewMDMWindowsConfigProfile(ctx, 1, "prof", strings.NewReader(winProfContent), nil, false)
|
||||
_, err = svc.NewMDMWindowsConfigProfile(ctx, 1, "prof", strings.NewReader(winProfContent), nil, fleet.LabelsIncludeAll)
|
||||
checkShouldFail(t, err, tt.shouldFailTeamWrite)
|
||||
|
||||
// test authz delete config profile (no team)
|
||||
|
|
@ -1216,7 +1217,7 @@ func TestUploadWindowsMDMConfigProfileValidations(t *testing.T) {
|
|||
}, nil
|
||||
}
|
||||
ctx = test.UserContext(ctx, test.UserAdmin)
|
||||
_, err := svc.NewMDMWindowsConfigProfile(ctx, c.tmID, "foo", strings.NewReader(c.profile), nil, false)
|
||||
_, err := svc.NewMDMWindowsConfigProfile(ctx, c.tmID, "foo", strings.NewReader(c.profile), nil, fleet.LabelsIncludeAll)
|
||||
if c.wantErr != "" {
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, c.wantErr)
|
||||
|
|
@ -1972,3 +1973,175 @@ func TestMDMResendConfigProfileAuthz(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchSetMDMProfilesLabels(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
// while the config profiles are not premium-only, teams are and we want to test with teams.
|
||||
license := &fleet.LicenseInfo{Tier: fleet.TierPremium}
|
||||
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true})
|
||||
_ = ctx
|
||||
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{
|
||||
MDM: fleet.MDM{
|
||||
EnabledAndConfigured: true,
|
||||
WindowsEnabledAndConfigured: true,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) {
|
||||
return &fleet.Team{
|
||||
ID: tid,
|
||||
Name: "team1",
|
||||
}, nil
|
||||
}
|
||||
|
||||
type ProfileLabels struct {
|
||||
IncludeAll bool
|
||||
IncludeAny bool
|
||||
ExcludeAny bool
|
||||
}
|
||||
|
||||
profileLabels := map[string]*ProfileLabels{}
|
||||
|
||||
ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDeclarations []*fleet.MDMAppleDeclaration) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
for _, profile := range macProfiles {
|
||||
profileLabels[profile.Name] = &ProfileLabels{}
|
||||
if len(profile.LabelsIncludeAll) > 0 {
|
||||
assert.True(t, profile.LabelsIncludeAll[0].RequireAll, "profile label missing RequireAll: %s", profile.Name)
|
||||
assert.False(t, profile.LabelsIncludeAll[0].Exclude, "profile label shouldn't have Exclude: %s", profile.Name)
|
||||
profileLabels[profile.Name].IncludeAll = true
|
||||
}
|
||||
if len(profile.LabelsIncludeAny) > 0 {
|
||||
assert.False(t, profile.LabelsIncludeAny[0].RequireAll, "profile label shouldn't have RequireAll: %s", profile.Name)
|
||||
assert.False(t, profile.LabelsIncludeAny[0].Exclude, "profile label shouldn't have Exclude: %s", profile.Name)
|
||||
profileLabels[profile.Name].IncludeAny = true
|
||||
}
|
||||
if len(profile.LabelsExcludeAny) > 0 {
|
||||
assert.False(t, profile.LabelsExcludeAny[0].RequireAll, "profile label shouldn't have RequireAll: %s", profile.Name)
|
||||
assert.True(t, profile.LabelsExcludeAny[0].Exclude, "profile label should have Exclude: %s", profile.Name)
|
||||
profileLabels[profile.Name].ExcludeAny = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, profile := range winProfiles {
|
||||
profileLabels[profile.Name] = &ProfileLabels{}
|
||||
if len(profile.LabelsIncludeAll) > 0 {
|
||||
assert.True(t, profile.LabelsIncludeAll[0].RequireAll, "profile label missing RequireAll: %s", profile.Name)
|
||||
assert.False(t, profile.LabelsIncludeAll[0].Exclude, "profile label shouldn't have Exclude: %s", profile.Name)
|
||||
profileLabels[profile.Name].IncludeAll = true
|
||||
}
|
||||
if len(profile.LabelsIncludeAny) > 0 {
|
||||
assert.False(t, profile.LabelsIncludeAny[0].RequireAll, "profile label shouldn't have RequireAll: %s", profile.Name)
|
||||
assert.False(t, profile.LabelsIncludeAny[0].Exclude, "profile label shouldn't have Exclude: %s", profile.Name)
|
||||
profileLabels[profile.Name].IncludeAny = true
|
||||
}
|
||||
if len(profile.LabelsExcludeAny) > 0 {
|
||||
assert.False(t, profile.LabelsExcludeAny[0].RequireAll, "profile label shouldn't have RequireAll: %s", profile.Name)
|
||||
assert.True(t, profile.LabelsExcludeAny[0].Exclude, "profile label should have Exclude: %s", profile.Name)
|
||||
profileLabels[profile.Name].ExcludeAny = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, profile := range macDeclarations {
|
||||
profileLabels[profile.Name] = &ProfileLabels{}
|
||||
if len(profile.LabelsIncludeAll) > 0 {
|
||||
assert.True(t, profile.LabelsIncludeAll[0].RequireAll, "profile label missing RequireAll: %s", profile.Name)
|
||||
assert.False(t, profile.LabelsIncludeAll[0].Exclude, "profile label shouldn't have Exclude: %s", profile.Name)
|
||||
profileLabels[profile.Name].IncludeAll = true
|
||||
}
|
||||
if len(profile.LabelsIncludeAny) > 0 {
|
||||
assert.False(t, profile.LabelsIncludeAny[0].RequireAll, "profile label shouldn't have RequireAll: %s", profile.Name)
|
||||
assert.False(t, profile.LabelsIncludeAny[0].Exclude, "profile label shouldn't have Exclude: %s", profile.Name)
|
||||
profileLabels[profile.Name].IncludeAny = true
|
||||
}
|
||||
if len(profile.LabelsExcludeAny) > 0 {
|
||||
assert.False(t, profile.LabelsExcludeAny[0].RequireAll, "profile label shouldn't have RequireAll: %s", profile.Name)
|
||||
assert.True(t, profile.LabelsExcludeAny[0].Exclude, "profile label should have Exclude: %s", profile.Name)
|
||||
profileLabels[profile.Name].ExcludeAny = true
|
||||
}
|
||||
}
|
||||
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
var labelID uint
|
||||
ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) {
|
||||
m := map[string]uint{}
|
||||
for _, label := range labels {
|
||||
labelID++
|
||||
m[label] = labelID
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
profiles := []fleet.MDMProfileBatchPayload{
|
||||
// macOS
|
||||
{
|
||||
Name: "MIncAll",
|
||||
Contents: mobileconfigForTest("MIncAll", "1"),
|
||||
LabelsIncludeAll: []string{"a", "b"},
|
||||
},
|
||||
{
|
||||
Name: "MIncAny",
|
||||
Contents: mobileconfigForTest("MIncAny", "2"),
|
||||
LabelsIncludeAny: []string{"a", "b"},
|
||||
},
|
||||
{
|
||||
Name: "MExclAny",
|
||||
Contents: mobileconfigForTest("MExclAny", "3"),
|
||||
LabelsExcludeAny: []string{"a", "b"},
|
||||
},
|
||||
// Windows
|
||||
{
|
||||
Name: "WIncAll",
|
||||
Contents: syncMLForTest("./Foo/Bar"),
|
||||
LabelsIncludeAll: []string{"a", "b"},
|
||||
},
|
||||
{
|
||||
Name: "WIncAny",
|
||||
Contents: syncMLForTest("./Foo/Barz"),
|
||||
LabelsIncludeAny: []string{"a", "b"},
|
||||
},
|
||||
{
|
||||
Name: "WExclAny",
|
||||
Contents: syncMLForTest("./Foo/Barf"),
|
||||
LabelsExcludeAny: []string{"a", "b"},
|
||||
},
|
||||
// Declarative
|
||||
{
|
||||
Name: "DIncAll",
|
||||
Contents: declarationForTest("DIncAll"),
|
||||
LabelsIncludeAll: []string{"a", "b"},
|
||||
},
|
||||
{
|
||||
Name: "DIncAny",
|
||||
Contents: declarationForTest("DIncAny"),
|
||||
LabelsIncludeAny: []string{"a", "b"},
|
||||
},
|
||||
{
|
||||
Name: "DExclAny",
|
||||
Contents: declarationForTest("DExclAny"),
|
||||
LabelsExcludeAny: []string{"a", "b"},
|
||||
},
|
||||
}
|
||||
|
||||
authCtx := test.UserContext(ctx, test.UserAdmin)
|
||||
|
||||
err := svc.BatchSetMDMProfiles(authCtx, ptr.Uint(1), nil, profiles, false, false, ptr.Bool(true))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, ProfileLabels{IncludeAll: true}, *profileLabels["MIncAll"])
|
||||
assert.Equal(t, ProfileLabels{IncludeAny: true}, *profileLabels["MIncAny"])
|
||||
assert.Equal(t, ProfileLabels{ExcludeAny: true}, *profileLabels["MExclAny"])
|
||||
|
||||
assert.Equal(t, ProfileLabels{IncludeAll: true}, *profileLabels["WIncAll"])
|
||||
assert.Equal(t, ProfileLabels{IncludeAny: true}, *profileLabels["WIncAny"])
|
||||
assert.Equal(t, ProfileLabels{ExcludeAny: true}, *profileLabels["WExclAny"])
|
||||
|
||||
assert.Equal(t, ProfileLabels{IncludeAll: true}, *profileLabels["DIncAll"])
|
||||
assert.Equal(t, ProfileLabels{IncludeAny: true}, *profileLabels["DIncAny"])
|
||||
assert.Equal(t, ProfileLabels{ExcludeAny: true}, *profileLabels["DExclAny"])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -136,6 +136,7 @@ github.com/fleetdm/fleet/v4/server/fleet/MacOSSettings CustomSettings []fleet.MD
|
|||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec Path string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec Labels []string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec LabelsIncludeAll []string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec LabelsIncludeAny []string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec LabelsExcludeAny []string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MacOSSettings DeprecatedEnableDiskEncryption *bool
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDM MacOSSetup fleet.MacOSSetup
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec Path string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec Labels []string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec LabelsIncludeAll []string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec LabelsIncludeAny []string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec LabelsExcludeAny []string
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ github.com/fleetdm/fleet/v4/server/fleet/MacOSSettings CustomSettings []fleet.MD
|
|||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec Path string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec Labels []string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec LabelsIncludeAll []string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec LabelsIncludeAny []string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec LabelsExcludeAny []string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MacOSSettings DeprecatedEnableDiskEncryption *bool
|
||||
github.com/fleetdm/fleet/v4/server/fleet/TeamMDM MacOSSetup fleet.MacOSSetup
|
||||
|
|
|
|||
Loading…
Reference in a new issue