Custom OS settings: "include any label" option for custom target (#23802)

This commit is contained in:
Sarah Gillespie 2024-11-14 11:10:49 -06:00 committed by GitHub
commit b8c9816e53
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 1799 additions and 450 deletions

View file

@ -0,0 +1,2 @@
- add UI for allowing users to install custom profiles on hosts that include any of the defined
labels

View file

@ -0,0 +1 @@
- Add support for labels_include_any to gitops

1
changes/22578-db-schema Normal file
View file

@ -0,0 +1 @@
- Adds DB support for "include any" label profile deployment

View file

@ -0,0 +1 @@
- Adds support for "include any" label/profile relationships to the profile reconciliation machinery.

View file

@ -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 {

View 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:

View 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:

View file

@ -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

View file

@ -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[];
}

View file

@ -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}
/>

View file

@ -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;
}
}
}
}

View file

@ -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&apos;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&apos;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>
);
};

View file

@ -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;
}
}
}
}

View file

@ -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) {

View file

@ -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

View file

@ -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;
}
}
}

View file

@ -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&apos;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>
);

View file

@ -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&apos;t have <b>any</b> of
these labels{" "}
Profile will only be applied to hosts that <b>don&apos;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;
};

View file

@ -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);
});

View file

@ -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{}{}

View file

@ -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)
}

View file

@ -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{}{}

View file

@ -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},
},
}

View file

@ -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)
}
}

View file

@ -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})
}
}

View file

@ -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
}

View file

@ -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

View file

@ -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

View file

@ -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"`

View file

@ -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]))
}

View file

@ -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 {

View file

@ -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) {

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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")
}

View file

@ -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
}

View file

@ -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},

View file

@ -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 != "" {

View file

@ -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()

View file

@ -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 {

View file

@ -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"])
}

View file

@ -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

View file

@ -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

View file

@ -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