mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 01:18:42 +00:00
Merge branch 'main' into mna-17401-puppet-related-integration-tests
This commit is contained in:
commit
9d878f1fd2
84 changed files with 7946 additions and 1126 deletions
|
|
@ -1,3 +1,9 @@
|
|||
## Fleet 4.47.3 (Mar 26, 2024)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Fixed a bug where valid Windows MDM enrollments would show up as unmanaged (EnrollmentState 3).
|
||||
|
||||
## Fleet 4.47.2 (Mar 22, 2024)
|
||||
|
||||
### Bug fixes
|
||||
|
|
|
|||
1
changes/17288-fix-sort-of-sql-results
Normal file
1
changes/17288-fix-sort-of-sql-results
Normal file
|
|
@ -0,0 +1 @@
|
|||
* UI fix of sql result sort for both string and numerical columns on live query results, live policy results, and query report
|
||||
1
changes/17404-mdm-custom-settings
Normal file
1
changes/17404-mdm-custom-settings
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Adds API functionality for creating DDM declarations, both individually and as a batch.
|
||||
1
changes/issue-17409-add-ddm-activities-to-ui
Normal file
1
changes/issue-17409-add-ddm-activities-to-ui
Normal file
|
|
@ -0,0 +1 @@
|
|||
- add ddm activities to the fleet UI
|
||||
1
changes/issue-17416-update-ui-to-support-ddm
Normal file
1
changes/issue-17416-update-ui-to-support-ddm
Normal file
|
|
@ -0,0 +1 @@
|
|||
- update UI to support macos DDM profiles.
|
||||
|
|
@ -8,7 +8,7 @@ version: v6.0.2
|
|||
home: https://github.com/fleetdm/fleet
|
||||
sources:
|
||||
- https://github.com/fleetdm/fleet.git
|
||||
appVersion: v4.47.2
|
||||
appVersion: v4.47.3
|
||||
dependencies:
|
||||
- name: mysql
|
||||
condition: mysql.enabled
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# All settings related to how Fleet is deployed in Kubernetes
|
||||
hostName: fleet.localhost
|
||||
replicas: 3 # The number of Fleet instances to deploy
|
||||
imageTag: v4.47.2 # Version of Fleet to deploy
|
||||
imageTag: v4.47.3 # Version of Fleet to deploy
|
||||
podAnnotations: {} # Additional annotations to add to the Fleet pod
|
||||
serviceAccountAnnotations: {} # Additional annotations to add to the Fleet service account
|
||||
resources:
|
||||
|
|
|
|||
|
|
@ -1033,6 +1033,9 @@ func newMDMProfileManager(
|
|||
schedule.WithJob("manage_apple_profiles", func(ctx context.Context) error {
|
||||
return service.ReconcileAppleProfiles(ctx, ds, commander, logger)
|
||||
}),
|
||||
schedule.WithJob("manage_apple_declarations", func(ctx context.Context) error {
|
||||
return service.ReconcileAppleDeclarations(ctx, ds, commander, logger)
|
||||
}),
|
||||
schedule.WithJob("manage_windows_profiles", func(ctx context.Context) error {
|
||||
return service.ReconcileWindowsProfiles(ctx, ds, logger)
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -462,6 +462,7 @@ the way that the Fleet server works.
|
|||
mdmStorage *mysql.NanoMDMStorage
|
||||
mdmPushService push.Pusher
|
||||
mdmCheckinAndCommandService *service.MDMAppleCheckinAndCommandService
|
||||
ddmService *service.MDMAppleDDMService
|
||||
mdmPushCertTopic string
|
||||
)
|
||||
|
||||
|
|
@ -546,6 +547,7 @@ the way that the Fleet server works.
|
|||
}
|
||||
commander := apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService)
|
||||
mdmCheckinAndCommandService = service.NewMDMAppleCheckinAndCommandService(ds, commander, logger)
|
||||
ddmService = service.NewMDMAppleDDMService(ds, logger)
|
||||
appCfg.MDM.EnabledAndConfigured = true
|
||||
}
|
||||
|
||||
|
|
@ -882,6 +884,7 @@ the way that the Fleet server works.
|
|||
scepStorage,
|
||||
logger,
|
||||
mdmCheckinAndCommandService,
|
||||
ddmService,
|
||||
); err != nil {
|
||||
initFatal(err, "setup mdm apple services")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ func TestApplyTeamSpecs(t *testing.T) {
|
|||
return nil
|
||||
}
|
||||
|
||||
ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile) error {
|
||||
ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -1110,7 +1110,7 @@ func TestApplyAsGitOps(t *testing.T) {
|
|||
teamEnrollSecrets = secrets
|
||||
return nil
|
||||
}
|
||||
ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile) error {
|
||||
ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration) error {
|
||||
return nil
|
||||
}
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string) error {
|
||||
|
|
|
|||
|
|
@ -2207,7 +2207,7 @@ func TestGetTeamsYAMLAndApply(t *testing.T) {
|
|||
}
|
||||
return nil, fmt.Errorf("team not found: %s", name)
|
||||
}
|
||||
ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile) error {
|
||||
ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration) error {
|
||||
return nil
|
||||
}
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, uuids []string) error {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ func TestBasicGlobalGitOps(t *testing.T) {
|
|||
|
||||
ds.BatchSetMDMProfilesFunc = func(
|
||||
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile,
|
||||
macDecls []*fleet.MDMAppleDeclaration,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -133,7 +134,7 @@ func TestBasicTeamGitOps(t *testing.T) {
|
|||
|
||||
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error { return nil }
|
||||
ds.BatchSetMDMProfilesFunc = func(
|
||||
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile,
|
||||
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -246,7 +247,7 @@ func TestFullGlobalGitOps(t *testing.T) {
|
|||
var appliedMacProfiles []*fleet.MDMAppleConfigProfile
|
||||
var appliedWinProfiles []*fleet.MDMWindowsConfigProfile
|
||||
ds.BatchSetMDMProfilesFunc = func(
|
||||
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile,
|
||||
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration,
|
||||
) error {
|
||||
appliedMacProfiles = macProfiles
|
||||
appliedWinProfiles = winProfiles
|
||||
|
|
@ -408,7 +409,7 @@ func TestFullTeamGitOps(t *testing.T) {
|
|||
var appliedMacProfiles []*fleet.MDMAppleConfigProfile
|
||||
var appliedWinProfiles []*fleet.MDMWindowsConfigProfile
|
||||
ds.BatchSetMDMProfilesFunc = func(
|
||||
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile,
|
||||
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration,
|
||||
) error {
|
||||
appliedMacProfiles = macProfiles
|
||||
appliedWinProfiles = winProfiles
|
||||
|
|
|
|||
4
ee/fleetd-chrome/package-lock.json
generated
4
ee/fleetd-chrome/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "fleetd-for-chrome",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "fleetd-for-chrome",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.1",
|
||||
"dependencies": {
|
||||
"dotenv": "^16.0.3",
|
||||
"wa-sqlite": "github:rhashimoto/wa-sqlite#v0.9.11"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "fleetd-for-chrome",
|
||||
"description": "Extension for Fleetd on ChromeOS",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.1",
|
||||
"dependencies": {
|
||||
"dotenv": "^16.0.3",
|
||||
"wa-sqlite": "github:rhashimoto/wa-sqlite#v0.9.11"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>
|
||||
<app appid='bfleegjcoffelppfmadimianphbcdjkb'>
|
||||
<updatecheck codebase='https://chrome-beta.fleetdm.com/fleetd.crx' version='1.2.0' />
|
||||
<updatecheck codebase='https://chrome-beta.fleetdm.com/fleetd.crx' version='1.2.1' />
|
||||
</app>
|
||||
</gupdate>
|
||||
</gupdate>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const DEFAULT_HOST_PROFILE_MOCK: IHostMdmProfile = {
|
|||
detail: "This is verified",
|
||||
};
|
||||
|
||||
export const createMockHostMacMdmProfile = (
|
||||
export const createMockHostMdmProfile = (
|
||||
overrides?: Partial<IHostMdmProfile>
|
||||
): IHostMdmProfile => {
|
||||
return { ...DEFAULT_HOST_PROFILE_MOCK, ...overrides };
|
||||
|
|
|
|||
|
|
@ -67,6 +67,9 @@ export enum ActivityType {
|
|||
LockedHost = "locked_host",
|
||||
UnlockedHost = "unlocked_host",
|
||||
WipedHost = "wiped_host",
|
||||
CreatedDeclarationProfile = "created_declaration_profile",
|
||||
DeletedDeclarationProfile = "deleted_declaration_profile",
|
||||
EditedDeclarationProfile = "edited_declaration_profile",
|
||||
}
|
||||
|
||||
// This is a subset of ActivityType that are shown only for the host past activities
|
||||
|
|
|
|||
|
|
@ -81,6 +81,11 @@ export interface IMdmProfile {
|
|||
}
|
||||
|
||||
export type MdmProfileStatus = "verified" | "verifying" | "pending" | "failed";
|
||||
export type MdmDDMProfileStatus =
|
||||
| "success"
|
||||
| "pending"
|
||||
| "failed"
|
||||
| "acknowledged";
|
||||
|
||||
export type ProfileOperationType = "remove" | "install";
|
||||
|
||||
|
|
@ -89,7 +94,7 @@ export interface IHostMdmProfile {
|
|||
name: string;
|
||||
operation_type: ProfileOperationType | null;
|
||||
platform: ProfilePlatform;
|
||||
status: MdmProfileStatus;
|
||||
status: MdmProfileStatus | MdmDDMProfileStatus;
|
||||
detail: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -763,6 +763,55 @@ const TAGGED_TEMPLATES = {
|
|||
</>
|
||||
);
|
||||
},
|
||||
createdDeclarationProfile: (activity: IActivity, isPremiumTier: boolean) => {
|
||||
return (
|
||||
<>
|
||||
{" "}
|
||||
added declaration (DDM) profile <b>
|
||||
{activity.details?.profile_name}
|
||||
</b>{" "}
|
||||
to{" "}
|
||||
{getProfileMessageSuffix(
|
||||
isPremiumTier,
|
||||
"darwin",
|
||||
activity.details?.team_name
|
||||
)}
|
||||
.
|
||||
</>
|
||||
);
|
||||
},
|
||||
deletedDeclarationProfile: (activity: IActivity, isPremiumTier: boolean) => {
|
||||
return (
|
||||
<>
|
||||
{" "}
|
||||
removed declaration (DDM) profile{" "}
|
||||
<b>{activity.details?.profile_name}</b> from{" "}
|
||||
{getProfileMessageSuffix(
|
||||
isPremiumTier,
|
||||
"darwin",
|
||||
activity.details?.team_name
|
||||
)}
|
||||
.
|
||||
</>
|
||||
);
|
||||
},
|
||||
editedDeclarationProfile: (activity: IActivity, isPremiumTier: boolean) => {
|
||||
return (
|
||||
<>
|
||||
{" "}
|
||||
edited declaration (DDM) profile <b>
|
||||
{activity.details?.profile_name}
|
||||
</b>{" "}
|
||||
for{" "}
|
||||
{getProfileMessageSuffix(
|
||||
isPremiumTier,
|
||||
"darwin",
|
||||
activity.details?.team_name
|
||||
)}{" "}
|
||||
via fleetctl.
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const getDetail = (
|
||||
|
|
@ -918,6 +967,22 @@ const getDetail = (
|
|||
case ActivityType.WipedHost: {
|
||||
return TAGGED_TEMPLATES.wipedHost(activity);
|
||||
}
|
||||
case ActivityType.CreatedDeclarationProfile: {
|
||||
return TAGGED_TEMPLATES.createdDeclarationProfile(
|
||||
activity,
|
||||
isPremiumTier
|
||||
);
|
||||
}
|
||||
case ActivityType.DeletedDeclarationProfile: {
|
||||
return TAGGED_TEMPLATES.deletedDeclarationProfile(
|
||||
activity,
|
||||
isPremiumTier
|
||||
);
|
||||
}
|
||||
case ActivityType.EditedDeclarationProfile: {
|
||||
return TAGGED_TEMPLATES.editedDeclarationProfile(activity, isPremiumTier);
|
||||
}
|
||||
|
||||
default: {
|
||||
return TAGGED_TEMPLATES.defaultActivityTemplate(activity);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ const AGGREGATE_STATUS_DISPLAY_OPTIONS: IAggregateDisplayOption[] = [
|
|||
text: "Verified",
|
||||
iconName: "success",
|
||||
tooltipText:
|
||||
"These hosts applied all OS settings. Fleet verified with osquery.",
|
||||
"These hosts applied all OS settings. Fleet verified with osquery. " +
|
||||
"Declaration profiles are verified with DDM.",
|
||||
},
|
||||
{
|
||||
value: "verifying",
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@
|
|||
flex-direction: column;
|
||||
border-radius: $border-radius;
|
||||
border: 1px solid $ui-fleet-black-10;
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto;
|
||||
|
||||
.loading-spinner {
|
||||
margin: 69.5px auto;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import FileSaver from "file-saver";
|
|||
import classnames from "classnames";
|
||||
|
||||
import { IMdmProfile } from "interfaces/mdm";
|
||||
import mdmAPI from "services/entities/mdm";
|
||||
import mdmAPI, { isDDMProfile } from "services/entities/mdm";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
import Graphic from "components/Graphic";
|
||||
|
|
@ -29,11 +29,17 @@ const LabelCount = ({
|
|||
interface IProfileDetailsProps {
|
||||
platform: string;
|
||||
createdAt: string;
|
||||
isDDM?: boolean;
|
||||
}
|
||||
|
||||
const ProfileDetails = ({ platform, createdAt }: IProfileDetailsProps) => {
|
||||
const ProfileDetails = ({
|
||||
platform,
|
||||
createdAt,
|
||||
isDDM,
|
||||
}: IProfileDetailsProps) => {
|
||||
const getPlatformName = () => {
|
||||
return platform === "darwin" ? "macOS" : "Windows";
|
||||
if (platform === "windows") return "Windows";
|
||||
return isDDM ? "macOS (declaration)" : "macOS";
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -47,6 +53,21 @@ const ProfileDetails = ({ platform, createdAt }: IProfileDetailsProps) => {
|
|||
);
|
||||
};
|
||||
|
||||
const createProfileExtension = (profile: IMdmProfile) => {
|
||||
if (isDDMProfile(profile)) {
|
||||
return "json";
|
||||
}
|
||||
return profile.platform === "darwin" ? "mobileconfig" : "xml";
|
||||
};
|
||||
|
||||
const createFileContent = async (profile: IMdmProfile) => {
|
||||
const content = await mdmAPI.downloadProfile(profile.profile_uuid);
|
||||
if (isDDMProfile(profile)) {
|
||||
return JSON.stringify(content, null, 2);
|
||||
}
|
||||
return content;
|
||||
};
|
||||
|
||||
interface IProfileListItemProps {
|
||||
isPremium: boolean;
|
||||
profile: IMdmProfile;
|
||||
|
|
@ -62,13 +83,13 @@ const ProfileListItem = ({
|
|||
onDelete,
|
||||
setProfileLabelsModalData,
|
||||
}: IProfileListItemProps) => {
|
||||
const { created_at, labels, name, platform, profile_uuid } = profile;
|
||||
const { created_at, labels, name, platform } = profile;
|
||||
const subClass = "list-item";
|
||||
|
||||
const onClickDownload = async () => {
|
||||
const fileContent = await mdmAPI.downloadProfile(profile_uuid);
|
||||
const fileContent = await createFileContent(profile);
|
||||
const formatDate = format(new Date(), "yyyy-MM-dd");
|
||||
const extension = platform === "darwin" ? "mobileconfig" : "xml";
|
||||
const extension = createProfileExtension(profile);
|
||||
const filename = `${formatDate}_${name}.${extension}`;
|
||||
const file = new File([fileContent], filename);
|
||||
FileSaver.saveAs(file);
|
||||
|
|
@ -81,7 +102,11 @@ const ProfileListItem = ({
|
|||
<div className={`${subClass}__info`}>
|
||||
<span className={`${subClass}__title`}>{name}</span>
|
||||
<div className={`${subClass}__details`}>
|
||||
<ProfileDetails platform={platform} createdAt={created_at} />
|
||||
<ProfileDetails
|
||||
platform={platform}
|
||||
createdAt={created_at}
|
||||
isDDM={isDDMProfile(profile)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import React from "react";
|
|||
import Graphic from "components/Graphic";
|
||||
|
||||
const ALLOWED_FILE_TYPES_MESSAGE =
|
||||
"Configuration profile (.mobileconfig for macOS or .xml for Windows)";
|
||||
"Configuration profile (.mobileconfig and .json for macOS or .xml for Windows)";
|
||||
|
||||
const ProfileGraphic = ({
|
||||
baseClass,
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ const FileChooser = ({
|
|||
</label>
|
||||
</Button>
|
||||
<input
|
||||
accept=".mobileconfig,application/x-apple-aspen-config,.xml"
|
||||
accept=".json,.mobileconfig,application/x-apple-aspen-config,.xml"
|
||||
id="upload-profile"
|
||||
type="file"
|
||||
onChange={(e) => {
|
||||
|
|
|
|||
|
|
@ -78,6 +78,9 @@ export const parseFile = async (file: File): Promise<[string, string]> => {
|
|||
// }
|
||||
return [name, "macOS"];
|
||||
}
|
||||
case "json": {
|
||||
return [name, "macOS"];
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Invalid file type: ${ext}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ import {
|
|||
HOST_ABOUT_DATA,
|
||||
HOST_OSQUERY_DATA,
|
||||
} from "utilities/constants";
|
||||
import { createMockHostMdmProfile } from "__mocks__/hostMock";
|
||||
|
||||
import Spinner from "components/Spinner";
|
||||
import TabsWrapper from "components/TabsWrapper";
|
||||
|
|
|
|||
|
|
@ -22,28 +22,35 @@ type OperationTypeOption = Record<
|
|||
|
||||
type ProfileDisplayConfig = Record<ProfileOperationType, OperationTypeOption>;
|
||||
|
||||
const MAC_PROFILE_VERIFIED_DISPLAY_CONFIG: ProfileDisplayOption = {
|
||||
statusText: "Verified",
|
||||
iconName: "success",
|
||||
tooltip: (innerProps) =>
|
||||
innerProps.isDiskEncryptionProfile
|
||||
? "The host turned disk encryption on and sent the key to Fleet. " +
|
||||
"Fleet verified with osquery."
|
||||
: "The host applied the setting. Fleet verified with osquery. " +
|
||||
"Declaration profiles are verified with DDM.",
|
||||
} as const;
|
||||
|
||||
const MAC_PROFILE_VERIFYING_DISPLAY_CONFIG: ProfileDisplayOption = {
|
||||
statusText: "Verifying",
|
||||
iconName: "success-outline",
|
||||
tooltip: (innerProps) =>
|
||||
innerProps.isDiskEncryptionProfile
|
||||
? "The host acknowledged the MDM command to turn on disk encryption. " +
|
||||
"Fleet is verifying with osquery and retrieving the disk encryption key. " +
|
||||
"This may take up to one hour."
|
||||
: "The host acknowledged the MDM command to apply the setting. Fleet is " +
|
||||
"verifying with osquery.",
|
||||
} as const;
|
||||
|
||||
export const PROFILE_DISPLAY_CONFIG: ProfileDisplayConfig = {
|
||||
install: {
|
||||
verified: {
|
||||
statusText: "Verified",
|
||||
iconName: "success",
|
||||
tooltip: (innerProps) =>
|
||||
innerProps.isDiskEncryptionProfile
|
||||
? "The host turned disk encryption on and sent the key to Fleet. " +
|
||||
"Fleet verified with osquery."
|
||||
: "The host applied the setting. Fleet verified with osquery.",
|
||||
},
|
||||
verifying: {
|
||||
statusText: "Verifying",
|
||||
iconName: "success-outline",
|
||||
tooltip: (innerProps) =>
|
||||
innerProps.isDiskEncryptionProfile
|
||||
? "The host acknowledged the MDM command to turn on disk encryption. " +
|
||||
"Fleet is verifying with osquery and retrieving the disk encryption key. " +
|
||||
"This may take up to one hour."
|
||||
: "The host acknowledged the MDM command to apply the setting. Fleet is " +
|
||||
"verifying with osquery.",
|
||||
},
|
||||
verified: MAC_PROFILE_VERIFIED_DISPLAY_CONFIG,
|
||||
success: MAC_PROFILE_VERIFIED_DISPLAY_CONFIG,
|
||||
verifying: MAC_PROFILE_VERIFYING_DISPLAY_CONFIG,
|
||||
acknowledged: MAC_PROFILE_VERIFYING_DISPLAY_CONFIG,
|
||||
pending: {
|
||||
statusText: "Enforcing (pending)",
|
||||
iconName: "pending-outline",
|
||||
|
|
@ -79,6 +86,8 @@ export const PROFILE_DISPLAY_CONFIG: ProfileDisplayConfig = {
|
|||
action_required: null, // should not be reached
|
||||
verified: null, // should not be reached
|
||||
verifying: null, // should not be reached
|
||||
success: null, // should not be reached
|
||||
acknowledged: null, // should not be reached
|
||||
failed: {
|
||||
statusText: "Failed",
|
||||
iconName: "error",
|
||||
|
|
@ -89,7 +98,8 @@ export const PROFILE_DISPLAY_CONFIG: ProfileDisplayConfig = {
|
|||
|
||||
type WindowsDiskEncryptionDisplayConfig = Omit<
|
||||
OperationTypeOption,
|
||||
"action_required"
|
||||
// windows disk encryption does not have these states
|
||||
"action_required" | "success" | "acknowledged"
|
||||
>;
|
||||
|
||||
export const WINDOWS_DISK_ENCRYPTION_DISPLAY_CONFIG: WindowsDiskEncryptionDisplayConfig = {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import React from "react";
|
||||
import TableContainer from "components/TableContainer";
|
||||
|
||||
import tableHeaders, { ITableRowOsSettings } from "./OSSettingsTableConfig";
|
||||
import tableHeaders, {
|
||||
IHostMdmProfileWithAddedStatus,
|
||||
} from "./OSSettingsTableConfig";
|
||||
|
||||
const baseClass = "os-settings-table";
|
||||
|
||||
interface IOSSettingsTableProps {
|
||||
tableData?: ITableRowOsSettings[];
|
||||
tableData?: IHostMdmProfileWithAddedStatus[];
|
||||
}
|
||||
|
||||
const OSSettingsTable = ({ tableData }: IOSSettingsTableProps) => {
|
||||
|
|
|
|||
|
|
@ -1,58 +1,44 @@
|
|||
import TextCell from "components/TableContainer/DataTable/TextCell";
|
||||
import React from "react";
|
||||
import { Column } from "react-table";
|
||||
|
||||
import { IStringCellProps } from "interfaces/datatable_config";
|
||||
import { IHostMdmData } from "interfaces/host";
|
||||
import {
|
||||
FLEET_FILEVAULT_PROFILE_DISPLAY_NAME,
|
||||
// FLEET_FILEVAULT_PROFILE_IDENTIFIER,
|
||||
IHostMdmProfile,
|
||||
MdmDDMProfileStatus,
|
||||
MdmProfileStatus,
|
||||
ProfilePlatform,
|
||||
isWindowsDiskEncryptionStatus,
|
||||
} from "interfaces/mdm";
|
||||
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
|
||||
|
||||
import TextCell from "components/TableContainer/DataTable/TextCell";
|
||||
import TooltipTruncatedTextCell from "components/TableContainer/DataTable/TooltipTruncatedTextCell";
|
||||
|
||||
import OSSettingStatusCell from "./OSSettingStatusCell";
|
||||
import { generateWinDiskEncryptionProfile } from "../../helpers";
|
||||
|
||||
export interface ITableRowOsSettings extends Omit<IHostMdmProfile, "status"> {
|
||||
status: OsSettingsTableStatusValue;
|
||||
}
|
||||
|
||||
export type OsSettingsTableStatusValue = MdmProfileStatus | "action_required";
|
||||
|
||||
export const isMdmProfileStatus = (
|
||||
status: string
|
||||
): status is MdmProfileStatus => {
|
||||
return status !== "action_required";
|
||||
};
|
||||
|
||||
interface IHeaderProps {
|
||||
column: {
|
||||
title: string;
|
||||
isSortedDesc: boolean;
|
||||
};
|
||||
export interface IHostMdmProfileWithAddedStatus
|
||||
extends Omit<IHostMdmProfile, "status"> {
|
||||
status: OsSettingsTableStatusValue;
|
||||
}
|
||||
|
||||
interface ICellProps {
|
||||
cell: {
|
||||
value: string;
|
||||
};
|
||||
row: {
|
||||
original: ITableRowOsSettings;
|
||||
};
|
||||
}
|
||||
type ITableColumnConfig = Column<IHostMdmProfileWithAddedStatus>;
|
||||
type ITableStringCellProps = IStringCellProps<IHostMdmProfileWithAddedStatus>;
|
||||
|
||||
interface IDataColumn {
|
||||
Header: ((props: IHeaderProps) => JSX.Element) | string;
|
||||
Cell: (props: ICellProps) => JSX.Element;
|
||||
id?: string;
|
||||
title?: string;
|
||||
accessor?: string;
|
||||
disableHidden?: boolean;
|
||||
disableSortBy?: boolean;
|
||||
sortType?: string;
|
||||
}
|
||||
export type INonDDMProfileStatus = MdmProfileStatus | "action_required";
|
||||
|
||||
export type OsSettingsTableStatusValue =
|
||||
| MdmDDMProfileStatus
|
||||
| INonDDMProfileStatus;
|
||||
|
||||
/**
|
||||
* generates the formatted tooltip for the error column.
|
||||
|
|
@ -107,22 +93,20 @@ const generateErrorTooltip = (
|
|||
return generateFormattedTooltip(detail);
|
||||
};
|
||||
|
||||
const tableHeaders: IDataColumn[] = [
|
||||
const tableHeaders: ITableColumnConfig[] = [
|
||||
{
|
||||
title: "Name",
|
||||
Header: "Name",
|
||||
disableSortBy: true,
|
||||
accessor: "name",
|
||||
Cell: (cellProps: ICellProps): JSX.Element => (
|
||||
<TextCell value={cellProps.cell.value} />
|
||||
),
|
||||
Cell: (cellProps: ITableStringCellProps) => {
|
||||
return <TextCell value={cellProps.cell.value} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Status",
|
||||
Header: "Status",
|
||||
disableSortBy: true,
|
||||
accessor: "statusText",
|
||||
Cell: (cellProps: ICellProps) => {
|
||||
accessor: "status",
|
||||
Cell: (cellProps: ITableStringCellProps) => {
|
||||
return (
|
||||
<OSSettingStatusCell
|
||||
status={cellProps.row.original.status}
|
||||
|
|
@ -133,11 +117,10 @@ const tableHeaders: IDataColumn[] = [
|
|||
},
|
||||
},
|
||||
{
|
||||
title: "Error",
|
||||
Header: "Error",
|
||||
disableSortBy: true,
|
||||
accessor: "detail",
|
||||
Cell: (cellProps: ICellProps): JSX.Element => {
|
||||
Cell: (cellProps: ITableStringCellProps): JSX.Element => {
|
||||
const profile = cellProps.row.original;
|
||||
|
||||
const value =
|
||||
|
|
@ -165,7 +148,7 @@ const tableHeaders: IDataColumn[] = [
|
|||
];
|
||||
|
||||
const makeWindowsRows = ({ profiles, os_settings }: IHostMdmData) => {
|
||||
const rows: ITableRowOsSettings[] = [];
|
||||
const rows: IHostMdmProfileWithAddedStatus[] = [];
|
||||
|
||||
if (profiles) {
|
||||
rows.push(...profiles);
|
||||
|
|
@ -190,15 +173,12 @@ const makeWindowsRows = ({ profiles, os_settings }: IHostMdmData) => {
|
|||
return rows;
|
||||
};
|
||||
|
||||
const makeDarwinRows = ({
|
||||
profiles,
|
||||
macos_settings,
|
||||
}: IHostMdmData): ITableRowOsSettings[] | null => {
|
||||
const makeDarwinRows = ({ profiles, macos_settings }: IHostMdmData) => {
|
||||
if (!profiles) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let rows: ITableRowOsSettings[] = profiles;
|
||||
let rows: IHostMdmProfileWithAddedStatus[] = profiles;
|
||||
if (macos_settings?.disk_encryption === "action_required") {
|
||||
rows = profiles.map((p) => {
|
||||
// TODO: this is a brittle check for the filevault profile
|
||||
|
|
|
|||
|
|
@ -32,7 +32,8 @@ const STATUS_DISPLAY_OPTIONS: StatusDisplayOptions = {
|
|||
Verified: {
|
||||
iconName: "success",
|
||||
tooltipText:
|
||||
"The host applied all OS settings. Fleet verified with osquery.",
|
||||
"The host applied all OS settings. Fleet verified with osquery. " +
|
||||
"Declaration profiles are verified with DDM.",
|
||||
},
|
||||
Verifying: {
|
||||
iconName: "success-outline",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
generateCSVFilename,
|
||||
generateCSVQueryResults,
|
||||
} from "utilities/generate_csv";
|
||||
import { getTableColumnsFromSql } from "utilities/helpers";
|
||||
import { IQueryReport, IQueryReportResultRow } from "interfaces/query_report";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
|
|
@ -52,9 +53,13 @@ const QueryReport = ({
|
|||
|
||||
useEffect(() => {
|
||||
if (queryReport && queryReport.results && queryReport.results.length > 0) {
|
||||
const tableColumns = getTableColumnsFromSql(lastEditedQueryBody);
|
||||
|
||||
const newColumnConfigs = generateReportColumnConfigsFromResults(
|
||||
flattenResults(queryReport.results)
|
||||
flattenResults(queryReport.results),
|
||||
tableColumns
|
||||
);
|
||||
|
||||
// Update tableHeaders if new headers are found
|
||||
if (newColumnConfigs !== columnConfigs) {
|
||||
setColumnConfigs(newColumnConfigs);
|
||||
|
|
|
|||
|
|
@ -9,10 +9,12 @@ import DefaultColumnFilter from "components/TableContainer/DataTable/DefaultColu
|
|||
import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell";
|
||||
|
||||
import {
|
||||
getSortTypeFromColumnType,
|
||||
getUniqueColumnNamesFromRows,
|
||||
humanHostLastSeen,
|
||||
internallyTruncateText,
|
||||
} from "utilities/helpers";
|
||||
import { IQueryTableColumn } from "interfaces/osquery_table";
|
||||
import { IHeaderProps, IWebSocketData } from "interfaces/datatable_config";
|
||||
|
||||
type IQueryReportTableColumnConfig = Column<IWebSocketData>;
|
||||
|
|
@ -40,7 +42,8 @@ const _unshiftHostname = (headers: IQueryReportTableColumnConfig[]) => {
|
|||
};
|
||||
|
||||
const generateReportColumnConfigsFromResults = (
|
||||
results: IWebSocketData[]
|
||||
results: IWebSocketData[],
|
||||
tableColumns?: IQueryTableColumn[] | []
|
||||
): IQueryReportTableColumnConfig[] => {
|
||||
/* Results include an array of objects, each representing a table row
|
||||
Each key value pair in an object represents a column name and value
|
||||
|
|
@ -79,7 +82,7 @@ const generateReportColumnConfigsFromResults = (
|
|||
Filter: DefaultColumnFilter, // Component hides filter for last_fetched
|
||||
filterType: "text",
|
||||
disableSortBy: false,
|
||||
sortType: "caseInsensitive",
|
||||
sortType: getSortTypeFromColumnType(key, tableColumns),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,16 +5,14 @@ import classnames from "classnames";
|
|||
import FileSaver from "file-saver";
|
||||
import { QueryContext } from "context/query";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { find } from "lodash";
|
||||
|
||||
import {
|
||||
generateCSVFilename,
|
||||
generateCSVQueryResults,
|
||||
} from "utilities/generate_csv";
|
||||
import { osqueryTables } from "utilities/osquery_tables";
|
||||
import { getTableColumnsFromSql } from "utilities/helpers";
|
||||
import { ICampaign, ICampaignError } from "interfaces/campaign";
|
||||
import { ITarget } from "interfaces/target";
|
||||
import { IQueryTableColumn } from "interfaces/osquery_table";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
import Icon from "components/Icon/Icon";
|
||||
|
|
@ -25,7 +23,6 @@ import QueryResultsHeading from "components/queries/queryResults/QueryResultsHea
|
|||
import AwaitingResults from "components/queries/queryResults/AwaitingResults";
|
||||
import InfoBanner from "components/InfoBanner";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import { checkTable } from "utilities/sql_tools";
|
||||
|
||||
import generateColumnConfigsFromRows from "./QueryResultsTableConfig";
|
||||
|
||||
|
|
@ -77,9 +74,6 @@ const QueryResults = ({
|
|||
const [queryResultsForTableRender, setQueryResultsForTableRender] = useState(
|
||||
queryResults
|
||||
);
|
||||
const [osqueryTableColumns, setOsqueryTableColumns] = useState<
|
||||
IQueryTableColumn[] | []
|
||||
>([]);
|
||||
|
||||
// immediately reset results
|
||||
const onRunAgain = useCallback(() => {
|
||||
|
|
@ -98,32 +92,20 @@ const QueryResults = ({
|
|||
debounceQueryResults(queryResults);
|
||||
}, [queryResults, debounceQueryResults]);
|
||||
|
||||
// Set table/s columns from SQL
|
||||
useEffect(() => {
|
||||
const tableNames =
|
||||
(lastEditedQueryBody && checkTable(lastEditedQueryBody).tables) || [];
|
||||
|
||||
let columns: IQueryTableColumn[] | [] = [];
|
||||
tableNames.forEach((tableName: string) => {
|
||||
const tableColumns =
|
||||
find(osqueryTables, { name: tableName })?.columns || [];
|
||||
columns = [...columns, ...tableColumns];
|
||||
});
|
||||
setOsqueryTableColumns(columns);
|
||||
}, [lastEditedQueryBody]);
|
||||
|
||||
useEffect(() => {
|
||||
if (queryResults && queryResults.length > 0) {
|
||||
const tableColumns = getTableColumnsFromSql(lastEditedQueryBody);
|
||||
|
||||
const newResultsColumnConfigs = generateColumnConfigsFromRows(
|
||||
queryResults,
|
||||
osqueryTableColumns
|
||||
tableColumns
|
||||
);
|
||||
// Update tableHeaders if new headers are found
|
||||
if (newResultsColumnConfigs !== resultsColumnConfigs) {
|
||||
setResultsColumnConfigs(newResultsColumnConfigs);
|
||||
}
|
||||
}
|
||||
}, [queryResults]); // Cannot use tableHeaders as it will cause infinite loop with setTableHeaders
|
||||
}, [queryResults, lastEditedQueryBody]); // Cannot use tableHeaders as it will cause infinite loop with setTableHeaders
|
||||
|
||||
useEffect(() => {
|
||||
if (errorColumnConfigs?.length === 0 && !!errors?.length) {
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@
|
|||
import React from "react";
|
||||
|
||||
import { CellProps, Column, HeaderProps } from "react-table";
|
||||
import { find } from "lodash";
|
||||
|
||||
import DefaultColumnFilter from "components/TableContainer/DataTable/DefaultColumnFilter";
|
||||
import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell";
|
||||
import {
|
||||
getSortTypeFromColumnType,
|
||||
getUniqueColumnNamesFromRows,
|
||||
internallyTruncateText,
|
||||
} from "utilities/helpers";
|
||||
|
|
@ -34,28 +34,11 @@ const _unshiftHostname = <T extends object>(columns: Column<T>[]) => {
|
|||
return newHeaders;
|
||||
};
|
||||
|
||||
// Sorts numerical columns correctly while perserving case insensitive sort for text columns
|
||||
const sortType = (
|
||||
colName: string | number | symbol,
|
||||
osqueryTableColumns?: IQueryTableColumn[] | []
|
||||
) => {
|
||||
if (typeof colName === "string" && !!osqueryTableColumns) {
|
||||
const numberTypes = ["integer", "bigint", "unsigned_bigint", "double"];
|
||||
|
||||
const type = find(osqueryTableColumns, { name: colName })?.type;
|
||||
|
||||
if (type && numberTypes.includes(type)) {
|
||||
return "alphanumeric";
|
||||
}
|
||||
}
|
||||
return "caseInsensitive";
|
||||
};
|
||||
|
||||
const generateColumnConfigsFromRows = <T extends Record<keyof T, unknown>>(
|
||||
// TODO - narrow typing down this entire chain of logic
|
||||
// typed as any[] to accomodate loose typing of websocket API
|
||||
results: T[], // {col:val, ...} for each row of query results
|
||||
osqueryTableColumns?: IQueryTableColumn[] | []
|
||||
tableColumns?: IQueryTableColumn[] | []
|
||||
): Column<T>[] => {
|
||||
const uniqueColumnNames = getUniqueColumnNamesFromRows(results);
|
||||
const columnsConfigs = uniqueColumnNames.map<Column<T>>((colName) => {
|
||||
|
|
@ -76,7 +59,7 @@ const generateColumnConfigsFromRows = <T extends Record<keyof T, unknown>>(
|
|||
},
|
||||
Filter: DefaultColumnFilter,
|
||||
disableSortBy: false,
|
||||
sortType: sortType(colName, osqueryTableColumns),
|
||||
sortType: getSortTypeFromColumnType(colName, tableColumns),
|
||||
};
|
||||
});
|
||||
return _unshiftHostname(columnsConfigs);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import {
|
||||
DiskEncryptionStatus,
|
||||
IHostMdmProfile,
|
||||
IMdmProfile,
|
||||
MdmProfileStatus,
|
||||
} from "interfaces/mdm";
|
||||
|
|
@ -47,6 +48,10 @@ export interface IUploadProfileApiParams {
|
|||
labels?: string[];
|
||||
}
|
||||
|
||||
export const isDDMProfile = (profile: IMdmProfile | IHostMdmProfile) => {
|
||||
return profile.profile_uuid.startsWith("d");
|
||||
};
|
||||
|
||||
interface IUpdateSetupExperienceBody {
|
||||
team_id?: number;
|
||||
enable_release_device_manually: boolean;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React from "react";
|
|||
import {
|
||||
isEmpty,
|
||||
flatMap,
|
||||
find,
|
||||
omit,
|
||||
pick,
|
||||
size,
|
||||
|
|
@ -25,6 +26,7 @@ import { buildQueryStringFromParams } from "utilities/url";
|
|||
import { IHost } from "interfaces/host";
|
||||
import { ILabel } from "interfaces/label";
|
||||
import { IPack } from "interfaces/pack";
|
||||
import { IQueryTableColumn } from "interfaces/osquery_table";
|
||||
import {
|
||||
IScheduledQuery,
|
||||
IPackQueryFormData,
|
||||
|
|
@ -39,6 +41,8 @@ import { UserRole } from "interfaces/user";
|
|||
|
||||
import stringUtils from "utilities/strings";
|
||||
import sortUtils from "utilities/sort";
|
||||
import { checkTable } from "utilities/sql_tools";
|
||||
import { osqueryTables } from "utilities/osquery_tables";
|
||||
import {
|
||||
DEFAULT_EMPTY_CELL_VALUE,
|
||||
DEFAULT_GRAVATAR_LINK,
|
||||
|
|
@ -887,6 +891,40 @@ export const getUniqueColumnNamesFromRows = <
|
|||
// can allow additional dropdown value types in the future
|
||||
type DropdownOptionValue = IDropdownOption["value"];
|
||||
|
||||
/** Generates the column schema for a sql query */
|
||||
export const getTableColumnsFromSql = (
|
||||
sql: string
|
||||
): IQueryTableColumn[] | [] => {
|
||||
const tableNames = (sql && checkTable(sql).tables) || [];
|
||||
|
||||
let sqlColumns: IQueryTableColumn[] | [] = [];
|
||||
tableNames.forEach((tableName: string) => {
|
||||
const tableColumns =
|
||||
find(osqueryTables, { name: tableName })?.columns || [];
|
||||
sqlColumns = [...sqlColumns, ...tableColumns];
|
||||
});
|
||||
// TODO: Edge case of tables sharing column names with different typing not considered
|
||||
|
||||
return sqlColumns;
|
||||
};
|
||||
|
||||
/** Sorts sql results numerical columns correctly while perserving case insensitive sort for text columns */
|
||||
export const getSortTypeFromColumnType = (
|
||||
colName: string | number | symbol,
|
||||
tableColumns?: IQueryTableColumn[] | []
|
||||
) => {
|
||||
if (typeof colName === "string") {
|
||||
const numberTypes = ["integer", "bigint", "unsigned_bigint", "double"];
|
||||
|
||||
const type = find(tableColumns, { name: colName })?.type;
|
||||
|
||||
if (type && numberTypes.includes(type)) {
|
||||
return "alphanumeric";
|
||||
}
|
||||
}
|
||||
return "caseInsensitive";
|
||||
};
|
||||
|
||||
export function getCustomDropdownOptions(
|
||||
defaultOptions: IDropdownOption[],
|
||||
customValue: DropdownOptionValue,
|
||||
|
|
@ -918,6 +956,8 @@ export default {
|
|||
generateRole,
|
||||
generateTeam,
|
||||
getUniqueColumnNamesFromRows,
|
||||
getTableColumnsFromSql,
|
||||
getSortTypeFromColumnType,
|
||||
getCustomDropdownOptions,
|
||||
greyCell,
|
||||
humanHostLastSeen,
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ variable "database_name" {
|
|||
|
||||
variable "fleet_image" {
|
||||
description = "the name of the container image to run"
|
||||
default = "fleetdm/fleet:v4.47.2"
|
||||
default = "fleetdm/fleet:v4.47.3"
|
||||
}
|
||||
|
||||
variable "software_inventory" {
|
||||
|
|
|
|||
|
|
@ -68,5 +68,5 @@ variable "redis_mem" {
|
|||
}
|
||||
|
||||
variable "image" {
|
||||
default = "fleet:v4.47.2"
|
||||
default = "fleet:v4.47.3"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -222,7 +222,7 @@ func checkWine(wineChecked bool) error {
|
|||
cmd := exec.Command(wix.WineCmd, "--version")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf(
|
||||
"%s failed. Is Wine installed? Creating a fleetd agent for Windows (.msi) requires Wine. To install Wine see the script here: https://github.com/fleetdm/fleet/blob/fleet-v4.44.0/scripts/macos-install-wine.sh %w",
|
||||
"%s failed. Is Wine installed? Creating a fleetd agent for Windows (.msi) requires Wine. To install Wine see the script here: https://fleetdm.com/install-wine %w",
|
||||
wix.WineCmd, err,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
|
@ -21,6 +22,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/scep/cryptoutil/x509util"
|
||||
|
|
@ -31,7 +33,6 @@ import (
|
|||
httptransport "github.com/go-kit/kit/transport/http"
|
||||
"github.com/google/uuid"
|
||||
"github.com/groob/plist"
|
||||
micromdm "github.com/micromdm/micromdm/mdm/mdm"
|
||||
"go.mozilla.org/pkcs7"
|
||||
)
|
||||
|
||||
|
|
@ -386,6 +387,30 @@ func (c *TestAppleMDMClient) TokenUpdate() error {
|
|||
return err
|
||||
}
|
||||
|
||||
// DeclarativeManagement sends a DeclarativeManagement checkin request to the server.
|
||||
//
|
||||
// The endpoint argument is used as the value for the `Endpoint` key in the request payload.
|
||||
//
|
||||
// For more details check https://developer.apple.com/documentation/devicemanagement/declarativemanagementrequest
|
||||
func (c *TestAppleMDMClient) DeclarativeManagement(endpoint string, data ...fleet.MDMAppleDDMStatusReport) (*http.Response, error) {
|
||||
payload := map[string]any{
|
||||
"MessageType": "DeclarativeManagement",
|
||||
"UDID": c.UUID,
|
||||
"Topic": "com.apple.mgmt.External." + c.UUID,
|
||||
"EnrollmentID": "testenrollmentid-" + c.UUID,
|
||||
"Endpoint": endpoint,
|
||||
}
|
||||
if len(data) != 0 {
|
||||
rawData, err := json.Marshal(data[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshaling status report: %w", err)
|
||||
}
|
||||
payload["Data"] = rawData
|
||||
}
|
||||
r, err := c.request("application/x-apple-aspen-mdm-checkin", payload)
|
||||
return r, err
|
||||
}
|
||||
|
||||
// Checkout sends the CheckOut message to the MDM server.
|
||||
func (c *TestAppleMDMClient) Checkout() error {
|
||||
payload := map[string]any{
|
||||
|
|
@ -404,7 +429,7 @@ func (c *TestAppleMDMClient) Checkout() error {
|
|||
// receive commands. The server can signal back with either a command to run
|
||||
// or an empty (nil, nil) response body to end the communication
|
||||
// (i.e. no commands to run).
|
||||
func (c *TestAppleMDMClient) Idle() (*micromdm.CommandPayload, error) {
|
||||
func (c *TestAppleMDMClient) Idle() (*mdm.Command, error) {
|
||||
payload := map[string]any{
|
||||
"Status": "Idle",
|
||||
"Topic": "com.apple.mgmt.External." + c.UUID,
|
||||
|
|
@ -420,7 +445,7 @@ func (c *TestAppleMDMClient) Idle() (*micromdm.CommandPayload, error) {
|
|||
// The server can signal back with either a command to run
|
||||
// or an empty (nil, nil) response body to end the communication
|
||||
// (i.e. no commands to run).
|
||||
func (c *TestAppleMDMClient) Acknowledge(cmdUUID string) (*micromdm.CommandPayload, error) {
|
||||
func (c *TestAppleMDMClient) Acknowledge(cmdUUID string) (*mdm.Command, error) {
|
||||
payload := map[string]any{
|
||||
"Status": "Acknowledged",
|
||||
"Topic": "com.apple.mgmt.External." + c.UUID,
|
||||
|
|
@ -473,7 +498,7 @@ func (c *TestAppleMDMClient) GetBootstrapToken() ([]byte, error) {
|
|||
// The server can signal back with either a command to run
|
||||
// or an empty (nil, nil) response body to end the communication
|
||||
// (i.e. no commands to run).
|
||||
func (c *TestAppleMDMClient) Err(cmdUUID string, errChain []mdm.ErrorChain) (*micromdm.CommandPayload, error) {
|
||||
func (c *TestAppleMDMClient) Err(cmdUUID string, errChain []mdm.ErrorChain) (*mdm.Command, error) {
|
||||
payload := map[string]any{
|
||||
"Status": "Error",
|
||||
"Topic": "com.apple.mgmt.External." + c.UUID,
|
||||
|
|
@ -485,7 +510,7 @@ func (c *TestAppleMDMClient) Err(cmdUUID string, errChain []mdm.ErrorChain) (*mi
|
|||
return c.sendAndDecodeCommandResponse(payload)
|
||||
}
|
||||
|
||||
func (c *TestAppleMDMClient) sendAndDecodeCommandResponse(payload map[string]any) (*micromdm.CommandPayload, error) {
|
||||
func (c *TestAppleMDMClient) sendAndDecodeCommandResponse(payload map[string]any) (*mdm.Command, error) {
|
||||
res, err := c.request("", payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request error: %w", err)
|
||||
|
|
@ -510,11 +535,12 @@ func (c *TestAppleMDMClient) sendAndDecodeCommandResponse(payload map[string]any
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("decode command: %w", err)
|
||||
}
|
||||
var p micromdm.CommandPayload
|
||||
var p mdm.Command
|
||||
err = plist.Unmarshal(cmd.Raw, &p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unmarshal command payload: %w", err)
|
||||
}
|
||||
p.Raw = cmd.Raw
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@
|
|||
|
||||
set -eo pipefail
|
||||
|
||||
# Run this script in user context (not root).
|
||||
# Reference: https://wiki.winehq.org/MacOS
|
||||
# NOTE: Wine can be installed without brew via a distribution such as https://github.com/Gcenx/macOS_Wine_builds/releases/tag/9.0, or by building from source.
|
||||
# Wine can be installed without brew via a distribution such as https://github.com/Gcenx/macOS_Wine_builds/releases/tag/9.0, or by building from source.
|
||||
|
||||
# Check if brew is installed
|
||||
if ! command -v brew >/dev/null 2>&1 ; then
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -992,6 +992,80 @@ func expectAppleProfiles(
|
|||
return m
|
||||
}
|
||||
|
||||
func expectAppleDeclarations(
|
||||
t *testing.T,
|
||||
ds *Datastore,
|
||||
tmID *uint,
|
||||
want []*fleet.MDMAppleDeclaration,
|
||||
) map[string]string {
|
||||
if tmID == nil {
|
||||
tmID = ptr.Uint(0)
|
||||
}
|
||||
|
||||
var got []*fleet.MDMAppleDeclaration
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
ctx := context.Background()
|
||||
return sqlx.SelectContext(ctx, q, &got, `SELECT * FROM mdm_apple_declarations WHERE team_id = ?`, tmID)
|
||||
})
|
||||
|
||||
// create map of expected declarations keyed by identifier
|
||||
wantMap := make(map[string]*fleet.MDMAppleDeclaration, len(want))
|
||||
for _, cp := range want {
|
||||
wantMap[cp.Identifier] = cp
|
||||
}
|
||||
|
||||
JSONRemarshal := func(bytes []byte) ([]byte, error) {
|
||||
var ifce interface{}
|
||||
err := json.Unmarshal(bytes, &ifce)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(ifce)
|
||||
}
|
||||
|
||||
// compare only the fields we care about, and build the resulting map of
|
||||
// declaration identifier as key to declaration UUID as value
|
||||
m := make(map[string]string)
|
||||
for _, gotD := range got {
|
||||
|
||||
wantD := wantMap[gotD.Identifier]
|
||||
|
||||
m[gotD.Identifier] = gotD.DeclarationUUID
|
||||
if gotD.TeamID != nil && *gotD.TeamID == 0 {
|
||||
gotD.TeamID = nil
|
||||
}
|
||||
|
||||
// DeclarationUUID is non-empty and starts with "d", but otherwise we don't
|
||||
// care about it for test assertions.
|
||||
require.NotEmpty(t, gotD.DeclarationUUID)
|
||||
require.True(t, strings.HasPrefix(gotD.DeclarationUUID, fleet.MDMAppleDeclarationUUIDPrefix))
|
||||
gotD.DeclarationUUID = ""
|
||||
gotD.Checksum = "" // don't care about md5checksum here
|
||||
|
||||
gotD.CreatedAt = time.Time{}
|
||||
|
||||
gotBytes, err := JSONRemarshal(gotD.RawJSON)
|
||||
require.NoError(t, err)
|
||||
|
||||
wantBytes, err := JSONRemarshal(wantD.RawJSON)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, wantBytes, gotBytes)
|
||||
|
||||
// if an expected uploaded_at timestamp is provided for this declaration, keep
|
||||
// its value, otherwise clear it as we don't care about asserting its
|
||||
// value.
|
||||
if wantD == nil || wantD.UploadedAt.IsZero() {
|
||||
gotD.UploadedAt = time.Time{}
|
||||
}
|
||||
|
||||
require.Equal(t, wantD.Name, gotD.Name)
|
||||
require.Equal(t, wantD.Identifier, gotD.Identifier)
|
||||
require.Equal(t, wantD.Labels, gotD.Labels)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func testBatchSetMDMAppleProfiles(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
applyAndExpect := func(newSet []*fleet.MDMAppleConfigProfile, tmID *uint, want []*fleet.MDMAppleConfigProfile) map[string]string {
|
||||
|
|
@ -1170,6 +1244,30 @@ func configProfileForTest(t *testing.T, name, identifier, uuid string, labels ..
|
|||
return cp
|
||||
}
|
||||
|
||||
func declForTest(name, identifier, payloadContent string, labels ...*fleet.Label) *fleet.MDMAppleDeclaration {
|
||||
tmpl := `{
|
||||
"Type": "com.apple.configuration.decl%s",
|
||||
"Identifier": "com.fleet.config%s",
|
||||
"Payload": {
|
||||
"ServiceType": "com.apple.service%s"
|
||||
}
|
||||
}`
|
||||
|
||||
declBytes := []byte(fmt.Sprintf(tmpl, identifier, identifier, payloadContent))
|
||||
|
||||
decl := &fleet.MDMAppleDeclaration{
|
||||
RawJSON: declBytes,
|
||||
Identifier: fmt.Sprintf("com.fleet.config%s", identifier),
|
||||
Name: name,
|
||||
}
|
||||
|
||||
for _, l := range labels {
|
||||
decl.Labels = append(decl.Labels, fleet.ConfigurationProfileLabel{LabelName: l.Name, LabelID: l.ID})
|
||||
}
|
||||
|
||||
return decl
|
||||
}
|
||||
|
||||
func teamConfigProfileForTest(t *testing.T, name, identifier, uuid string, teamID uint) *fleet.MDMAppleConfigProfile {
|
||||
prof := configProfileBytesForTest(name, identifier, uuid)
|
||||
cp, err := fleet.NewMDMAppleConfigProfile(configProfileBytesForTest(name, identifier, uuid), &teamID)
|
||||
|
|
@ -1689,13 +1787,19 @@ func testAggregateMacOSSettingsStatusWithFileVault(t *testing.T, ds *Datastore)
|
|||
expectedIDs = append(expectedIDs, h.ID)
|
||||
}
|
||||
|
||||
gotHosts, err := ds.ListHosts(ctx, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String("admin")}}, fleet.HostListOptions{MacOSSettingsFilter: status, TeamFilter: teamID})
|
||||
gotHosts, err := ds.ListHosts(
|
||||
ctx,
|
||||
fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String("admin")}},
|
||||
fleet.HostListOptions{MacOSSettingsFilter: status, TeamFilter: teamID},
|
||||
)
|
||||
gotIDs := []uint{}
|
||||
for _, h := range gotHosts {
|
||||
gotIDs = append(gotIDs, h.ID)
|
||||
}
|
||||
|
||||
return assert.NoError(t, err) && assert.Len(t, gotHosts, len(expected)) && assert.ElementsMatch(t, expectedIDs, gotIDs)
|
||||
return assert.NoError(t, err) &&
|
||||
assert.Len(t, gotHosts, len(expected)) &&
|
||||
assert.ElementsMatch(t, expectedIDs, gotIDs)
|
||||
}
|
||||
|
||||
var hosts []*fleet.Host
|
||||
|
|
@ -2517,7 +2621,7 @@ func TestMDMAppleFileVaultSummary(t *testing.T) {
|
|||
|
||||
allProfilesSummary, err = ds.GetMDMAppleProfilesSummary(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, fvProfileSummary)
|
||||
require.NotNil(t, allProfilesSummary)
|
||||
require.Equal(t, uint(2), allProfilesSummary.Pending)
|
||||
require.Equal(t, uint(0), allProfilesSummary.Failed)
|
||||
require.Equal(t, uint(1), allProfilesSummary.Verifying)
|
||||
|
|
@ -2543,7 +2647,7 @@ func TestMDMAppleFileVaultSummary(t *testing.T) {
|
|||
|
||||
allProfilesSummary, err = ds.GetMDMAppleProfilesSummary(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, fvProfileSummary)
|
||||
require.NotNil(t, allProfilesSummary)
|
||||
require.Equal(t, uint(2), allProfilesSummary.Pending)
|
||||
require.Equal(t, uint(0), allProfilesSummary.Failed)
|
||||
require.Equal(t, uint(1), allProfilesSummary.Verifying)
|
||||
|
|
@ -2571,7 +2675,7 @@ func TestMDMAppleFileVaultSummary(t *testing.T) {
|
|||
|
||||
allProfilesSummary, err = ds.GetMDMAppleProfilesSummary(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, fvProfileSummary)
|
||||
require.NotNil(t, allProfilesSummary)
|
||||
require.Equal(t, uint(2), allProfilesSummary.Pending)
|
||||
require.Equal(t, uint(0), allProfilesSummary.Failed)
|
||||
require.Equal(t, uint(1), allProfilesSummary.Verifying)
|
||||
|
|
@ -2593,7 +2697,7 @@ func TestMDMAppleFileVaultSummary(t *testing.T) {
|
|||
|
||||
allProfilesSummary, err = ds.GetMDMAppleProfilesSummary(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, fvProfileSummary)
|
||||
require.NotNil(t, allProfilesSummary)
|
||||
require.Equal(t, uint(2), allProfilesSummary.Pending)
|
||||
require.Equal(t, uint(1), allProfilesSummary.Failed)
|
||||
require.Equal(t, uint(1), allProfilesSummary.Verifying)
|
||||
|
|
@ -2615,7 +2719,7 @@ func TestMDMAppleFileVaultSummary(t *testing.T) {
|
|||
|
||||
allProfilesSummary, err = ds.GetMDMAppleProfilesSummary(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, fvProfileSummary)
|
||||
require.NotNil(t, allProfilesSummary)
|
||||
require.Equal(t, uint(3), allProfilesSummary.Pending)
|
||||
require.Equal(t, uint(1), allProfilesSummary.Failed)
|
||||
require.Equal(t, uint(1), allProfilesSummary.Verifying)
|
||||
|
|
@ -2645,7 +2749,7 @@ func TestMDMAppleFileVaultSummary(t *testing.T) {
|
|||
|
||||
allProfilesSummary, err = ds.GetMDMAppleProfilesSummary(ctx, &tm.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, fvProfileSummary)
|
||||
require.NotNil(t, allProfilesSummary)
|
||||
require.Equal(t, uint(0), allProfilesSummary.Pending)
|
||||
require.Equal(t, uint(0), allProfilesSummary.Failed)
|
||||
require.Equal(t, uint(1), allProfilesSummary.Verifying)
|
||||
|
|
@ -2671,7 +2775,7 @@ func TestMDMAppleFileVaultSummary(t *testing.T) {
|
|||
|
||||
allProfilesSummary, err = ds.GetMDMAppleProfilesSummary(ctx, &tm.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, fvProfileSummary)
|
||||
require.NotNil(t, allProfilesSummary)
|
||||
require.Equal(t, uint(0), allProfilesSummary.Pending)
|
||||
require.Equal(t, uint(0), allProfilesSummary.Failed)
|
||||
require.Equal(t, uint(0), allProfilesSummary.Verifying)
|
||||
|
|
|
|||
|
|
@ -1211,34 +1211,22 @@ func filterHostsByMacOSSettingsStatus(sql string, opt fleet.HostListOptions, par
|
|||
return sql, params, nil
|
||||
}
|
||||
|
||||
newSQL := ""
|
||||
whereStatus := ""
|
||||
// macOS settings filter is not compatible with the "all teams" option so append the "no
|
||||
// team" filter here (note that filterHostsByTeam applies the "no team" filter if TeamFilter == 0)
|
||||
if opt.TeamFilter == nil {
|
||||
// macOS settings filter is not compatible with the "all teams" option so append the "no
|
||||
// team" filter here (note that filterHostsByTeam applies the "no team" filter if TeamFilter == 0)
|
||||
newSQL += ` AND h.team_id IS NULL`
|
||||
whereStatus += ` AND h.team_id IS NULL`
|
||||
}
|
||||
|
||||
var subquery string
|
||||
var subqueryParams []any
|
||||
var err error
|
||||
switch opt.MacOSSettingsFilter {
|
||||
case fleet.OSSettingsFailed:
|
||||
subquery, subqueryParams, err = subqueryAppleProfileStatus(fleet.MDMDeliveryFailed)
|
||||
case fleet.OSSettingsPending:
|
||||
subquery, subqueryParams, err = subqueryAppleProfileStatus(fleet.MDMDeliveryPending)
|
||||
case fleet.OSSettingsVerifying:
|
||||
subquery, subqueryParams, err = subqueryAppleProfileStatus(fleet.MDMDeliveryVerifying)
|
||||
case fleet.OSSettingsVerified:
|
||||
subquery, subqueryParams, err = subqueryAppleProfileStatus(fleet.MDMDeliveryVerified)
|
||||
}
|
||||
subqueryStatus, paramsStatus, err := subqueryOSSettingsStatusMac()
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("building subquery for %s filter: %w", opt.MacOSSettingsFilter, err)
|
||||
}
|
||||
if subquery != "" {
|
||||
newSQL += fmt.Sprintf(` AND EXISTS (%s)`, subquery)
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return sql + newSQL, append(params, subqueryParams...), nil
|
||||
whereStatus += fmt.Sprintf(` AND %s = ?`, subqueryStatus)
|
||||
paramsStatus = append(paramsStatus, opt.MacOSSettingsFilter)
|
||||
|
||||
return sql + whereStatus, append(params, paramsStatus...), nil
|
||||
}
|
||||
|
||||
func filterHostsByMacOSDiskEncryptionStatus(sql string, opt fleet.HostListOptions, params []interface{}) (string, []interface{}) {
|
||||
|
|
@ -1286,30 +1274,16 @@ func (ds *Datastore) filterHostsByOSSettingsStatus(sql string, opt fleet.HostLis
|
|||
sqlFmt += ` AND h.team_id IS NULL`
|
||||
}
|
||||
var whereMacOS, whereWindows string
|
||||
sqlFmt += ` AND ((h.platform = 'windows' AND (%s)) OR (h.platform = 'darwin' AND (%s)))`
|
||||
sqlFmt += `
|
||||
AND ((h.platform = 'windows' AND (%s))
|
||||
OR (h.platform = 'darwin' AND (%s)))`
|
||||
|
||||
// construct the WHERE for macOS
|
||||
var subqueryMacOS string
|
||||
var paramsMacOS []interface{}
|
||||
var err error
|
||||
switch opt.OSSettingsFilter {
|
||||
case fleet.OSSettingsFailed:
|
||||
subqueryMacOS, paramsMacOS, err = subqueryAppleProfileStatus(fleet.MDMDeliveryFailed)
|
||||
case fleet.OSSettingsPending:
|
||||
subqueryMacOS, paramsMacOS, err = subqueryAppleProfileStatus(fleet.MDMDeliveryPending)
|
||||
case fleet.OSSettingsVerifying:
|
||||
subqueryMacOS, paramsMacOS, err = subqueryAppleProfileStatus(fleet.MDMDeliveryVerifying)
|
||||
case fleet.OSSettingsVerified:
|
||||
subqueryMacOS, paramsMacOS, err = subqueryAppleProfileStatus(fleet.MDMDeliveryVerified)
|
||||
}
|
||||
whereMacOS, paramsMacOS, err := subqueryOSSettingsStatusMac()
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("building subquery for %s filter: %w", opt.OSSettingsFilter, err)
|
||||
}
|
||||
if subqueryMacOS != "" {
|
||||
whereMacOS = "EXISTS (" + subqueryMacOS + ")"
|
||||
} else {
|
||||
whereMacOS = "FALSE"
|
||||
return "", nil, err
|
||||
}
|
||||
whereMacOS += ` = ?`
|
||||
paramsMacOS = append(paramsMacOS, opt.OSSettingsFilter)
|
||||
|
||||
// construct the WHERE for windows
|
||||
whereWindows = `hmdm.name = ? AND hmdm.enrolled = 1 AND hmdm.is_server = 0`
|
||||
|
|
|
|||
|
|
@ -364,6 +364,7 @@ func testLabelsListHostsInLabelAndStatus(t *testing.T, db *Datastore) {
|
|||
NodeKey: ptr.String("1"),
|
||||
UUID: "1",
|
||||
Hostname: "foo.local",
|
||||
Platform: "darwin",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -377,6 +378,7 @@ func testLabelsListHostsInLabelAndStatus(t *testing.T, db *Datastore) {
|
|||
NodeKey: ptr.String("2"),
|
||||
UUID: "2",
|
||||
Hostname: "bar.local",
|
||||
Platform: "darwin",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
h3, err := db.NewHost(context.Background(), &fleet.Host{
|
||||
|
|
@ -388,6 +390,7 @@ func testLabelsListHostsInLabelAndStatus(t *testing.T, db *Datastore) {
|
|||
NodeKey: ptr.String("3"),
|
||||
UUID: "3",
|
||||
Hostname: "baz.local",
|
||||
Platform: "darwin",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -427,6 +430,7 @@ func testLabelsListHostsInLabelAndTeamFilter(deferred bool, t *testing.T, db *Da
|
|||
NodeKey: ptr.String("1"),
|
||||
UUID: "1",
|
||||
Hostname: "foo.local",
|
||||
Platform: "darwin",
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
|
|
@ -440,6 +444,7 @@ func testLabelsListHostsInLabelAndTeamFilter(deferred bool, t *testing.T, db *Da
|
|||
NodeKey: ptr.String("2"),
|
||||
UUID: "2",
|
||||
Hostname: "bar.local",
|
||||
Platform: "darwin",
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
|
|
|
|||
|
|
@ -82,7 +82,6 @@ func (ds *Datastore) ListMDMCommands(
|
|||
tmFilter fleet.TeamFilter,
|
||||
listOpts *fleet.MDMCommandListOptions,
|
||||
) ([]*fleet.MDMCommand, error) {
|
||||
|
||||
jointStmt := getCombinedMDMCommandsQuery() + ds.whereFilterHostsByTeams(tmFilter, "h")
|
||||
jointStmt, params := appendListOptionsWithCursorToSQL(jointStmt, nil, &listOpts.ListOptions)
|
||||
var results []*fleet.MDMCommand
|
||||
|
|
@ -102,7 +101,7 @@ func (ds *Datastore) getMDMCommand(ctx context.Context, q sqlx.QueryerContext, c
|
|||
return &cmd, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) BatchSetMDMProfiles(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile) error {
|
||||
func (ds *Datastore) BatchSetMDMProfiles(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDeclarations []*fleet.MDMAppleDeclaration) error {
|
||||
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
||||
if err := ds.batchSetMDMWindowsProfilesDB(ctx, tx, tmID, winProfiles); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "batch set windows profiles")
|
||||
|
|
@ -112,6 +111,10 @@ func (ds *Datastore) BatchSetMDMProfiles(ctx context.Context, tmID *uint, macPro
|
|||
return ctxerr.Wrap(ctx, err, "batch set apple profiles")
|
||||
}
|
||||
|
||||
if _, err := ds.batchSetMDMAppleDeclarations(ctx, tx, tmID, macDeclarations); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "batch set apple declarations")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
@ -122,6 +125,7 @@ func (ds *Datastore) ListMDMConfigProfiles(ctx context.Context, teamID *uint, op
|
|||
|
||||
var profs []*fleet.MDMConfigProfilePayload
|
||||
|
||||
// TODO(roberto): Consider using UNION ALL here, as we know there won't be any duplicates between the tables.
|
||||
const selectStmt = `
|
||||
SELECT
|
||||
profile_uuid,
|
||||
|
|
@ -164,6 +168,20 @@ FROM (
|
|||
WHERE
|
||||
team_id = ? AND
|
||||
name NOT IN (?)
|
||||
|
||||
UNION
|
||||
|
||||
SELECT
|
||||
declaration_uuid AS profile_uuid,
|
||||
team_id,
|
||||
name,
|
||||
'darwin' AS platform,
|
||||
identifier,
|
||||
checksum AS checksum,
|
||||
created_at,
|
||||
uploaded_at
|
||||
FROM mdm_apple_declarations
|
||||
WHERE team_id = ?
|
||||
) as combined_profiles
|
||||
`
|
||||
|
||||
|
|
@ -183,7 +201,7 @@ FROM (
|
|||
fleetNames = append(fleetNames, k)
|
||||
}
|
||||
|
||||
args := []any{globalOrTeamID, fleetIdentifiers, globalOrTeamID, fleetNames}
|
||||
args := []any{globalOrTeamID, fleetIdentifiers, globalOrTeamID, fleetNames, globalOrTeamID}
|
||||
stmt, args := appendListOptionsWithCursorToSQL(selectStmt, args, &opt)
|
||||
|
||||
stmt, args, err := sqlx.In(stmt, args...)
|
||||
|
|
@ -205,15 +223,20 @@ FROM (
|
|||
}
|
||||
|
||||
// load the labels associated with those profiles
|
||||
var winProfUUIDs, macProfUUIDs []string
|
||||
var winProfUUIDs, macProfUUIDs, macDeclUUIDs []string
|
||||
for _, prof := range profs {
|
||||
if prof.Platform == "windows" {
|
||||
winProfUUIDs = append(winProfUUIDs, prof.ProfileUUID)
|
||||
} else {
|
||||
if strings.HasPrefix(prof.ProfileUUID, fleet.MDMAppleDeclarationUUIDPrefix) {
|
||||
macDeclUUIDs = append(macDeclUUIDs, prof.ProfileUUID)
|
||||
continue
|
||||
}
|
||||
|
||||
macProfUUIDs = append(macProfUUIDs, prof.ProfileUUID)
|
||||
}
|
||||
}
|
||||
labels, err := ds.listProfileLabelsForProfiles(ctx, winProfUUIDs, macProfUUIDs)
|
||||
labels, err := ds.listProfileLabelsForProfiles(ctx, winProfUUIDs, macProfUUIDs, macDeclUUIDs)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
|
@ -232,7 +255,7 @@ FROM (
|
|||
return profs, metaData, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) listProfileLabelsForProfiles(ctx context.Context, winProfUUIDs, macProfUUIDs []string) ([]fleet.ConfigurationProfileLabel, error) {
|
||||
func (ds *Datastore) listProfileLabelsForProfiles(ctx context.Context, winProfUUIDs, macProfUUIDs, macDeclUUIDs []string) ([]fleet.ConfigurationProfileLabel, error) {
|
||||
// load the labels associated with those profiles
|
||||
const labelsStmt = `
|
||||
SELECT
|
||||
|
|
@ -245,6 +268,16 @@ FROM
|
|||
WHERE
|
||||
mcpl.apple_profile_uuid IN (?) OR
|
||||
mcpl.windows_profile_uuid IN (?)
|
||||
UNION ALL
|
||||
SELECT
|
||||
apple_declaration_uuid as profile_uuid,
|
||||
label_name,
|
||||
COALESCE(label_id, 0) as label_id,
|
||||
IF(label_id IS NULL, 1, 0) as broken
|
||||
FROM
|
||||
mdm_declaration_labels mdl
|
||||
WHERE
|
||||
mdl.apple_declaration_uuid IN (?)
|
||||
ORDER BY
|
||||
profile_uuid, label_name
|
||||
`
|
||||
|
|
@ -257,8 +290,11 @@ ORDER BY
|
|||
if len(macProfUUIDs) == 0 {
|
||||
macProfUUIDs = []string{"-"}
|
||||
}
|
||||
if len(macDeclUUIDs) == 0 {
|
||||
macDeclUUIDs = []string{"-"}
|
||||
}
|
||||
|
||||
stmt, args, err := sqlx.In(labelsStmt, macProfUUIDs, winProfUUIDs)
|
||||
stmt, args, err := sqlx.In(labelsStmt, macProfUUIDs, winProfUUIDs, macDeclUUIDs)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "sqlx.In to list labels for profiles")
|
||||
}
|
||||
|
|
@ -415,6 +451,20 @@ WHERE
|
|||
return ctxerr.Wrap(ctx, err, "bulk set pending windows host profiles")
|
||||
}
|
||||
|
||||
const defaultBatchSize = 1000
|
||||
batchSize := defaultBatchSize
|
||||
if ds.testUpsertMDMDesiredProfilesBatchSize > 0 {
|
||||
batchSize = ds.testUpsertMDMDesiredProfilesBatchSize
|
||||
}
|
||||
// TODO(roberto): this method currently sets the state of all
|
||||
// declarations for all hosts. I don't see an immediate concern
|
||||
// (and my hunch is that we could even do the same for
|
||||
// profiles) but this could be optimized to use only a provided
|
||||
// set of host uuids.
|
||||
if _, err := mdmAppleBatchSetHostDeclarationStateDB(ctx, tx, batchSize, nil); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "bulk set pending apple declarations")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -734,7 +734,7 @@ WHERE
|
|||
return nil, ctxerr.Wrap(ctx, err, "get mdm windows config profile")
|
||||
}
|
||||
|
||||
labels, err := ds.listProfileLabelsForProfiles(ctx, []string{res.ProfileUUID}, nil)
|
||||
labels, err := ds.listProfileLabelsForProfiles(ctx, []string{res.ProfileUUID}, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -1495,6 +1495,8 @@ INSERT INTO
|
|||
(SELECT ?, ?, ?, ?, CURRENT_TIMESTAMP() FROM DUAL WHERE
|
||||
NOT EXISTS (
|
||||
SELECT 1 FROM mdm_apple_configuration_profiles WHERE name = ? AND team_id = ?
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1 FROM mdm_apple_declarations WHERE name = ? AND team_id = ?
|
||||
)
|
||||
)`
|
||||
|
||||
|
|
@ -1504,7 +1506,7 @@ INSERT INTO
|
|||
}
|
||||
|
||||
err := ds.withTx(ctx, func(tx sqlx.ExtContext) error {
|
||||
res, err := tx.ExecContext(ctx, insertProfileStmt, profileUUID, teamID, cp.Name, cp.SyncML, cp.Name, teamID)
|
||||
res, err := tx.ExecContext(ctx, insertProfileStmt, profileUUID, teamID, cp.Name, cp.SyncML, cp.Name, teamID, cp.Name, teamID)
|
||||
if err != nil {
|
||||
switch {
|
||||
case isDuplicate(err):
|
||||
|
|
@ -1556,6 +1558,8 @@ INSERT INTO
|
|||
(SELECT ?, ?, ?, ?, CURRENT_TIMESTAMP() FROM DUAL WHERE
|
||||
NOT EXISTS (
|
||||
SELECT 1 FROM mdm_apple_configuration_profiles WHERE name = ? AND team_id = ?
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1 FROM mdm_apple_declarations WHERE name = ? AND team_id = ?
|
||||
)
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
|
|
@ -1568,7 +1572,7 @@ ON DUPLICATE KEY UPDATE
|
|||
teamID = *cp.TeamID
|
||||
}
|
||||
|
||||
res, err := ds.writer(ctx).ExecContext(ctx, stmt, profileUUID, teamID, cp.Name, cp.SyncML, cp.Name, teamID)
|
||||
res, err := ds.writer(ctx).ExecContext(ctx, stmt, profileUUID, teamID, cp.Name, cp.SyncML, cp.Name, teamID, cp.Name, teamID)
|
||||
if err != nil {
|
||||
switch {
|
||||
case isDuplicate(err):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,135 @@
|
|||
package tables
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func init() {
|
||||
MigrationClient.AddMigration(Up_20240327115530, Down_20240327115530)
|
||||
}
|
||||
|
||||
func Up_20240327115530(tx *sql.Tx) error {
|
||||
_, err := tx.Exec(`
|
||||
CREATE TABLE mdm_apple_declarations (
|
||||
-- declaration_uuid is used as the primary key of the declaration
|
||||
declaration_uuid varchar(37) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
|
||||
|
||||
-- team_id references the team that owns this declaration
|
||||
team_id int(10) unsigned NOT NULL DEFAULT '0',
|
||||
|
||||
-- identifier is the "Identifier" field in the declaration, surfaced for convenience.
|
||||
identifier varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
|
||||
-- name is the name of the declaration
|
||||
name varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
|
||||
-- raw_json contains a JSON blob with the declaration contents
|
||||
raw_json json NOT NULL,
|
||||
|
||||
-- checksum is an MD5 checksum of the declaration, in binary form
|
||||
checksum binary(16) NOT NULL,
|
||||
|
||||
created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
uploaded_at timestamp NULL DEFAULT NULL,
|
||||
|
||||
PRIMARY KEY (declaration_uuid),
|
||||
UNIQUE KEY idx_mdm_apple_declaration_team_identifier (team_id, identifier),
|
||||
UNIQUE KEY idx_mdm_apple_declaration_team_name (team_id, name)
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating mdm_apple_declarations table: %w", err)
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`
|
||||
CREATE TABLE mdm_declaration_labels (
|
||||
-- id is used as the primary key of this table
|
||||
id int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- apple_declaration_uuid references a declaration
|
||||
apple_declaration_uuid varchar(37) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
|
||||
|
||||
|
||||
-- label name is stored here because we need to list the labels in the UI
|
||||
-- even if it has been deleted from the labels table.
|
||||
label_name varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
|
||||
-- label id is nullable in case it gets deleted from the labels table.
|
||||
-- A row in this table with label_id = null indicates the "broken" state
|
||||
label_id int(10) unsigned DEFAULT NULL,
|
||||
|
||||
created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
uploaded_at timestamp NULL DEFAULT NULL,
|
||||
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY idx_mdm_declaration_labels_label_name (apple_declaration_uuid, label_name),
|
||||
KEY label_id (label_id),
|
||||
CONSTRAINT mdm_declaration_labels_ibfk_1 FOREIGN KEY (apple_declaration_uuid) REFERENCES mdm_apple_declarations (declaration_uuid) ON DELETE CASCADE,
|
||||
CONSTRAINT mdm_declaration_labels_ibfk_3 FOREIGN KEY (label_id) REFERENCES labels (id) ON DELETE SET NULL
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating mdm_declaration_labels table: %w", err)
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`
|
||||
CREATE TABLE mdm_apple_declaration_activation_references (
|
||||
-- declaration_uuid is the declaration that contains the references
|
||||
declaration_uuid varchar(37) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
|
||||
|
||||
-- reference is the declaration_uuid of another declaration
|
||||
reference varchar(37) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
|
||||
|
||||
PRIMARY KEY (declaration_uuid, reference),
|
||||
CONSTRAINT FOREIGN KEY (declaration_uuid) REFERENCES mdm_apple_declarations (declaration_uuid) ON UPDATE CASCADE,
|
||||
CONSTRAINT FOREIGN KEY (reference) REFERENCES mdm_apple_declarations (declaration_uuid) ON UPDATE CASCADE
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating mdm_apple_declaration_activation_references table: %w", err)
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`
|
||||
CREATE TABLE host_mdm_apple_declarations (
|
||||
-- host_uuid references a host in the hosts table
|
||||
host_uuid varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
|
||||
-- status represents the status of the declaration in the host
|
||||
status varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
|
||||
-- operation_type is used to signal if the declaration is being added, removed, etc
|
||||
operation_type varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
|
||||
-- detail contains any messages or errors from the protocol or Fleet
|
||||
detail text COLLATE utf8mb4_unicode_ci,
|
||||
|
||||
-- checksum of the currently implemented declaration
|
||||
checksum binary(16) NOT NULL,
|
||||
|
||||
-- declaration_uuid references the declaration assigned to the host's team
|
||||
declaration_uuid varchar(37) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
|
||||
|
||||
-- declaration_identifier is the identifier of the declaration
|
||||
declaration_identifier varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
|
||||
-- declaration_name is the name of the declaration
|
||||
declaration_name varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
|
||||
|
||||
PRIMARY KEY (host_uuid, declaration_uuid),
|
||||
KEY status (status),
|
||||
KEY operation_type (operation_type),
|
||||
CONSTRAINT host_mdm_apple_declarations_ibfk_1 FOREIGN KEY (status) REFERENCES mdm_delivery_status (status) ON UPDATE CASCADE,
|
||||
CONSTRAINT host_mdm_apple_declarations_ibfk_2 FOREIGN KEY (operation_type) REFERENCES mdm_operation_types (operation_type) ON UPDATE CASCADE
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creatign host_mdm_apple_declarations table %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Down_20240327115530(tx *sql.Tx) error {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package tables
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func init() {
|
||||
MigrationClient.AddMigration(Up_20240327115617, Down_20240327115617)
|
||||
}
|
||||
|
||||
func Up_20240327115617(tx *sql.Tx) error {
|
||||
_, err := tx.Exec(`
|
||||
CREATE TABLE mdm_apple_declarative_requests (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
enrollment_id VARCHAR(255) NOT NULL,
|
||||
-- Should be one of "tokens", "declaration-items", "status", or "declaration/…/…" where the ellipses reference a declaration on the server
|
||||
message_type VARCHAR(255) NOT NULL,
|
||||
-- json payload
|
||||
raw_json TEXT,
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT mdm_apple_declarative_requests_enrollment_id FOREIGN KEY (enrollment_id) REFERENCES nano_enrollments (id) ON DELETE CASCADE
|
||||
)
|
||||
`)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating mdm_apple_declarative_requsts: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Down_20240327115617(tx *sql.Tx) error {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1281,3 +1281,57 @@ func (ds *Datastore) optimisticGetOrInsert(ctx context.Context, readStmt, insert
|
|||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// batchProcessDB abstracts the batch processing logic, for a given payload:
|
||||
//
|
||||
// - generateValueArgs will get called for each item, the expected return values are:
|
||||
// - a string containing the placeholders for each item in the batch
|
||||
// - a slice of arguments containing one item for each placeholder
|
||||
//
|
||||
// - executeBatch will get called on each batch to perform the operation in the db
|
||||
//
|
||||
// TODO(roberto): use this function in all the functions where we do ad-hoc
|
||||
// batch processing.
|
||||
func batchProcessDB[T any](
|
||||
payload []T,
|
||||
batchSize int,
|
||||
generateValueArgs func(T) (string, []any),
|
||||
executeBatch func(string, []any) error,
|
||||
) error {
|
||||
if len(payload) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
args []any
|
||||
sb strings.Builder
|
||||
batchCount int
|
||||
)
|
||||
|
||||
resetBatch := func() {
|
||||
batchCount = 0
|
||||
args = args[:0]
|
||||
sb.Reset()
|
||||
}
|
||||
|
||||
for _, item := range payload {
|
||||
valuePart, itemArgs := generateValueArgs(item)
|
||||
args = append(args, itemArgs...)
|
||||
sb.WriteString(valuePart)
|
||||
batchCount++
|
||||
|
||||
if batchCount >= batchSize {
|
||||
if err := executeBatch(sb.String(), args); err != nil {
|
||||
return err
|
||||
}
|
||||
resetBatch()
|
||||
}
|
||||
}
|
||||
|
||||
if batchCount > 0 {
|
||||
if err := executeBatch(sb.String(), args); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1229,3 +1229,55 @@ func TestWhereFilterGlobalOrTeamIDByTeams(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchProcessDB(t *testing.T) {
|
||||
type testData struct {
|
||||
id int
|
||||
value string
|
||||
}
|
||||
|
||||
payload := []interface{}{
|
||||
&testData{id: 1, value: "a"},
|
||||
&testData{id: 2, value: "b"},
|
||||
&testData{id: 3, value: "c"},
|
||||
}
|
||||
|
||||
generateValueArgs := func(item interface{}) (string, []any) {
|
||||
p := item.(*testData)
|
||||
valuePart := "(?, ?),"
|
||||
args := []any{p.id, p.value}
|
||||
return valuePart, args
|
||||
}
|
||||
|
||||
t.Run("TestEmptyPayload", func(t *testing.T) {
|
||||
executeBatch := func(valuePart string, args []any) error {
|
||||
return errors.New("execute shouldn't be called for an empty payload")
|
||||
}
|
||||
err := batchProcessDB([]interface{}{}, 1000, generateValueArgs, executeBatch)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("TestSingleBatch", func(t *testing.T) {
|
||||
callCount := 0
|
||||
executeBatch := func(valuePart string, args []any) error {
|
||||
callCount++
|
||||
require.Equal(t, 2, len(args)/2) // each item adds 2 args
|
||||
return nil
|
||||
}
|
||||
err := batchProcessDB(payload[:2], 2, generateValueArgs, executeBatch)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, callCount)
|
||||
})
|
||||
|
||||
t.Run("TestMultipleBatches", func(t *testing.T) {
|
||||
callCount := 0
|
||||
executeBatch := func(valuePart string, args []any) error {
|
||||
callCount++
|
||||
require.Equal(t, 2/callCount, len(args)/2) // each item adds 2 args
|
||||
return nil
|
||||
}
|
||||
err := batchProcessDB(payload, 2, generateValueArgs, executeBatch)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, callCount)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -313,11 +313,12 @@ func TruncateTables(t testing.TB, ds *Datastore, tables ...string) {
|
|||
// be truncated - a more precise approach must be used for those, e.g.
|
||||
// delete where id > max before test, or something like that.
|
||||
nonEmptyTables := map[string]bool{
|
||||
"app_config_json": true,
|
||||
"migration_status_tables": true,
|
||||
"osquery_options": true,
|
||||
"mdm_delivery_status": true,
|
||||
"mdm_operation_types": true,
|
||||
"app_config_json": true,
|
||||
"migration_status_tables": true,
|
||||
"osquery_options": true,
|
||||
"mdm_delivery_status": true,
|
||||
"mdm_operation_types": true,
|
||||
"mdm_apple_declaration_categories": true,
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
|
|
|
|||
|
|
@ -84,6 +84,9 @@ var ActivityDetailsList = []ActivityDetails{
|
|||
ActivityTypeLockedHost{},
|
||||
ActivityTypeUnlockedHost{},
|
||||
ActivityTypeWipedHost{},
|
||||
|
||||
ActivityTypeCreatedDeclarationProfile{},
|
||||
ActivityTypeDeletedDeclarationProfile{},
|
||||
}
|
||||
|
||||
type ActivityDetails interface {
|
||||
|
|
@ -1317,6 +1320,56 @@ func (a ActivityTypeWipedHost) Documentation() (activity, details, detailsExampl
|
|||
}`
|
||||
}
|
||||
|
||||
type ActivityTypeCreatedDeclarationProfile struct {
|
||||
ProfileName string `json:"profile_name"`
|
||||
Identifier string `json:"identifier"`
|
||||
TeamID *uint `json:"team_id"`
|
||||
TeamName *string `json:"team_name"`
|
||||
}
|
||||
|
||||
func (a ActivityTypeCreatedDeclarationProfile) ActivityName() string {
|
||||
return "created_declaration_profile"
|
||||
}
|
||||
|
||||
func (a ActivityTypeCreatedDeclarationProfile) Documentation() (activity string, details string, detailsExample string) {
|
||||
return `Generated when a user adds a new macOS declaration to a team (or no team).`,
|
||||
`This activity contains the following fields:
|
||||
- "profile_name": Name of the declaration.
|
||||
- "identifier": Identifier of the declaration.
|
||||
- "team_id": The ID of the team that the declaration applies to, ` + "`null`" + ` if it applies to devices that are not in a team.
|
||||
- "team_name": The name of the team that the declaration applies to, ` + "`null`" + ` if it applies to devices that are not in a team.`, `{
|
||||
"profile_name": "Passcode requirements",
|
||||
"profile_identifier": "com.my.declaration",
|
||||
"team_id": 123,
|
||||
"team_name": "Workstations"
|
||||
}`
|
||||
}
|
||||
|
||||
type ActivityTypeDeletedDeclarationProfile struct {
|
||||
ProfileName string `json:"profile_name"`
|
||||
Identifier string `json:"identifier"`
|
||||
TeamID *uint `json:"team_id"`
|
||||
TeamName *string `json:"team_name"`
|
||||
}
|
||||
|
||||
func (a ActivityTypeDeletedDeclarationProfile) ActivityName() string {
|
||||
return "deleted_declaration_profile"
|
||||
}
|
||||
|
||||
func (a ActivityTypeDeletedDeclarationProfile) Documentation() (activity string, details string, detailsExample string) {
|
||||
return `Generated when a user removes a macOS declaration from a team (or no team).`,
|
||||
`This activity contains the following fields:
|
||||
- "profile_name": Name of the declaration.
|
||||
- "identifier": Identifier of the declaration.
|
||||
- "team_id": The ID of the team that the declaration applies to, ` + "`null`" + ` if it applies to devices that are not in a team.
|
||||
- "team_name": The name of the team that the declaration applies to, ` + "`null`" + ` if it applies to devices that are not in a team.`, `{
|
||||
"profile_name": "Passcode requirements",
|
||||
"profile_identifier": "com.my.declaration",
|
||||
"team_id": 123,
|
||||
"team_name": "Workstations"
|
||||
}`
|
||||
}
|
||||
|
||||
// LogRoleChangeActivities logs activities for each role change, globally and one for each change in teams.
|
||||
func LogRoleChangeActivities(ctx context.Context, ds Datastore, adminUser *User, oldGlobalRole *string, oldTeamRoles []UserTeam, user *User) error {
|
||||
if user.GlobalRole != nil && (oldGlobalRole == nil || *oldGlobalRole != *user.GlobalRole) {
|
||||
|
|
|
|||
|
|
@ -451,6 +451,8 @@ const (
|
|||
DEPAssignProfileResponseFailed DEPAssignProfileResponseStatus = "FAILED"
|
||||
)
|
||||
|
||||
const MDMAppleDeclarationUUIDPrefix = "d"
|
||||
|
||||
// NanoEnrollment represents a row in the nano_enrollments table managed by
|
||||
// nanomdm. It is meant to be used internally by the server, not to be returned
|
||||
// as part of endpoints, and as a precaution its json-encoding is explicitly
|
||||
|
|
@ -533,3 +535,287 @@ type SCEPIdentityAssociation struct {
|
|||
EnrollReference string `db:"enroll_reference"`
|
||||
RenewCommandUUID string `db:"renew_command_uuid"`
|
||||
}
|
||||
|
||||
// MDMAppleDeclaration represents a DDM JSON declaration.
|
||||
type MDMAppleDeclaration struct {
|
||||
// DeclarationUUID is the unique identifier of the declaration in
|
||||
// Fleet. Since we use the same endpoints for declarations and profiles:
|
||||
// - This is marshalled as profile_uuid
|
||||
// - The value has a prefix (TODO: @jahzielv to determine and document this)
|
||||
DeclarationUUID string `db:"declaration_uuid" json:"profile_uuid"`
|
||||
|
||||
// TeamID is the id of the team with which the declaration is associated. A nil team id
|
||||
// represents a declaration that is not associated with any team.
|
||||
TeamID *uint `db:"team_id" json:"team_id"`
|
||||
|
||||
// Identifier corresponds to the "Identifier" key of the associated declaration.
|
||||
// Fleet requires that Identifier must be unique in combination with the Name and TeamID.
|
||||
Identifier string `db:"identifier" json:"identifier"`
|
||||
|
||||
// Name corresponds to the file name of the associated JSON declaration payload.
|
||||
// Fleet requires that Name must be unique in combination with the Identifier and TeamID.
|
||||
Name string `db:"name" json:"name"`
|
||||
|
||||
// RawJSON is the raw JSON content of the declaration
|
||||
RawJSON json.RawMessage `db:"raw_json" json:"-"`
|
||||
|
||||
// Checksum is a checksum of the JSON contents
|
||||
Checksum string `db:"checksum" json:"-"`
|
||||
|
||||
// Labels are the labels associated with this Declaration
|
||||
Labels []ConfigurationProfileLabel `db:"labels" json:"labels,omitempty"`
|
||||
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UploadedAt time.Time `db:"uploaded_at" json:"uploaded_at"`
|
||||
}
|
||||
|
||||
type MDMAppleRawDeclaration struct {
|
||||
// Type is the "Type" field on the raw declaration JSON.
|
||||
Type string `json:"Type"`
|
||||
Identifier string `json:"Identifier"`
|
||||
}
|
||||
|
||||
// ForbiddenDeclTypes is a set of declaration types that are not allowed to be
|
||||
// added by users into Fleet.
|
||||
var ForbiddenDeclTypes = map[string]struct{}{
|
||||
"com.apple.configuration.account.caldav": {},
|
||||
"com.apple.configuration.account.carddav": {},
|
||||
"com.apple.configuration.account.exchange": {},
|
||||
"com.apple.configuration.account.google": {},
|
||||
"com.apple.configuration.account.ldap": {},
|
||||
"com.apple.configuration.account.mail": {},
|
||||
"com.apple.configuration.screensharing.connection": {},
|
||||
"com.apple.configuration.security.certificate": {},
|
||||
"com.apple.configuration.security.identity": {},
|
||||
"com.apple.configuration.security.passkey.attestation": {},
|
||||
"com.apple.configuration.services.configuration-files": {},
|
||||
"com.apple.configuration.watch.enrollment": {},
|
||||
}
|
||||
|
||||
func (r *MDMAppleRawDeclaration) ValidateUserProvided() error {
|
||||
var err error
|
||||
|
||||
// Check against types we don't allow
|
||||
if r.Type == `com.apple.configuration.softwareupdate.enforcement.specific` {
|
||||
return NewInvalidArgumentError(r.Type, "Declaration profile can’t include OS updates settings. To control these settings, go to OS updates.")
|
||||
}
|
||||
|
||||
if _, forbidden := ForbiddenDeclTypes[r.Type]; forbidden {
|
||||
return NewInvalidArgumentError(r.Type, "Only configuration declarations that don’t require an asset reference are supported.")
|
||||
}
|
||||
|
||||
if r.Type == "com.apple.configuration.management.status-subscriptions" {
|
||||
return NewInvalidArgumentError(r.Type, "Declaration profile can’t include status subscription type. To get host’s vitals, please use queries and policies.")
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(r.Type, "com.apple.configuration") {
|
||||
return NewInvalidArgumentError(r.Type, "Only configuration declarations (com.apple.configuration) are supported.")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func GetRawDeclarationValues(raw []byte) (*MDMAppleRawDeclaration, error) {
|
||||
var rawDecl MDMAppleRawDeclaration
|
||||
if err := json.Unmarshal(raw, &rawDecl); err != nil {
|
||||
return nil, NewInvalidArgumentError("declaration", fmt.Sprintf("Couldn't upload. The file should include valid JSON: %s", err)).WithStatus(http.StatusBadRequest)
|
||||
}
|
||||
|
||||
return &rawDecl, nil
|
||||
}
|
||||
|
||||
// MDMAppleHostDeclaration represents the state of a declaration on a host
|
||||
type MDMAppleHostDeclaration struct {
|
||||
// HostUUID is the uuid of the host affected by this declaration
|
||||
HostUUID string `db:"host_uuid" json:"-"`
|
||||
|
||||
// DeclarationUUID is the unique identifier of the declaration in
|
||||
// Fleet. Since we use the same endpoints for declarations and profiles:
|
||||
// - This is marshalled as profile_uuid
|
||||
// - The value has a prefix (TODO: @jahzielv to determine and document this)
|
||||
DeclarationUUID string `db:"declaration_uuid" json:"profile_uuid"`
|
||||
|
||||
// Name corresponds to the file name of the associated JSON declaration payload.
|
||||
Name string `db:"declaration_name" json:"name"`
|
||||
|
||||
// Identifier corresponds to the "Identifier" key of the associated declaration.
|
||||
Identifier string `db:"declaration_identifier" json:"-"`
|
||||
|
||||
// Status represent the current state of the declaration, as known by the Fleet server.
|
||||
Status *MDMDeliveryStatus `db:"status" json:"status"`
|
||||
|
||||
// Operation type represents the operation being performed.
|
||||
OperationType MDMOperationType `db:"operation_type" json:"operation_type"`
|
||||
|
||||
// Detail contains any messages that must be surfaced to the user,
|
||||
// either by the MDM protocol or the Fleet server.
|
||||
Detail string `db:"detail" json:"detail"`
|
||||
|
||||
// Checksum contains the MD5 checksum of the declaration JSON uploaded
|
||||
// by the IT admin. Fleet uses this value as the ServerToken.
|
||||
Checksum string `db:"checksum" json:"-"`
|
||||
}
|
||||
|
||||
func NewMDMAppleDeclaration(raw []byte, teamID *uint, name string, declType, ident string) *MDMAppleDeclaration {
|
||||
var decl MDMAppleDeclaration
|
||||
|
||||
decl.Identifier = ident
|
||||
decl.Name = name
|
||||
decl.RawJSON = raw
|
||||
decl.TeamID = teamID
|
||||
|
||||
return &decl
|
||||
}
|
||||
|
||||
// MDMAppleDDMTokensResponse is the response from the DDM tokens endpoint.
|
||||
//
|
||||
// https://developer.apple.com/documentation/devicemanagement/tokensresponse
|
||||
type MDMAppleDDMTokensResponse struct {
|
||||
SyncTokens MDMAppleDDMDeclarationsToken
|
||||
}
|
||||
|
||||
// MDMAppleDDMDeclarationsToken is dictionary describes the state of declarations on the server.
|
||||
//
|
||||
// https://developer.apple.com/documentation/devicemanagement/synchronizationtokens
|
||||
type MDMAppleDDMDeclarationsToken struct {
|
||||
DeclarationsToken string `db:"checksum"`
|
||||
Timestamp time.Time `db:"latest_created_timestamp"`
|
||||
}
|
||||
|
||||
// MDMAppleDDMDeclarationItemsResponse is the response from the DDM declaration items endpoint.
|
||||
//
|
||||
// https://developer.apple.com/documentation/devicemanagement/declarationitemsresponse
|
||||
type MDMAppleDDMDeclarationItemsResponse struct {
|
||||
Declarations MDMAppleDDMManifestItems
|
||||
DeclarationsToken string
|
||||
}
|
||||
|
||||
// MDMAppleDDMManifestItems is a dictionary that contains the lists of declarations available on the
|
||||
// server.
|
||||
//
|
||||
// https://developer.apple.com/documentation/devicemanagement/declarationitemsresponse/manifestdeclarationitems
|
||||
type MDMAppleDDMManifestItems struct {
|
||||
Activations []MDMAppleDDMManifest
|
||||
Assets []MDMAppleDDMManifest
|
||||
Configurations []MDMAppleDDMManifest
|
||||
Management []MDMAppleDDMManifest
|
||||
}
|
||||
|
||||
// MDMAppleDDMManifest is a dictionary that describes a declaration.
|
||||
//
|
||||
// https://developer.apple.com/documentation/devicemanagement/declarationitemsresponse/manifestdeclarationitems
|
||||
type MDMAppleDDMManifest struct {
|
||||
Identifier string
|
||||
ServerToken string
|
||||
}
|
||||
|
||||
// MDMAppleDDMDeclarationItem represents a declaration item in the datastore. It is used to
|
||||
// construct the DDM `declaration-items` endpoint response.
|
||||
//
|
||||
// https://developer.apple.com/documentation/devicemanagement/declarationitemsresponse
|
||||
type MDMAppleDDMDeclarationItem struct {
|
||||
Identifier string `db:"identifier"`
|
||||
ServerToken string `db:"checksum"`
|
||||
}
|
||||
|
||||
// MDMAppleDDMDeclarationResponse represents a declaration in the datastore. It is used for the DDM
|
||||
// `declaration/.../...` enpoint response.
|
||||
//
|
||||
// https://developer.apple.com/documentation/devicemanagement/declarationresponse
|
||||
type MDMAppleDDMDeclarationResponse struct {
|
||||
Identifier string `db:"identifier"`
|
||||
Type string `db:"type"`
|
||||
Payload json.RawMessage `db:"payload"`
|
||||
ServerToken string `db:"server_token"`
|
||||
}
|
||||
|
||||
// MDMAppleDDMStatusReport represents a report of the device's current state.
|
||||
//
|
||||
// https://developer.apple.com/documentation/devicemanagement/statusreport
|
||||
type MDMAppleDDMStatusReport struct {
|
||||
StatusItems MDMAppleDDMStatusItems `json:"StatusItems"`
|
||||
Errors []MDMAppleDDMErrors `json:"Errors"`
|
||||
}
|
||||
|
||||
// MDMAppleDDMStatusItems are the status items for a report.
|
||||
//
|
||||
// https://developer.apple.com/documentation/devicemanagement/statusreport/statusitems
|
||||
type MDMAppleDDMStatusItems struct {
|
||||
Management MDMAppleDDMStatusManagement `json:"management"`
|
||||
}
|
||||
|
||||
// MDMAppleDDMStatusManagement represents status report of the client's
|
||||
// processed declarations.
|
||||
//
|
||||
// https://developer.apple.com/documentation/devicemanagement/statusmanagementdeclarations
|
||||
type MDMAppleDDMStatusManagement struct {
|
||||
Declarations MDMAppleDDMStatusDeclarations `json:"declarations"`
|
||||
}
|
||||
|
||||
// MDMAppleDDMStatusDeclarations represents a collection of the client's
|
||||
// processed declarations.
|
||||
//
|
||||
// https://developer.apple.com/documentation/devicemanagement/statusmanagementdeclarationsdeclarationsobject
|
||||
type MDMAppleDDMStatusDeclarations struct {
|
||||
// Activations is an array of declarations that represent the client's
|
||||
// processed activation types.
|
||||
Activations []MDMAppleDDMStatusDeclaration `json:"activations"`
|
||||
// Configurations is an array of declarations that represent the
|
||||
// client's processed configuration types.
|
||||
Configurations []MDMAppleDDMStatusDeclaration `json:"configurations"`
|
||||
// Assets is an array of declarations that represent the client's
|
||||
// processed assets.
|
||||
Assets []MDMAppleDDMStatusDeclaration `json:"assets"`
|
||||
// Management is an array of declarations that represent the client's
|
||||
// processed declaration types.
|
||||
Management []MDMAppleDDMStatusDeclaration `json:"management"`
|
||||
}
|
||||
|
||||
type MDMAppleDeclarationValidity string
|
||||
|
||||
const (
|
||||
MDMAppleDeclarationValid MDMAppleDeclarationValidity = "valid"
|
||||
MDMAppleDeclarationInvalid MDMAppleDeclarationValidity = "invalid"
|
||||
MDMAppleDeclarationUnknown MDMAppleDeclarationValidity = "valid"
|
||||
)
|
||||
|
||||
// MDMAppleDDMStatusDeclaration represents a processed declaration for the client.
|
||||
//
|
||||
// https://developer.apple.com/documentation/devicemanagement/statusmanagementdeclarationsdeclarationobject
|
||||
type MDMAppleDDMStatusDeclaration struct {
|
||||
// Active signals if the declaration is active on the device.
|
||||
Active bool `json:"active"`
|
||||
// Identifier is the identifier of the declaration this status report refers to.
|
||||
Identifier string `json:"identifier"`
|
||||
// Valid defines the validity of the declaration. If it's invalid, the
|
||||
// reasons property contains more details.
|
||||
Valid MDMAppleDeclarationValidity `json:"valid"`
|
||||
// ServerToken of the declaration this status report refers to.
|
||||
ServerToken string `json:"server-token"`
|
||||
// Reasons are the details of any client errors.
|
||||
Reasons []MDMAppleDDMStatusErrorReason `json:"reasons,omitempty"`
|
||||
}
|
||||
|
||||
// A status report's error that contains the status item and the reasons for
|
||||
// the error.
|
||||
//
|
||||
// https://developer.apple.com/documentation/devicemanagement/statusreport/error
|
||||
type MDMAppleDDMErrors struct {
|
||||
// StatusItem is the status item that this error pertains to.
|
||||
StatusItem string `json:"StatusItem"`
|
||||
// Reasons is an array of reasons for the error.
|
||||
Reasons []MDMAppleDDMStatusErrorReason `json:"Reasons"`
|
||||
}
|
||||
|
||||
// A status report that contains details about an error.
|
||||
//
|
||||
// https://developer.apple.com/documentation/devicemanagement/statusreason
|
||||
type MDMAppleDDMStatusErrorReason struct {
|
||||
// Code is the error code for this error.
|
||||
Code string `json:"Code"`
|
||||
// Description is a short error description.
|
||||
Description string `json:"Description"`
|
||||
// Details is a dictionary that contains further details about this
|
||||
// error.
|
||||
Details map[string]any `json:"Details"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -927,6 +927,9 @@ type Datastore interface {
|
|||
// profile uuid.
|
||||
GetMDMAppleConfigProfile(ctx context.Context, profileUUID string) (*MDMAppleConfigProfile, error)
|
||||
|
||||
// GetMDMAppleDeclaration returns the declaration corresponding to the specified uuid.
|
||||
GetMDMAppleDeclaration(ctx context.Context, declUUID string) (*MDMAppleDeclaration, error)
|
||||
|
||||
// ListMDMAppleConfigProfiles lists mdm config profiles associated with the specified team id.
|
||||
// For global config profiles, specify nil as the team id.
|
||||
ListMDMAppleConfigProfiles(ctx context.Context, teamID *uint) ([]*MDMAppleConfigProfile, error)
|
||||
|
|
@ -1169,6 +1172,29 @@ type Datastore interface {
|
|||
// serials.
|
||||
UpdateDEPAssignProfileRetryPending(ctx context.Context, jobID uint, serials []string) error
|
||||
|
||||
// InsertMDMAppleDDMRequest inserts a DDM request.
|
||||
InsertMDMAppleDDMRequest(ctx context.Context, hostUUID, messageType, rawJSON string) error
|
||||
|
||||
// MDMAppleDDMDeclarationsToken returns the token used to synchronize declarations for the
|
||||
// specified host UUID.
|
||||
MDMAppleDDMDeclarationsToken(ctx context.Context, hostUUID string) (*MDMAppleDDMDeclarationsToken, error)
|
||||
// MDMAppleDDMDeclarationItems returns the declaration items for the specified host UUID.
|
||||
MDMAppleDDMDeclarationItems(ctx context.Context, hostUUID string) ([]MDMAppleDDMDeclarationItem, error)
|
||||
// MDMAppleDDMDeclarationPayload returns the declaration payload for the specified identifier and team.
|
||||
MDMAppleDDMDeclarationsResponse(ctx context.Context, identifier string, hostUUID string) (*MDMAppleDeclaration, error)
|
||||
//MDMAppleBatchSetHostDeclarationState
|
||||
MDMAppleBatchSetHostDeclarationState(ctx context.Context) ([]string, error)
|
||||
// MDMAppleStoreDDMStatusReport receives a host.uuid and a slice
|
||||
// of declarations, and updates the tracked host declaration status for
|
||||
// matching declarations.
|
||||
//
|
||||
// It also takes care of cleaning up all host declarations that are
|
||||
// pending removal.
|
||||
MDMAppleStoreDDMStatusReport(ctx context.Context, hostUUID string, updates []*MDMAppleHostDeclaration) error
|
||||
// MDMAppleSetDeclarationsAsVerifying updates all
|
||||
// ("pending", "install") declarations for a host to be ("verifying", "install")
|
||||
MDMAppleSetDeclarationsAsVerifying(ctx context.Context, hostUUID string) error
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Microsoft MDM
|
||||
|
||||
|
|
@ -1287,7 +1313,10 @@ type Datastore interface {
|
|||
|
||||
// BatchSetMDMProfiles sets the MDM Apple or Windows profiles for the given team or
|
||||
// no team in a single transaction.
|
||||
BatchSetMDMProfiles(ctx context.Context, tmID *uint, macProfiles []*MDMAppleConfigProfile, winProfiles []*MDMWindowsConfigProfile) error
|
||||
BatchSetMDMProfiles(ctx context.Context, tmID *uint, macProfiles []*MDMAppleConfigProfile, winProfiles []*MDMWindowsConfigProfile, macDeclarations []*MDMAppleDeclaration) error
|
||||
|
||||
// NewMDMAppleDeclaration creates and returns a new MDM Apple declaration.
|
||||
NewMDMAppleDeclaration(ctx context.Context, declaration *MDMAppleDeclaration) (*MDMAppleDeclaration, error)
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Host Script Results
|
||||
|
|
|
|||
|
|
@ -413,6 +413,24 @@ func NewMDMConfigProfilePayloadFromApple(cp *MDMAppleConfigProfile) *MDMConfigPr
|
|||
}
|
||||
}
|
||||
|
||||
func NewMDMConfigProfilePayloadFromAppleDDM(decl *MDMAppleDeclaration) *MDMConfigProfilePayload {
|
||||
var tid *uint
|
||||
if decl.TeamID != nil && *decl.TeamID > 0 {
|
||||
tid = decl.TeamID
|
||||
}
|
||||
return &MDMConfigProfilePayload{
|
||||
ProfileUUID: decl.DeclarationUUID,
|
||||
TeamID: tid,
|
||||
Name: decl.Name,
|
||||
Identifier: decl.Identifier,
|
||||
Platform: "darwin",
|
||||
Checksum: []byte(decl.Checksum),
|
||||
CreatedAt: decl.CreatedAt,
|
||||
UploadedAt: decl.UploadedAt,
|
||||
Labels: decl.Labels,
|
||||
}
|
||||
}
|
||||
|
||||
// MDMProfileSpec represents the spec used to define configuration
|
||||
// profiles via yaml files.
|
||||
type MDMProfileSpec struct {
|
||||
|
|
|
|||
|
|
@ -652,18 +652,29 @@ type Service interface {
|
|||
|
||||
// NewMDMAppleConfigProfile creates a new configuration profile for the specified team.
|
||||
NewMDMAppleConfigProfile(ctx context.Context, teamID uint, r io.Reader, labels []string) (*MDMAppleConfigProfile, error)
|
||||
// NewMDMAppleConfigProfileWithPayload creates a new declaration for the specified team.
|
||||
NewMDMAppleDeclaration(ctx context.Context, teamID uint, r io.Reader, labels []string, name string) (*MDMAppleDeclaration, error)
|
||||
|
||||
// GetMDMAppleConfigProfileByDeprecatedID retrieves the specified Apple
|
||||
// configuration profile via its numeric ID. This method is deprecated and
|
||||
// should not be used for new endpoints.
|
||||
GetMDMAppleConfigProfileByDeprecatedID(ctx context.Context, profileID uint) (*MDMAppleConfigProfile, error)
|
||||
// GetMDMAppleConfigProfile retrieves the specified configuration profile.
|
||||
GetMDMAppleConfigProfile(ctx context.Context, profileUUID string) (*MDMAppleConfigProfile, error)
|
||||
|
||||
// GetMDMAppleDeclaration retrieves the specified declaration.
|
||||
GetMDMAppleDeclaration(ctx context.Context, declarationUUID string) (*MDMAppleDeclaration, error)
|
||||
|
||||
// DeleteMDMAppleConfigProfileByDeprecatedID deletes the specified Apple
|
||||
// configuration profile via its numeric ID. This method is deprecated and
|
||||
// should not be used for new endpoints.
|
||||
DeleteMDMAppleConfigProfileByDeprecatedID(ctx context.Context, profileID uint) error
|
||||
// DeleteMDMAppleConfigProfile deletes the specified configuration profile.
|
||||
DeleteMDMAppleConfigProfile(ctx context.Context, profileUUID string) error
|
||||
|
||||
// DeleteMDMAppleDeclaration deletes the specified declaration.
|
||||
DeleteMDMAppleDeclaration(ctx context.Context, declarationUUID string) error
|
||||
|
||||
// ListMDMAppleConfigProfiles returns the list of all the configuration profiles for the
|
||||
// specified team.
|
||||
ListMDMAppleConfigProfiles(ctx context.Context, teamID uint) ([]*MDMAppleConfigProfile, error)
|
||||
|
|
|
|||
|
|
@ -226,6 +226,28 @@ func (svc *MDMAppleCommander) AccountConfiguration(ctx context.Context, hostUUID
|
|||
return svc.EnqueueCommand(ctx, hostUUIDs, raw)
|
||||
}
|
||||
|
||||
// DeclarativeManagement sends the homonym [command][1] to the device to enable DDM or start a new DDM session.
|
||||
//
|
||||
// [1]: https://developer.apple.com/documentation/devicemanagement/declarativemanagementcommand
|
||||
func (svc *MDMAppleCommander) DeclarativeManagement(ctx context.Context, hostUUIDs []string, uuid string) error {
|
||||
raw := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Command</key>
|
||||
<dict>
|
||||
<key>RequestType</key>
|
||||
<string>DeclarativeManagement</string>
|
||||
</dict>
|
||||
|
||||
<key>CommandUUID</key>
|
||||
<string>%s</string>
|
||||
</dict>
|
||||
</plist>`, uuid)
|
||||
|
||||
return svc.EnqueueCommand(ctx, hostUUIDs, raw)
|
||||
}
|
||||
|
||||
func (svc *MDMAppleCommander) DeviceConfigured(ctx context.Context, hostUUID, cmdUUID string) error {
|
||||
raw := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ func GenerateRandomPin(length int) string {
|
|||
return fmt.Sprintf(f, v)
|
||||
}
|
||||
|
||||
// FmtErrorChain formats Command error message for macOS MDM v1
|
||||
func FmtErrorChain(chain []mdm.ErrorChain) string {
|
||||
var sb strings.Builder
|
||||
for _, mdmErr := range chain {
|
||||
|
|
@ -113,6 +114,15 @@ func FmtErrorChain(chain []mdm.ErrorChain) string {
|
|||
return sb.String()
|
||||
}
|
||||
|
||||
// FmtDDMError formats a DDM error message
|
||||
func FmtDDMError(reasons []fleet.MDMAppleDDMStatusErrorReason) string {
|
||||
var errMsg strings.Builder
|
||||
for _, r := range reasons {
|
||||
errMsg.WriteString(fmt.Sprintf("%s: %s %+v\n", r.Code, r.Description, r.Details))
|
||||
}
|
||||
return errMsg.String()
|
||||
}
|
||||
|
||||
func EnrollURL(token string, appConfig *fleet.AppConfig) (string, error) {
|
||||
enrollURL, err := url.Parse(appConfig.ServerSettings.ServerURL)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ func GetRawProfilePlatform(profile []byte) string {
|
|||
bytes.EqualFold(prefix, trimmedProfile[:len(prefix)])
|
||||
}
|
||||
|
||||
if prefixMatches([]byte("<?xml")) {
|
||||
if prefixMatches([]byte("<?xml")) || prefixMatches([]byte(`{`)) {
|
||||
return "darwin"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -179,6 +179,16 @@ func TestGetRawProfilePlatform(t *testing.T) {
|
|||
input: []byte("<?x"),
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "DDM JSON",
|
||||
input: []byte(`{"foo": "bar"}`),
|
||||
expected: "darwin",
|
||||
},
|
||||
{
|
||||
name: "DDM JSON with whitespace",
|
||||
input: []byte(` {"foo": "bar"}`),
|
||||
expected: "darwin",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
|
|
|
|||
|
|
@ -642,6 +642,8 @@ type GetMDMAppleConfigProfileByDeprecatedIDFunc func(ctx context.Context, profil
|
|||
|
||||
type GetMDMAppleConfigProfileFunc func(ctx context.Context, profileUUID string) (*fleet.MDMAppleConfigProfile, error)
|
||||
|
||||
type GetMDMAppleDeclarationFunc func(ctx context.Context, declUUID string) (*fleet.MDMAppleDeclaration, error)
|
||||
|
||||
type ListMDMAppleConfigProfilesFunc func(ctx context.Context, teamID *uint) ([]*fleet.MDMAppleConfigProfile, error)
|
||||
|
||||
type DeleteMDMAppleConfigProfileByDeprecatedIDFunc func(ctx context.Context, profileID uint) error
|
||||
|
|
@ -778,6 +780,20 @@ type GetDEPAssignProfileExpiredCooldownsFunc func(ctx context.Context) (map[uint
|
|||
|
||||
type UpdateDEPAssignProfileRetryPendingFunc func(ctx context.Context, jobID uint, serials []string) error
|
||||
|
||||
type InsertMDMAppleDDMRequestFunc func(ctx context.Context, hostUUID string, messageType string, rawJSON string) error
|
||||
|
||||
type MDMAppleDDMDeclarationsTokenFunc func(ctx context.Context, hostUUID string) (*fleet.MDMAppleDDMDeclarationsToken, error)
|
||||
|
||||
type MDMAppleDDMDeclarationItemsFunc func(ctx context.Context, hostUUID string) ([]fleet.MDMAppleDDMDeclarationItem, error)
|
||||
|
||||
type MDMAppleDDMDeclarationsResponseFunc func(ctx context.Context, identifier string, hostUUID string) (*fleet.MDMAppleDeclaration, error)
|
||||
|
||||
type MDMAppleBatchSetHostDeclarationStateFunc func(ctx context.Context) ([]string, error)
|
||||
|
||||
type MDMAppleStoreDDMStatusReportFunc func(ctx context.Context, hostUUID string, updates []*fleet.MDMAppleHostDeclaration) error
|
||||
|
||||
type MDMAppleSetDeclarationsAsVerifyingFunc func(ctx context.Context, hostUUID string) error
|
||||
|
||||
type WSTEPStoreCertificateFunc func(ctx context.Context, name string, crt *x509.Certificate) error
|
||||
|
||||
type WSTEPNewSerialFunc func(ctx context.Context) (*big.Int, error)
|
||||
|
|
@ -836,7 +852,9 @@ type NewMDMWindowsConfigProfileFunc func(ctx context.Context, cp fleet.MDMWindow
|
|||
|
||||
type SetOrUpdateMDMWindowsConfigProfileFunc func(ctx context.Context, cp fleet.MDMWindowsConfigProfile) error
|
||||
|
||||
type BatchSetMDMProfilesFunc func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile) error
|
||||
type BatchSetMDMProfilesFunc func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDeclarations []*fleet.MDMAppleDeclaration) error
|
||||
|
||||
type NewMDMAppleDeclarationFunc func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error)
|
||||
|
||||
type NewHostScriptExecutionRequestFunc func(ctx context.Context, request *fleet.HostScriptRequestPayload) (*fleet.HostScriptResult, error)
|
||||
|
||||
|
|
@ -1817,6 +1835,9 @@ type DataStore struct {
|
|||
GetMDMAppleConfigProfileFunc GetMDMAppleConfigProfileFunc
|
||||
GetMDMAppleConfigProfileFuncInvoked bool
|
||||
|
||||
GetMDMAppleDeclarationFunc GetMDMAppleDeclarationFunc
|
||||
GetMDMAppleDeclarationFuncInvoked bool
|
||||
|
||||
ListMDMAppleConfigProfilesFunc ListMDMAppleConfigProfilesFunc
|
||||
ListMDMAppleConfigProfilesFuncInvoked bool
|
||||
|
||||
|
|
@ -2021,6 +2042,27 @@ type DataStore struct {
|
|||
UpdateDEPAssignProfileRetryPendingFunc UpdateDEPAssignProfileRetryPendingFunc
|
||||
UpdateDEPAssignProfileRetryPendingFuncInvoked bool
|
||||
|
||||
InsertMDMAppleDDMRequestFunc InsertMDMAppleDDMRequestFunc
|
||||
InsertMDMAppleDDMRequestFuncInvoked bool
|
||||
|
||||
MDMAppleDDMDeclarationsTokenFunc MDMAppleDDMDeclarationsTokenFunc
|
||||
MDMAppleDDMDeclarationsTokenFuncInvoked bool
|
||||
|
||||
MDMAppleDDMDeclarationItemsFunc MDMAppleDDMDeclarationItemsFunc
|
||||
MDMAppleDDMDeclarationItemsFuncInvoked bool
|
||||
|
||||
MDMAppleDDMDeclarationsResponseFunc MDMAppleDDMDeclarationsResponseFunc
|
||||
MDMAppleDDMDeclarationsResponseFuncInvoked bool
|
||||
|
||||
MDMAppleBatchSetHostDeclarationStateFunc MDMAppleBatchSetHostDeclarationStateFunc
|
||||
MDMAppleBatchSetHostDeclarationStateFuncInvoked bool
|
||||
|
||||
MDMAppleStoreDDMStatusReportFunc MDMAppleStoreDDMStatusReportFunc
|
||||
MDMAppleStoreDDMStatusReportFuncInvoked bool
|
||||
|
||||
MDMAppleSetDeclarationsAsVerifyingFunc MDMAppleSetDeclarationsAsVerifyingFunc
|
||||
MDMAppleSetDeclarationsAsVerifyingFuncInvoked bool
|
||||
|
||||
WSTEPStoreCertificateFunc WSTEPStoreCertificateFunc
|
||||
WSTEPStoreCertificateFuncInvoked bool
|
||||
|
||||
|
|
@ -2111,6 +2153,9 @@ type DataStore struct {
|
|||
BatchSetMDMProfilesFunc BatchSetMDMProfilesFunc
|
||||
BatchSetMDMProfilesFuncInvoked bool
|
||||
|
||||
NewMDMAppleDeclarationFunc NewMDMAppleDeclarationFunc
|
||||
NewMDMAppleDeclarationFuncInvoked bool
|
||||
|
||||
NewHostScriptExecutionRequestFunc NewHostScriptExecutionRequestFunc
|
||||
NewHostScriptExecutionRequestFuncInvoked bool
|
||||
|
||||
|
|
@ -4361,6 +4406,13 @@ func (s *DataStore) GetMDMAppleConfigProfile(ctx context.Context, profileUUID st
|
|||
return s.GetMDMAppleConfigProfileFunc(ctx, profileUUID)
|
||||
}
|
||||
|
||||
func (s *DataStore) GetMDMAppleDeclaration(ctx context.Context, declUUID string) (*fleet.MDMAppleDeclaration, error) {
|
||||
s.mu.Lock()
|
||||
s.GetMDMAppleDeclarationFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.GetMDMAppleDeclarationFunc(ctx, declUUID)
|
||||
}
|
||||
|
||||
func (s *DataStore) ListMDMAppleConfigProfiles(ctx context.Context, teamID *uint) ([]*fleet.MDMAppleConfigProfile, error) {
|
||||
s.mu.Lock()
|
||||
s.ListMDMAppleConfigProfilesFuncInvoked = true
|
||||
|
|
@ -4837,6 +4889,55 @@ func (s *DataStore) UpdateDEPAssignProfileRetryPending(ctx context.Context, jobI
|
|||
return s.UpdateDEPAssignProfileRetryPendingFunc(ctx, jobID, serials)
|
||||
}
|
||||
|
||||
func (s *DataStore) InsertMDMAppleDDMRequest(ctx context.Context, hostUUID string, messageType string, rawJSON string) error {
|
||||
s.mu.Lock()
|
||||
s.InsertMDMAppleDDMRequestFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.InsertMDMAppleDDMRequestFunc(ctx, hostUUID, messageType, rawJSON)
|
||||
}
|
||||
|
||||
func (s *DataStore) MDMAppleDDMDeclarationsToken(ctx context.Context, hostUUID string) (*fleet.MDMAppleDDMDeclarationsToken, error) {
|
||||
s.mu.Lock()
|
||||
s.MDMAppleDDMDeclarationsTokenFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.MDMAppleDDMDeclarationsTokenFunc(ctx, hostUUID)
|
||||
}
|
||||
|
||||
func (s *DataStore) MDMAppleDDMDeclarationItems(ctx context.Context, hostUUID string) ([]fleet.MDMAppleDDMDeclarationItem, error) {
|
||||
s.mu.Lock()
|
||||
s.MDMAppleDDMDeclarationItemsFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.MDMAppleDDMDeclarationItemsFunc(ctx, hostUUID)
|
||||
}
|
||||
|
||||
func (s *DataStore) MDMAppleDDMDeclarationsResponse(ctx context.Context, identifier string, hostUUID string) (*fleet.MDMAppleDeclaration, error) {
|
||||
s.mu.Lock()
|
||||
s.MDMAppleDDMDeclarationsResponseFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.MDMAppleDDMDeclarationsResponseFunc(ctx, identifier, hostUUID)
|
||||
}
|
||||
|
||||
func (s *DataStore) MDMAppleBatchSetHostDeclarationState(ctx context.Context) ([]string, error) {
|
||||
s.mu.Lock()
|
||||
s.MDMAppleBatchSetHostDeclarationStateFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.MDMAppleBatchSetHostDeclarationStateFunc(ctx)
|
||||
}
|
||||
|
||||
func (s *DataStore) MDMAppleStoreDDMStatusReport(ctx context.Context, hostUUID string, updates []*fleet.MDMAppleHostDeclaration) error {
|
||||
s.mu.Lock()
|
||||
s.MDMAppleStoreDDMStatusReportFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.MDMAppleStoreDDMStatusReportFunc(ctx, hostUUID, updates)
|
||||
}
|
||||
|
||||
func (s *DataStore) MDMAppleSetDeclarationsAsVerifying(ctx context.Context, hostUUID string) error {
|
||||
s.mu.Lock()
|
||||
s.MDMAppleSetDeclarationsAsVerifyingFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.MDMAppleSetDeclarationsAsVerifyingFunc(ctx, hostUUID)
|
||||
}
|
||||
|
||||
func (s *DataStore) WSTEPStoreCertificate(ctx context.Context, name string, crt *x509.Certificate) error {
|
||||
s.mu.Lock()
|
||||
s.WSTEPStoreCertificateFuncInvoked = true
|
||||
|
|
@ -5040,11 +5141,18 @@ func (s *DataStore) SetOrUpdateMDMWindowsConfigProfile(ctx context.Context, cp f
|
|||
return s.SetOrUpdateMDMWindowsConfigProfileFunc(ctx, cp)
|
||||
}
|
||||
|
||||
func (s *DataStore) BatchSetMDMProfiles(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile) error {
|
||||
func (s *DataStore) BatchSetMDMProfiles(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDeclarations []*fleet.MDMAppleDeclaration) error {
|
||||
s.mu.Lock()
|
||||
s.BatchSetMDMProfilesFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.BatchSetMDMProfilesFunc(ctx, tmID, macProfiles, winProfiles)
|
||||
return s.BatchSetMDMProfilesFunc(ctx, tmID, macProfiles, winProfiles, macDeclarations)
|
||||
}
|
||||
|
||||
func (s *DataStore) NewMDMAppleDeclaration(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
|
||||
s.mu.Lock()
|
||||
s.NewMDMAppleDeclarationFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.NewMDMAppleDeclarationFunc(ctx, declaration)
|
||||
}
|
||||
|
||||
func (s *DataStore) NewHostScriptExecutionRequest(ctx context.Context, request *fleet.HostScriptRequestPayload) (*fleet.HostScriptResult, error) {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/nanodep/godep"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
||||
nano_service "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/service"
|
||||
"github.com/fleetdm/fleet/v4/server/sso"
|
||||
"github.com/fleetdm/fleet/v4/server/worker"
|
||||
kitlog "github.com/go-kit/log"
|
||||
|
|
@ -387,6 +388,136 @@ func (svc *Service) NewMDMAppleConfigProfile(ctx context.Context, teamID uint, r
|
|||
return newCP, nil
|
||||
}
|
||||
|
||||
func (svc *Service) NewMDMAppleDeclaration(ctx context.Context, teamID uint, r io.Reader, labels []string, name string) (*fleet.MDMAppleDeclaration, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.MDMConfigProfileAuthz{TeamID: &teamID}, fleet.ActionWrite); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
|
||||
// check that Apple MDM is enabled - the middleware of that endpoint checks
|
||||
// only that any MDM is enabled, maybe it's just Windows
|
||||
if err := svc.VerifyMDMAppleConfigured(ctx); err != nil {
|
||||
err := fleet.NewInvalidArgumentError("declaration", fleet.AppleMDMNotConfiguredMessage).WithStatus(http.StatusBadRequest)
|
||||
return nil, ctxerr.Wrap(ctx, err, "check macOS MDM enabled")
|
||||
}
|
||||
|
||||
fleetNames := mdm_types.FleetReservedProfileNames()
|
||||
if _, ok := fleetNames[name]; ok {
|
||||
err := fleet.NewInvalidArgumentError("declaration", fmt.Sprintf("Profile name %q is not allowed.", name)).WithStatus(http.StatusBadRequest)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var teamName string
|
||||
if teamID >= 1 {
|
||||
tm, err := svc.EnterpriseOverrides.TeamByIDOrName(ctx, &teamID, nil)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
teamName = tm.Name
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tmID *uint
|
||||
if teamID >= 1 {
|
||||
tmID = &teamID
|
||||
}
|
||||
|
||||
validatedLabels, err := svc.validateDeclarationLabels(ctx, labels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO(roberto): Maybe GetRawDeclarationValues belongs inside NewMDMAppleDeclaration? We can refactor this in a follow up.
|
||||
rawDecl, err := fleet.GetRawDeclarationValues(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := rawDecl.ValidateUserProvided(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d := fleet.NewMDMAppleDeclaration(data, tmID, name, rawDecl.Type, rawDecl.Identifier)
|
||||
|
||||
// TODO(roberto): Is this already handled in NewMDMAppleDeclaration? Could we add the labels as well?
|
||||
d.Labels = validatedLabels
|
||||
d.TeamID = tmID
|
||||
|
||||
decl, err := svc.ds.NewMDMAppleDeclaration(ctx, d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var (
|
||||
actTeamID *uint
|
||||
actTeamName *string
|
||||
)
|
||||
if teamID > 0 {
|
||||
actTeamID = &teamID
|
||||
actTeamName = &teamName
|
||||
}
|
||||
if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeCreatedDeclarationProfile{
|
||||
TeamID: actTeamID,
|
||||
TeamName: actTeamName,
|
||||
ProfileName: decl.Name,
|
||||
Identifier: decl.Identifier,
|
||||
}); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "logging activity for create mdm apple declaration")
|
||||
}
|
||||
|
||||
return decl, nil
|
||||
}
|
||||
|
||||
func (svc *Service) batchValidateDeclarationLabels(ctx context.Context, labelNames []string) (map[string]fleet.ConfigurationProfileLabel, error) {
|
||||
if len(labelNames) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
labels, err := svc.ds.LabelIDsByName(ctx, labelNames)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "getting label IDs by name")
|
||||
}
|
||||
|
||||
uniqueNames := make(map[string]bool)
|
||||
for _, entry := range labelNames {
|
||||
if _, value := uniqueNames[entry]; !value {
|
||||
uniqueNames[entry] = true
|
||||
}
|
||||
}
|
||||
|
||||
if len(labels) != len(uniqueNames) {
|
||||
return nil, &fleet.BadRequestError{
|
||||
Message: "some or all the labels provided don't exist",
|
||||
InternalErr: fmt.Errorf("names provided: %v", labelNames),
|
||||
}
|
||||
}
|
||||
|
||||
profLabels := make(map[string]fleet.ConfigurationProfileLabel)
|
||||
for labelName, labelID := range labels {
|
||||
profLabels[labelName] = fleet.ConfigurationProfileLabel{
|
||||
LabelName: labelName,
|
||||
LabelID: labelID,
|
||||
}
|
||||
}
|
||||
return profLabels, nil
|
||||
}
|
||||
|
||||
func (svc *Service) validateDeclarationLabels(ctx context.Context, labelNames []string) ([]fleet.ConfigurationProfileLabel, error) {
|
||||
labelMap, err := svc.batchValidateDeclarationLabels(ctx, labelNames)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "validating declaration labels")
|
||||
}
|
||||
|
||||
var declLabels []fleet.ConfigurationProfileLabel
|
||||
for _, label := range labelMap {
|
||||
declLabels = append(declLabels, label)
|
||||
}
|
||||
return declLabels, nil
|
||||
}
|
||||
|
||||
type listMDMAppleConfigProfilesRequest struct {
|
||||
TeamID uint `query:"team_id,optional"`
|
||||
}
|
||||
|
|
@ -515,6 +646,25 @@ func (svc *Service) GetMDMAppleConfigProfile(ctx context.Context, profileUUID st
|
|||
return cp, nil
|
||||
}
|
||||
|
||||
func (svc *Service) GetMDMAppleDeclaration(ctx context.Context, profileUUID string) (*fleet.MDMAppleDeclaration, error) {
|
||||
// first we perform a perform basic authz check
|
||||
if err := svc.authz.Authorize(ctx, &fleet.Team{}, fleet.ActionRead); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cp, err := svc.ds.GetMDMAppleDeclaration(ctx, profileUUID)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
|
||||
// now we can do a specific authz check based on team id of profile before we return the profile
|
||||
if err := svc.authz.Authorize(ctx, &fleet.MDMConfigProfileAuthz{TeamID: cp.TeamID}, fleet.ActionRead); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cp, nil
|
||||
}
|
||||
|
||||
type deleteMDMAppleConfigProfileRequest struct {
|
||||
ProfileID uint `url:"profile_id"`
|
||||
}
|
||||
|
|
@ -623,6 +773,85 @@ func (svc *Service) DeleteMDMAppleConfigProfile(ctx context.Context, profileUUID
|
|||
return nil
|
||||
}
|
||||
|
||||
func (svc *Service) DeleteMDMAppleDeclaration(ctx context.Context, declUUID string) error {
|
||||
// first we perform a perform basic authz check
|
||||
if err := svc.authz.Authorize(ctx, &fleet.Team{}, fleet.ActionRead); err != nil {
|
||||
return ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
|
||||
// check that Apple MDM is enabled - the middleware of that endpoint checks
|
||||
// only that any MDM is enabled, maybe it's just Windows
|
||||
if err := svc.VerifyMDMAppleConfigured(ctx); err != nil {
|
||||
err := fleet.NewInvalidArgumentError("profile_uuid", fleet.AppleMDMNotConfiguredMessage).WithStatus(http.StatusBadRequest)
|
||||
return ctxerr.Wrap(ctx, err, "check macOS MDM enabled")
|
||||
}
|
||||
|
||||
decl, err := svc.ds.GetMDMAppleDeclaration(ctx, declUUID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
|
||||
if _, ok := mdm_types.FleetReservedProfileNames()[decl.Name]; ok {
|
||||
return &fleet.BadRequestError{
|
||||
Message: "profiles managed by Fleet can't be deleted using this endpoint.",
|
||||
InternalErr: fmt.Errorf("deleting profile %s is not allowed because it's managed by Fleet", decl.Name),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: refine our approach to deleting restricted/forbidden types of declarations so that we
|
||||
// can check that Fleet-managed aren't being deleted; this can be addressed once we add support
|
||||
// for more types of declarations
|
||||
var d fleet.MDMAppleRawDeclaration
|
||||
if err := json.Unmarshal(decl.RawJSON, &d); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "unmarshalling declaration")
|
||||
}
|
||||
if err := d.ValidateUserProvided(); err != nil {
|
||||
return ctxerr.Wrap(ctx, &fleet.BadRequestError{Message: err.Error()})
|
||||
}
|
||||
|
||||
var teamName string
|
||||
teamID := *decl.TeamID
|
||||
if teamID >= 1 {
|
||||
tm, err := svc.EnterpriseOverrides.TeamByIDOrName(ctx, &teamID, nil)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
teamName = tm.Name
|
||||
}
|
||||
|
||||
// now we can do a specific authz check based on team id of profile before we delete the profile
|
||||
if err := svc.authz.Authorize(ctx, &fleet.MDMConfigProfileAuthz{TeamID: decl.TeamID}, fleet.ActionWrite); err != nil {
|
||||
return ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
|
||||
if err := svc.ds.DeleteMDMAppleConfigProfile(ctx, declUUID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
|
||||
if err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{teamID}, nil, nil); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "bulk set pending host profiles")
|
||||
}
|
||||
|
||||
var (
|
||||
actTeamID *uint
|
||||
actTeamName *string
|
||||
)
|
||||
if teamID > 0 {
|
||||
actTeamID = &teamID
|
||||
actTeamName = &teamName
|
||||
}
|
||||
if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeDeletedDeclarationProfile{
|
||||
TeamID: actTeamID,
|
||||
TeamName: actTeamName,
|
||||
ProfileName: decl.Name,
|
||||
Identifier: decl.Identifier,
|
||||
}); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "logging activity for delete mdm apple declaration")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type getMDMAppleFileVaultSummaryRequest struct {
|
||||
TeamID *uint `query:"team_id,optional"`
|
||||
}
|
||||
|
|
@ -2225,7 +2454,6 @@ func (svc *MDMAppleCheckinAndCommandService) Authenticate(r *mdm.Request, m *mdm
|
|||
InstalledFromDEP: info.DEPAssignedToFleet,
|
||||
MDMPlatform: fleet.MDMPlatformApple,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// TokenUpdate handles MDM [TokenUpdate][1] requests.
|
||||
|
|
@ -2354,7 +2582,8 @@ func (svc *MDMAppleCheckinAndCommandService) UserAuthenticate(*mdm.Request, *mdm
|
|||
// This method is executed after the request has been handled by nanomdm.
|
||||
//
|
||||
// [1]: https://developer.apple.com/documentation/devicemanagement/declarative_management_checkin
|
||||
func (svc *MDMAppleCheckinAndCommandService) DeclarativeManagement(*mdm.Request, *mdm.DeclarativeManagement) ([]byte, error) {
|
||||
func (svc *MDMAppleCheckinAndCommandService) DeclarativeManagement(r *mdm.Request, dm *mdm.DeclarativeManagement) ([]byte, error) {
|
||||
// DeclarativeManagement is handled by the MDMAppleDDMService.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
|
@ -2409,7 +2638,13 @@ func (svc *MDMAppleCheckinAndCommandService) CommandAndReportResults(r *mdm.Requ
|
|||
cmdResult.Status == fleet.MDMAppleStatusCommandFormatError {
|
||||
return nil, svc.ds.UpdateHostLockWipeStatusFromAppleMDMResult(r.Context, cmdResult.UDID, cmdResult.CommandUUID, requestType, cmdResult.Status == fleet.MDMAppleStatusAcknowledged)
|
||||
}
|
||||
case "DeclarativeManagement":
|
||||
// set "pending-install" profiles to "verifying"
|
||||
err := svc.ds.MDMAppleSetDeclarationsAsVerifying(r.Context, cmdResult.UDID)
|
||||
return nil, ctxerr.Wrap(r.Context, err, "update declaration status on DeclarativeManagement ack")
|
||||
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
|
@ -2504,6 +2739,34 @@ func ensureFleetdConfig(ctx context.Context, ds fleet.Datastore, logger kitlog.L
|
|||
return nil
|
||||
}
|
||||
|
||||
func ReconcileAppleDeclarations(
|
||||
ctx context.Context,
|
||||
ds fleet.Datastore,
|
||||
commander *apple_mdm.MDMAppleCommander,
|
||||
logger kitlog.Logger,
|
||||
) error {
|
||||
|
||||
// batch set declarations as pending
|
||||
changedHosts, err := ds.MDMAppleBatchSetHostDeclarationState(ctx)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "updating host declaration state")
|
||||
}
|
||||
|
||||
if len(changedHosts) == 0 {
|
||||
logger.Log("msg", "no hosts with changed declarations")
|
||||
return nil
|
||||
}
|
||||
|
||||
// send a DeclarativeManagement command to start a sync
|
||||
if err := commander.DeclarativeManagement(ctx, changedHosts, uuid.NewString()); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "issuing DeclarativeManagement command")
|
||||
}
|
||||
|
||||
logger.Log("msg", "sent DeclarativeManagement command", "host_number", len(changedHosts))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReconcileAppleProfiles(
|
||||
ctx context.Context,
|
||||
ds fleet.Datastore,
|
||||
|
|
@ -2946,3 +3209,231 @@ func RenewSCEPCertificates(
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MDMAppleDDMService is the service that handles MDM [DeclarativeManagement][1] requests.
|
||||
//
|
||||
// [1]: https://developer.apple.com/documentation/devicemanagement/declarative_management_checkin
|
||||
type MDMAppleDDMService struct {
|
||||
ds fleet.Datastore
|
||||
logger kitlog.Logger
|
||||
}
|
||||
|
||||
func NewMDMAppleDDMService(ds fleet.Datastore, logger kitlog.Logger) *MDMAppleDDMService {
|
||||
return &MDMAppleDDMService{
|
||||
ds: ds,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// DeclarativeManagement handles MDM [DeclarativeManagement][1] requests.
|
||||
//
|
||||
// This method is when the request has been handled by nanomdm.
|
||||
//
|
||||
// [1]: https://developer.apple.com/documentation/devicemanagement/declarative_management_checkin
|
||||
func (svc *MDMAppleDDMService) DeclarativeManagement(r *mdm.Request, dm *mdm.DeclarativeManagement) ([]byte, error) {
|
||||
if dm == nil {
|
||||
level.Debug(svc.logger).Log("msg", "ddm request received with nil payload")
|
||||
return nil, nil
|
||||
}
|
||||
level.Debug(svc.logger).Log("msg", "ddm request received", "endpoint", dm.Endpoint)
|
||||
|
||||
if err := svc.ds.InsertMDMAppleDDMRequest(r.Context, dm.UDID, dm.Endpoint, string(dm.Data)); err != nil {
|
||||
return nil, ctxerr.Wrap(r.Context, err, "insert ddm request history")
|
||||
}
|
||||
|
||||
if dm.UDID == "" {
|
||||
return nil, nano_service.NewHTTPStatusError(http.StatusBadRequest, ctxerr.New(r.Context, "missing UDID in request"))
|
||||
}
|
||||
|
||||
switch {
|
||||
case dm.Endpoint == "tokens":
|
||||
level.Debug(svc.logger).Log("msg", "received tokens request")
|
||||
return svc.handleTokens(r.Context, dm.UDID)
|
||||
|
||||
case dm.Endpoint == "declaration-items":
|
||||
level.Debug(svc.logger).Log("msg", "received declaration-items request")
|
||||
return svc.handleDeclarationItems(r.Context, dm.UDID)
|
||||
|
||||
case dm.Endpoint == "status":
|
||||
level.Debug(svc.logger).Log("msg", "received status request")
|
||||
return nil, svc.handleDeclarationStatus(r.Context, dm)
|
||||
|
||||
case strings.HasPrefix(dm.Endpoint, "declaration/"):
|
||||
level.Debug(svc.logger).Log("msg", "received declarations request")
|
||||
return svc.handleDeclarationsResponse(r.Context, dm.Endpoint, dm.UDID)
|
||||
|
||||
default:
|
||||
return nil, nano_service.NewHTTPStatusError(http.StatusBadRequest, ctxerr.New(r.Context, fmt.Sprintf("unrecognized declarations endpoint: %s", dm.Endpoint)))
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *MDMAppleDDMService) handleTokens(ctx context.Context, hostUUID string) ([]byte, error) {
|
||||
tok, err := svc.ds.MDMAppleDDMDeclarationsToken(ctx, hostUUID)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "getting synchronization tokens")
|
||||
}
|
||||
|
||||
b, err := json.Marshal(fleet.MDMAppleDDMTokensResponse{
|
||||
SyncTokens: *tok,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "marshaling synchronization tokens")
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (svc *MDMAppleDDMService) handleDeclarationItems(ctx context.Context, hostUUID string) ([]byte, error) {
|
||||
di, err := svc.ds.MDMAppleDDMDeclarationItems(ctx, hostUUID)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "getting synchronization tokens")
|
||||
}
|
||||
|
||||
activations := []fleet.MDMAppleDDMManifest{}
|
||||
configurations := []fleet.MDMAppleDDMManifest{}
|
||||
for _, d := range di {
|
||||
configurations = append(configurations, fleet.MDMAppleDDMManifest(d))
|
||||
activations = append(activations, fleet.MDMAppleDDMManifest{
|
||||
Identifier: fmt.Sprintf("%s.activation", d.Identifier),
|
||||
ServerToken: d.ServerToken,
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Look for ways to optimize the declaration item query so that we don't have to get the declarations token separately.
|
||||
dTok, err := svc.ds.MDMAppleDDMDeclarationsToken(ctx, hostUUID)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "getting declarations token")
|
||||
}
|
||||
|
||||
b, err := json.Marshal(fleet.MDMAppleDDMDeclarationItemsResponse{
|
||||
Declarations: fleet.MDMAppleDDMManifestItems{
|
||||
Activations: activations,
|
||||
Configurations: configurations,
|
||||
Assets: []fleet.MDMAppleDDMManifest{},
|
||||
Management: []fleet.MDMAppleDDMManifest{},
|
||||
},
|
||||
DeclarationsToken: dTok.DeclarationsToken,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "marshaling synchronization tokens")
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (svc *MDMAppleDDMService) handleDeclarationsResponse(ctx context.Context, endpoint string, hostUUID string) ([]byte, error) {
|
||||
parts := strings.Split(endpoint, "/")
|
||||
if len(parts) != 3 {
|
||||
return nil, nano_service.NewHTTPStatusError(http.StatusBadRequest, ctxerr.New(ctx, fmt.Sprintf("unrecognized declarations endpoint: %s", endpoint)))
|
||||
}
|
||||
level.Debug(svc.logger).Log("msg", "parsed declarations request", "type", parts[1], "identifier", parts[2])
|
||||
|
||||
switch parts[1] {
|
||||
case "activation":
|
||||
return svc.handleActivationDeclaration(ctx, parts, hostUUID)
|
||||
case "configuration":
|
||||
return svc.handleConfigurationDeclaration(ctx, parts, hostUUID)
|
||||
default:
|
||||
return nil, newNotFoundError()
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *MDMAppleDDMService) handleActivationDeclaration(ctx context.Context, parts []string, hostUUID string) ([]byte, error) {
|
||||
references := strings.TrimSuffix(parts[2], ".activation")
|
||||
|
||||
// ensure the declaration for the requested activation stil exists
|
||||
d, err := svc.ds.MDMAppleDDMDeclarationsResponse(ctx, references, hostUUID)
|
||||
if err != nil {
|
||||
if fleet.IsNotFound(err) {
|
||||
return nil, nano_service.NewHTTPStatusError(http.StatusNotFound, err)
|
||||
}
|
||||
return nil, ctxerr.Wrap(ctx, err, "getting linked configuration for activation declaration")
|
||||
}
|
||||
|
||||
response := fmt.Sprintf(`
|
||||
{
|
||||
"Identifier": "%s",
|
||||
"Payload": {
|
||||
"StandardConfigurations": ["%s"]
|
||||
},
|
||||
"ServerToken": "%s",
|
||||
"Type": "com.apple.activation.simple"
|
||||
}`, parts[2], references, d.Checksum)
|
||||
|
||||
return []byte(response), nil
|
||||
}
|
||||
|
||||
func (svc *MDMAppleDDMService) handleConfigurationDeclaration(ctx context.Context, parts []string, hostUUID string) ([]byte, error) {
|
||||
d, err := svc.ds.MDMAppleDDMDeclarationsResponse(ctx, parts[2], hostUUID)
|
||||
if err != nil {
|
||||
if fleet.IsNotFound(err) {
|
||||
return nil, nano_service.NewHTTPStatusError(http.StatusNotFound, err)
|
||||
}
|
||||
return nil, ctxerr.Wrap(ctx, err, "getting declaration response")
|
||||
}
|
||||
|
||||
var tempd map[string]any
|
||||
if err := json.Unmarshal(d.RawJSON, &tempd); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "unmarshaling stored declaration")
|
||||
}
|
||||
tempd["ServerToken"] = d.Checksum
|
||||
|
||||
b, err := json.Marshal(tempd)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "marshaling declaration")
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (svc *MDMAppleDDMService) handleDeclarationStatus(ctx context.Context, dm *mdm.DeclarativeManagement) error {
|
||||
var status fleet.MDMAppleDDMStatusReport
|
||||
if err := json.Unmarshal(dm.Data, &status); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "unmarshalling response")
|
||||
}
|
||||
|
||||
configurationReports := status.StatusItems.Management.Declarations.Configurations
|
||||
updates := make([]*fleet.MDMAppleHostDeclaration, len(configurationReports))
|
||||
for i, r := range configurationReports {
|
||||
var status fleet.MDMDeliveryStatus
|
||||
var detail string
|
||||
switch {
|
||||
case r.Active && r.Valid == fleet.MDMAppleDeclarationValid:
|
||||
status = fleet.MDMDeliveryVerified
|
||||
case r.Valid == fleet.MDMAppleDeclarationInvalid:
|
||||
status = fleet.MDMDeliveryFailed
|
||||
detail = apple_mdm.FmtDDMError(r.Reasons)
|
||||
default:
|
||||
status = fleet.MDMDeliveryVerifying
|
||||
}
|
||||
|
||||
updates[i] = &fleet.MDMAppleHostDeclaration{
|
||||
Status: &status,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
Detail: detail,
|
||||
Checksum: r.ServerToken,
|
||||
}
|
||||
}
|
||||
|
||||
if len(updates) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MDMAppleStoreDDMStatusReport takes care of cleaning ("pending", "remove")
|
||||
// pairs for the host.
|
||||
//
|
||||
// TODO(roberto): in the DDM documentation, it's mentioned that status
|
||||
// report will give you a "remove" status so the server can track
|
||||
// removals. In my testing, I never saw this (after spending
|
||||
// considerable time trying to make it work.)
|
||||
//
|
||||
// My current guess is that the documentation is implicitly referring
|
||||
// to asset declarations (which deliver tangible "assets" to the host)
|
||||
//
|
||||
// The best indication I found so far, is that if the declaration is
|
||||
// not in the report, then it's implicitly removed.
|
||||
if err := svc.ds.MDMAppleStoreDDMStatusReport(ctx, dm.UDID, updates); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "updating host declaration status with reports")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2727,6 +2727,19 @@ func mobileconfigForTest(name, identifier string) []byte {
|
|||
`, name, identifier, uuid.New().String()))
|
||||
}
|
||||
|
||||
func declBytesForTest(identifier string, payloadContent string) []byte {
|
||||
tmpl := `{
|
||||
"Type": "com.apple.configuration.decl%s",
|
||||
"Identifier": "com.fleet.config%s",
|
||||
"Payload": {
|
||||
"ServiceType": "com.apple.service%s"
|
||||
}
|
||||
}`
|
||||
|
||||
declBytes := []byte(fmt.Sprintf(tmpl, identifier, identifier, payloadContent))
|
||||
return declBytes
|
||||
}
|
||||
|
||||
func mobileconfigForTestWithContent(outerName, outerIdentifier, innerIdentifier, innerType, innerName string) []byte {
|
||||
if innerName == "" {
|
||||
innerName = outerName + ".inner"
|
||||
|
|
|
|||
|
|
@ -294,8 +294,9 @@ func getProfilesContents(baseDir string, profiles []fleet.MDMProfileSpec) ([]fle
|
|||
}
|
||||
|
||||
// by default, use the file name. macOS profiles use their PayloadDisplayName
|
||||
name := strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath))
|
||||
if mdm.GetRawProfilePlatform(fileContents) == "darwin" {
|
||||
ext := filepath.Ext(filePath)
|
||||
name := strings.TrimSuffix(filepath.Base(filePath), ext)
|
||||
if mdm.GetRawProfilePlatform(fileContents) == "darwin" && ext == ".mobileconfig" {
|
||||
mc, err := fleet.NewMDMAppleConfigProfile(fileContents, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("applying fleet config: %w", err)
|
||||
|
|
|
|||
|
|
@ -1003,6 +1003,7 @@ func RegisterAppleMDMProtocolServices(
|
|||
scepStorage scep_depot.Depot,
|
||||
logger kitlog.Logger,
|
||||
checkinAndCommandService nanomdm_service.CheckinAndCommandService,
|
||||
ddmService nanomdm_service.DeclarativeManagement,
|
||||
) error {
|
||||
scepCACerts, scepCAKey, err := scepStorage.CA([]byte{})
|
||||
if err != nil {
|
||||
|
|
@ -1011,7 +1012,7 @@ func RegisterAppleMDMProtocolServices(
|
|||
if err := registerSCEP(mux, scepConfig, scepCACerts[0], scepCAKey, scepStorage, logger); err != nil {
|
||||
return fmt.Errorf("scep: %w", err)
|
||||
}
|
||||
if err := registerMDM(mux, scepCACerts[0], mdmStorage, checkinAndCommandService, logger); err != nil {
|
||||
if err := registerMDM(mux, scepCACerts[0], mdmStorage, checkinAndCommandService, ddmService, logger); err != nil {
|
||||
return fmt.Errorf("mdm: %w", err)
|
||||
}
|
||||
return nil
|
||||
|
|
@ -1085,6 +1086,7 @@ func registerMDM(
|
|||
scepCACert *x509.Certificate,
|
||||
mdmStorage nanomdm_storage.AllStorage,
|
||||
checkinAndCommandService nanomdm_service.CheckinAndCommandService,
|
||||
ddmService nanomdm_service.DeclarativeManagement,
|
||||
logger kitlog.Logger,
|
||||
) error {
|
||||
certVerifier, err := certverify.NewPoolVerifier(
|
||||
|
|
@ -1104,7 +1106,7 @@ func registerMDM(
|
|||
// enrollments and updates the Fleet hosts table accordingly with the UDID and serial number of
|
||||
// the device.
|
||||
// 5. Run actual MDM service operation (checkin handler or command and results handler).
|
||||
coreMDMService := nanomdm.New(mdmStorage, nanomdm.WithLogger(mdmLogger))
|
||||
coreMDMService := nanomdm.New(mdmStorage, nanomdm.WithLogger(mdmLogger), nanomdm.WithDeclarativeManagement(ddmService))
|
||||
// NOTE: it is critical that the coreMDMService runs first, as the first
|
||||
// service in the multi-service feature is run to completion _before_ running
|
||||
// the other ones in parallel. This way, subsequent services have access to
|
||||
|
|
|
|||
909
server/service/integration_ddm_test.go
Normal file
909
server/service/integration_ddm_test.go
Normal file
|
|
@ -0,0 +1,909 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5" // nolint:gosec // used only for tests
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/pkg/mdm/mdmtest"
|
||||
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
kitlog "github.com/go-kit/log"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func (s *integrationMDMTestSuite) TestAppleDDMBatchUpload() {
|
||||
t := s.T()
|
||||
tmpl := `
|
||||
{
|
||||
"Type": "com.apple.configuration.decl%d",
|
||||
"Identifier": "com.fleet.config%d",
|
||||
"Payload": {
|
||||
"ServiceType": "com.apple.bash",
|
||||
"DataAssetReference": "com.fleet.asset.bash" %s
|
||||
}
|
||||
}`
|
||||
// TODO: figure out the best way to do this. We might even consider
|
||||
// starting a different test suite.
|
||||
t.Cleanup(func() { s.cleanupDeclarations(t) })
|
||||
|
||||
newDeclBytes := func(i int, payload ...string) []byte {
|
||||
var p string
|
||||
if len(payload) > 0 {
|
||||
p = "," + strings.Join(payload, ",")
|
||||
}
|
||||
return []byte(fmt.Sprintf(tmpl, i, i, p))
|
||||
}
|
||||
|
||||
var decls [][]byte
|
||||
|
||||
for i := 0; i < 7; i++ {
|
||||
decls = append(decls, newDeclBytes(i))
|
||||
}
|
||||
|
||||
// Non-configuration type should fail
|
||||
res := s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
|
||||
{Name: "bad", Contents: []byte(`{"Type": "com.apple.activation"}`)},
|
||||
}}, http.StatusUnprocessableEntity)
|
||||
|
||||
errMsg := extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Only configuration declarations (com.apple.configuration) are supported")
|
||||
|
||||
// "com.apple.configuration.softwareupdate.enforcement.specific" type should fail
|
||||
res = s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
|
||||
{Name: "bad2", Contents: []byte(`{"Type": "com.apple.configuration.softwareupdate.enforcement.specific"}`)},
|
||||
}}, http.StatusUnprocessableEntity)
|
||||
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Declaration profile can’t include OS updates settings. To control these settings, go to OS updates.")
|
||||
|
||||
// Types from our list of forbidden types should fail
|
||||
for ft := range fleet.ForbiddenDeclTypes {
|
||||
res = s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
|
||||
{Name: "bad2", Contents: []byte(fmt.Sprintf(`{"Type": "%s"}`, ft))},
|
||||
}}, http.StatusUnprocessableEntity)
|
||||
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Only configuration declarations that don’t require an asset reference are supported.")
|
||||
}
|
||||
|
||||
// "com.apple.configuration.management.status-subscriptions" type should fail
|
||||
res = s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
|
||||
{Name: "bad2", Contents: []byte(`{"Type": "com.apple.configuration.management.status-subscriptions"}`)},
|
||||
}}, http.StatusUnprocessableEntity)
|
||||
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Declaration profile can’t include status subscription type. To get host’s vitals, please use queries and policies.")
|
||||
|
||||
// Two different payloads with the same name should fail
|
||||
res = s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
|
||||
{Name: "bad2", Contents: newDeclBytes(1, `"foo": "bar"`)},
|
||||
{Name: "bad2", Contents: newDeclBytes(2, `"baz": "bing"`)},
|
||||
}}, http.StatusUnprocessableEntity)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "A declaration profile with this name already exists.")
|
||||
|
||||
// Same identifier should fail
|
||||
res = s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
|
||||
{Name: "N1", Contents: decls[0]},
|
||||
{Name: "N2", Contents: decls[0]},
|
||||
}}, http.StatusUnprocessableEntity)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "A declaration profile with this identifier already exists.")
|
||||
|
||||
// Create 2 declarations
|
||||
s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
|
||||
{Name: "N1", Contents: decls[0]},
|
||||
{Name: "N2", Contents: decls[1]},
|
||||
}}, http.StatusNoContent)
|
||||
|
||||
var resp listMDMConfigProfilesResponse
|
||||
s.DoJSON("GET", "/api/latest/fleet/mdm/profiles", &listMDMConfigProfilesRequest{}, http.StatusOK, &resp)
|
||||
|
||||
require.Len(t, resp.Profiles, 2)
|
||||
require.Equal(t, "N1", resp.Profiles[0].Name)
|
||||
require.Equal(t, "darwin", resp.Profiles[0].Platform)
|
||||
require.Equal(t, "N2", resp.Profiles[1].Name)
|
||||
require.Equal(t, "darwin", resp.Profiles[1].Platform)
|
||||
|
||||
// Create 2 new declarations. These should take the place of the first two.
|
||||
s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
|
||||
{Name: "N3", Contents: decls[2]},
|
||||
{Name: "N4", Contents: decls[3]},
|
||||
}}, http.StatusNoContent)
|
||||
|
||||
s.DoJSON("GET", "/api/latest/fleet/mdm/profiles", &listMDMConfigProfilesRequest{}, http.StatusOK, &resp)
|
||||
|
||||
require.Len(t, resp.Profiles, 2)
|
||||
require.Equal(t, "N3", resp.Profiles[0].Name)
|
||||
require.Equal(t, "darwin", resp.Profiles[0].Platform)
|
||||
require.Equal(t, "N4", resp.Profiles[1].Name)
|
||||
require.Equal(t, "darwin", resp.Profiles[1].Platform)
|
||||
|
||||
// replace only 1 declaration, the other one should be the same
|
||||
|
||||
s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
|
||||
{Name: "N3", Contents: decls[2]},
|
||||
{Name: "N5", Contents: decls[4]},
|
||||
}}, http.StatusNoContent)
|
||||
|
||||
s.DoJSON("GET", "/api/latest/fleet/mdm/profiles", &listMDMConfigProfilesRequest{}, http.StatusOK, &resp)
|
||||
|
||||
require.Len(t, resp.Profiles, 2)
|
||||
require.Equal(t, "N3", resp.Profiles[0].Name)
|
||||
require.Equal(t, "darwin", resp.Profiles[0].Platform)
|
||||
require.Equal(t, "N5", resp.Profiles[1].Name)
|
||||
require.Equal(t, "darwin", resp.Profiles[1].Platform)
|
||||
|
||||
// update the declarations
|
||||
|
||||
s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
|
||||
{Name: "N3", Contents: newDeclBytes(2, `"foo": "bar"`)},
|
||||
{Name: "N5", Contents: newDeclBytes(4, `"bing": "baz"`)},
|
||||
}}, http.StatusNoContent)
|
||||
|
||||
s.DoJSON("GET", "/api/latest/fleet/mdm/profiles", &listMDMConfigProfilesRequest{}, http.StatusOK, &resp)
|
||||
|
||||
require.Len(t, resp.Profiles, 2)
|
||||
require.Equal(t, "N3", resp.Profiles[0].Name)
|
||||
require.Equal(t, "darwin", resp.Profiles[0].Platform)
|
||||
require.Equal(t, "N5", resp.Profiles[1].Name)
|
||||
require.Equal(t, "darwin", resp.Profiles[1].Platform)
|
||||
|
||||
var createResp createLabelResponse
|
||||
s.DoJSON("POST", "/api/latest/fleet/labels", &fleet.LabelPayload{Name: ptr.String("label_1"), Query: ptr.String("select 1")}, http.StatusOK, &createResp)
|
||||
require.NotZero(t, createResp.Label.ID)
|
||||
require.Equal(t, "label_1", createResp.Label.Name)
|
||||
lbl1 := createResp.Label.Label
|
||||
|
||||
s.DoJSON("POST", "/api/latest/fleet/labels", &fleet.LabelPayload{Name: ptr.String("label_2"), Query: ptr.String("select 1")}, http.StatusOK, &createResp)
|
||||
require.NotZero(t, createResp.Label.ID)
|
||||
require.Equal(t, "label_2", createResp.Label.Name)
|
||||
lbl2 := createResp.Label.Label
|
||||
|
||||
// Add with labels
|
||||
s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
|
||||
{Name: "N5", Contents: decls[5], Labels: []string{lbl1.Name, lbl2.Name}},
|
||||
{Name: "N6", Contents: decls[6], Labels: []string{lbl1.Name}},
|
||||
}}, http.StatusNoContent)
|
||||
|
||||
s.DoJSON("GET", "/api/latest/fleet/mdm/profiles", &listMDMConfigProfilesRequest{}, http.StatusOK, &resp)
|
||||
|
||||
require.Len(t, resp.Profiles, 2)
|
||||
require.Equal(t, "N5", resp.Profiles[0].Name)
|
||||
require.Equal(t, "darwin", resp.Profiles[0].Platform)
|
||||
require.Equal(t, "N6", resp.Profiles[1].Name)
|
||||
require.Equal(t, "darwin", resp.Profiles[1].Platform)
|
||||
require.Len(t, resp.Profiles[0].Labels, 2)
|
||||
require.Equal(t, lbl1.Name, resp.Profiles[0].Labels[0].LabelName)
|
||||
require.Equal(t, lbl2.Name, resp.Profiles[0].Labels[1].LabelName)
|
||||
require.Len(t, resp.Profiles[1].Labels, 1)
|
||||
require.Equal(t, lbl1.Name, resp.Profiles[1].Labels[0].LabelName)
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) TestMDMAppleDeviceManagementRequests() {
|
||||
t := s.T()
|
||||
_, mdmDevice := createHostThenEnrollMDM(s.ds, s.server.URL, t)
|
||||
|
||||
calcChecksum := func(source []byte) string {
|
||||
csum := fmt.Sprintf("%x", md5.Sum(source)) //nolint:gosec
|
||||
return strings.ToUpper(csum)
|
||||
}
|
||||
|
||||
t.Cleanup(func() { s.cleanupDeclarations(t) })
|
||||
|
||||
insertDeclaration := func(t *testing.T, decl fleet.MDMAppleDeclaration) {
|
||||
stmt := `
|
||||
INSERT INTO mdm_apple_declarations (
|
||||
declaration_uuid,
|
||||
team_id,
|
||||
identifier,
|
||||
name,
|
||||
raw_json,
|
||||
checksum,
|
||||
created_at,
|
||||
uploaded_at
|
||||
) VALUES (?,?,?,?,?,UNHEX(?),?,?)`
|
||||
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
_, err := q.ExecContext(context.Background(), stmt,
|
||||
decl.DeclarationUUID,
|
||||
decl.TeamID,
|
||||
decl.Identifier,
|
||||
decl.Name,
|
||||
decl.RawJSON,
|
||||
calcChecksum(decl.RawJSON),
|
||||
decl.CreatedAt,
|
||||
decl.UploadedAt,
|
||||
)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
insertHostDeclaration := func(t *testing.T, hostUUID string, decl fleet.MDMAppleDeclaration) {
|
||||
stmt := `
|
||||
INSERT INTO host_mdm_apple_declarations (
|
||||
host_uuid,
|
||||
status,
|
||||
operation_type,
|
||||
checksum,
|
||||
declaration_uuid
|
||||
) VALUES (?,?,?,UNHEX(?),?)`
|
||||
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
_, err := q.ExecContext(context.Background(), stmt,
|
||||
hostUUID,
|
||||
fleet.MDMDeliveryPending,
|
||||
fleet.MDMOperationTypeInstall,
|
||||
calcChecksum(decl.RawJSON),
|
||||
decl.DeclarationUUID,
|
||||
)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// initialize a time to use for our first declaration, subsequent declarations will be
|
||||
// incremented by a minute
|
||||
then := time.Now().UTC().Truncate(time.Second).Add(-1 * time.Hour)
|
||||
|
||||
// insert a declaration with no team
|
||||
noTeamDeclsByUUID := map[string]fleet.MDMAppleDeclaration{
|
||||
"123": {
|
||||
DeclarationUUID: "123",
|
||||
TeamID: ptr.Uint(0),
|
||||
Identifier: "com.example",
|
||||
Name: "Example",
|
||||
RawJSON: json.RawMessage(`{
|
||||
"Type": "com.apple.configuration.declaration-items.test",
|
||||
"Payload": {"foo":"bar"},
|
||||
"Identifier": "com.example"
|
||||
}`),
|
||||
CreatedAt: then,
|
||||
UploadedAt: then,
|
||||
},
|
||||
}
|
||||
insertDeclaration(t, noTeamDeclsByUUID["123"])
|
||||
insertHostDeclaration(t, mdmDevice.UUID, noTeamDeclsByUUID["123"])
|
||||
|
||||
mapDeclsByChecksum := func(byUUID map[string]fleet.MDMAppleDeclaration) map[string]fleet.MDMAppleDeclaration {
|
||||
byChecksum := make(map[string]fleet.MDMAppleDeclaration)
|
||||
for _, d := range byUUID {
|
||||
byChecksum[calcChecksum(d.RawJSON)] = byUUID[d.DeclarationUUID]
|
||||
}
|
||||
return byChecksum
|
||||
}
|
||||
|
||||
parseTokensResp := func(r *http.Response) fleet.MDMAppleDDMTokensResponse {
|
||||
require.NotNil(t, r)
|
||||
b, err := io.ReadAll(r.Body)
|
||||
require.NoError(t, err)
|
||||
defer r.Body.Close()
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(b))
|
||||
// t.Log("body", string(b))
|
||||
|
||||
// unmarsal the response to make sure it's valid
|
||||
var tok fleet.MDMAppleDDMTokensResponse
|
||||
err = json.NewDecoder(r.Body).Decode(&tok)
|
||||
require.NoError(t, err)
|
||||
// t.Log("decoded", tok)
|
||||
|
||||
return tok
|
||||
}
|
||||
|
||||
parseDeclarationItemsResp := func(r *http.Response) fleet.MDMAppleDDMDeclarationItemsResponse {
|
||||
require.NotNil(t, r)
|
||||
b, err := io.ReadAll(r.Body)
|
||||
require.NoError(t, err)
|
||||
defer r.Body.Close()
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(b))
|
||||
// t.Log("body", string(b))
|
||||
|
||||
// unmarsal the response to make sure it's valid
|
||||
var di fleet.MDMAppleDDMDeclarationItemsResponse
|
||||
err = json.NewDecoder(r.Body).Decode(&di)
|
||||
require.NoError(t, err)
|
||||
// t.Log("decoded", di)
|
||||
|
||||
return di
|
||||
}
|
||||
|
||||
assertDeclarationResponse := func(r *http.Response, expected fleet.MDMAppleDeclaration) {
|
||||
require.NotNil(t, r)
|
||||
|
||||
// unmarsal the response and assert it's valid
|
||||
var wantParsed fleet.MDMAppleDDMDeclarationResponse
|
||||
require.NoError(t, json.Unmarshal(expected.RawJSON, &wantParsed))
|
||||
var gotParsed fleet.MDMAppleDDMDeclarationResponse
|
||||
require.NoError(t, json.NewDecoder(r.Body).Decode(&gotParsed))
|
||||
require.EqualValues(t, wantParsed.Payload, gotParsed.Payload)
|
||||
require.Equal(t, calcChecksum(expected.RawJSON), gotParsed.ServerToken)
|
||||
require.Equal(t, expected.Identifier, gotParsed.Identifier)
|
||||
// t.Logf("decoded: %+v", gotParsed)
|
||||
}
|
||||
|
||||
checkTokensResp := func(t *testing.T, r fleet.MDMAppleDDMTokensResponse, expectedTimestamp time.Time, prevToken string) {
|
||||
require.Equal(t, expectedTimestamp, r.SyncTokens.Timestamp)
|
||||
require.NotEmpty(t, r.SyncTokens.DeclarationsToken)
|
||||
require.NotEqual(t, prevToken, r.SyncTokens.DeclarationsToken)
|
||||
}
|
||||
|
||||
checkDeclarationItemsResp := func(t *testing.T, r fleet.MDMAppleDDMDeclarationItemsResponse, expectedDeclTok string, expectedDeclsByChecksum map[string]fleet.MDMAppleDeclaration) {
|
||||
require.Equal(t, expectedDeclTok, r.DeclarationsToken)
|
||||
// TODO(roberto): better assertions
|
||||
require.NotEmpty(t, r.Declarations.Activations)
|
||||
require.Empty(t, r.Declarations.Assets)
|
||||
require.Empty(t, r.Declarations.Management)
|
||||
require.Len(t, r.Declarations.Configurations, len(expectedDeclsByChecksum))
|
||||
for _, m := range r.Declarations.Configurations {
|
||||
d, ok := expectedDeclsByChecksum[m.ServerToken]
|
||||
require.True(t, ok)
|
||||
require.Equal(t, d.Identifier, m.Identifier)
|
||||
}
|
||||
}
|
||||
|
||||
checkRequestsDatabase := func(t *testing.T, messageType, enrollmentID string, expectedCount int) {
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
var count int
|
||||
if err := sqlx.GetContext(
|
||||
context.Background(),
|
||||
q,
|
||||
&count,
|
||||
"SELECT count(*) AS count FROM mdm_apple_declarative_requests WHERE enrollment_id = ? AND message_type = ?",
|
||||
enrollmentID,
|
||||
messageType,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
require.Equal(t, expectedCount, count, "unexpected db row count for declaration requests")
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var currDeclToken string // we'll use this to track the expected token across tests
|
||||
|
||||
t.Run("Tokens", func(t *testing.T) {
|
||||
checkRequestsDatabase(t, "tokens", mdmDevice.UUID, 0)
|
||||
// get tokens, timestamp should be the same as the declaration and token should be non-empty
|
||||
r, err := mdmDevice.DeclarativeManagement("tokens")
|
||||
require.NoError(t, err)
|
||||
parsed := parseTokensResp(r)
|
||||
checkTokensResp(t, parsed, then, "")
|
||||
currDeclToken = parsed.SyncTokens.DeclarationsToken
|
||||
|
||||
// insert a new declaration
|
||||
noTeamDeclsByUUID["456"] = fleet.MDMAppleDeclaration{
|
||||
DeclarationUUID: "456",
|
||||
TeamID: ptr.Uint(0),
|
||||
Identifier: "com.example2",
|
||||
Name: "Example2",
|
||||
RawJSON: json.RawMessage(`{
|
||||
"Type": "com.apple.configuration.declaration-items.test",
|
||||
"Payload": {"foo":"baz"},
|
||||
"Identifier": "com.example2"
|
||||
}`),
|
||||
CreatedAt: then.Add(1 * time.Minute),
|
||||
UploadedAt: then.Add(1 * time.Minute),
|
||||
}
|
||||
insertDeclaration(t, noTeamDeclsByUUID["456"])
|
||||
insertHostDeclaration(t, mdmDevice.UUID, noTeamDeclsByUUID["456"])
|
||||
checkRequestsDatabase(t, "tokens", mdmDevice.UUID, 1)
|
||||
|
||||
// get tokens again, timestamp and token should have changed
|
||||
r, err = mdmDevice.DeclarativeManagement("tokens")
|
||||
require.NoError(t, err)
|
||||
parsed = parseTokensResp(r)
|
||||
checkTokensResp(t, parsed, then.Add(1*time.Minute), currDeclToken)
|
||||
currDeclToken = parsed.SyncTokens.DeclarationsToken
|
||||
checkRequestsDatabase(t, "tokens", mdmDevice.UUID, 2)
|
||||
})
|
||||
|
||||
t.Run("DeclarationItems", func(t *testing.T) {
|
||||
checkRequestsDatabase(t, "declaration-items", mdmDevice.UUID, 0)
|
||||
r, err := mdmDevice.DeclarativeManagement("declaration-items")
|
||||
require.NoError(t, err)
|
||||
checkDeclarationItemsResp(t, parseDeclarationItemsResp(r), currDeclToken, mapDeclsByChecksum(noTeamDeclsByUUID))
|
||||
|
||||
// insert a new declaration
|
||||
noTeamDeclsByUUID["789"] = fleet.MDMAppleDeclaration{
|
||||
DeclarationUUID: "789",
|
||||
TeamID: ptr.Uint(0),
|
||||
Identifier: "com.example3",
|
||||
Name: "Example3",
|
||||
RawJSON: json.RawMessage(`{
|
||||
"Type": "com.apple.configuration.declaration-items.test",
|
||||
"Payload": {"foo":"bang"},
|
||||
"Identifier": "com.example3"
|
||||
}`),
|
||||
CreatedAt: then.Add(2 * time.Minute),
|
||||
UploadedAt: then.Add(2 * time.Minute),
|
||||
}
|
||||
insertDeclaration(t, noTeamDeclsByUUID["789"])
|
||||
insertHostDeclaration(t, mdmDevice.UUID, noTeamDeclsByUUID["789"])
|
||||
checkRequestsDatabase(t, "declaration-items", mdmDevice.UUID, 1)
|
||||
|
||||
// get tokens again, timestamp and token should have changed
|
||||
r, err = mdmDevice.DeclarativeManagement("tokens")
|
||||
require.NoError(t, err)
|
||||
toks := parseTokensResp(r)
|
||||
checkTokensResp(t, toks, then.Add(2*time.Minute), currDeclToken)
|
||||
currDeclToken = toks.SyncTokens.DeclarationsToken
|
||||
|
||||
r, err = mdmDevice.DeclarativeManagement("declaration-items")
|
||||
require.NoError(t, err)
|
||||
checkDeclarationItemsResp(t, parseDeclarationItemsResp(r), currDeclToken, mapDeclsByChecksum(noTeamDeclsByUUID))
|
||||
checkRequestsDatabase(t, "declaration-items", mdmDevice.UUID, 2)
|
||||
})
|
||||
|
||||
t.Run("Status", func(t *testing.T) {
|
||||
checkRequestsDatabase(t, "status", mdmDevice.UUID, 0)
|
||||
_, err := mdmDevice.DeclarativeManagement("status", fleet.MDMAppleDDMStatusReport{})
|
||||
require.NoError(t, err)
|
||||
checkRequestsDatabase(t, "status", mdmDevice.UUID, 1)
|
||||
})
|
||||
|
||||
t.Run("Declaration", func(t *testing.T) {
|
||||
want := noTeamDeclsByUUID["123"]
|
||||
declarationPath := fmt.Sprintf("declaration/%s/%s", "configuration", want.Identifier)
|
||||
checkRequestsDatabase(t, declarationPath, mdmDevice.UUID, 0)
|
||||
r, err := mdmDevice.DeclarativeManagement(declarationPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
assertDeclarationResponse(r, want)
|
||||
|
||||
// insert a new declaration
|
||||
noTeamDeclsByUUID["abc"] = fleet.MDMAppleDeclaration{
|
||||
DeclarationUUID: "abc",
|
||||
TeamID: ptr.Uint(0),
|
||||
Identifier: "com.example4",
|
||||
Name: "Example4",
|
||||
RawJSON: json.RawMessage(`{
|
||||
"Type": "com.apple.configuration.test",
|
||||
"Payload": {"foo":"bar"},
|
||||
"Identifier": "com.example4"
|
||||
}`),
|
||||
CreatedAt: then.Add(3 * time.Minute),
|
||||
UploadedAt: then.Add(3 * time.Minute),
|
||||
}
|
||||
insertDeclaration(t, noTeamDeclsByUUID["abc"])
|
||||
insertHostDeclaration(t, mdmDevice.UUID, noTeamDeclsByUUID["abc"])
|
||||
want = noTeamDeclsByUUID["abc"]
|
||||
r, err = mdmDevice.DeclarativeManagement(fmt.Sprintf("declaration/%s/%s", "configuration", want.Identifier))
|
||||
require.NoError(t, err)
|
||||
checkRequestsDatabase(t, declarationPath, mdmDevice.UUID, 1)
|
||||
|
||||
// try getting a non-existent declaration, should fail 404
|
||||
nonExistantDeclarationPath := fmt.Sprintf("declaration/%s/%s", "configuration", "nonexistent")
|
||||
checkRequestsDatabase(t, nonExistantDeclarationPath, mdmDevice.UUID, 0)
|
||||
_, err = mdmDevice.DeclarativeManagement(nonExistantDeclarationPath)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "404 Not Found")
|
||||
checkRequestsDatabase(t, nonExistantDeclarationPath, mdmDevice.UUID, 1)
|
||||
|
||||
// typo should fail as bad request
|
||||
typoDeclarationPath := fmt.Sprintf("declarations/%s/%s", "configurations", want.Identifier)
|
||||
checkRequestsDatabase(t, typoDeclarationPath, mdmDevice.UUID, 0)
|
||||
_, err = mdmDevice.DeclarativeManagement(typoDeclarationPath)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "400 Bad Request")
|
||||
checkRequestsDatabase(t, typoDeclarationPath, mdmDevice.UUID, 1)
|
||||
|
||||
assertDeclarationResponse(r, want)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) TestAppleDDMReconciliation() {
|
||||
t := s.T()
|
||||
ctx := context.Background()
|
||||
// TODO: use config logger or take into account FLEET_INTEGRATION_TESTS_DISABLE_LOG
|
||||
logger := kitlog.NewJSONLogger(os.Stdout)
|
||||
|
||||
// TODO: use endpoints once those are available.
|
||||
addDeclaration := func(identifier string, teamID uint) {
|
||||
stmt := `
|
||||
INSERT INTO mdm_apple_declarations
|
||||
(declaration_uuid, team_id, identifier, name, raw_json, checksum)
|
||||
VALUES
|
||||
(UUID(), ?, ?, UUID(), ?, HEX(MD5(raw_json)) )`
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error {
|
||||
_, err := tx.ExecContext(ctx, stmt, teamID, identifier, declarationForTest(identifier))
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
deleteDeclaration := func(identifier string, teamID uint) {
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error {
|
||||
_, err := tx.ExecContext(ctx, "DELETE FROM mdm_apple_declarations WHERE team_id = ? AND identifier = ?", teamID, identifier)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// create a team
|
||||
teamName := t.Name() + "team1"
|
||||
team := &fleet.Team{
|
||||
Name: teamName,
|
||||
}
|
||||
var createTeamResp teamResponse
|
||||
s.DoJSON("POST", "/api/latest/fleet/teams", team, http.StatusOK, &createTeamResp)
|
||||
require.NotZero(t, createTeamResp.Team.ID)
|
||||
team = createTeamResp.Team
|
||||
|
||||
// TODO: figure out the best way to do this. We might even consider
|
||||
// starting a different test suite.
|
||||
t.Cleanup(func() { s.cleanupDeclarations(t) })
|
||||
|
||||
checkNoCommands := func(d *mdmtest.TestAppleMDMClient) {
|
||||
cmd, err := d.Idle()
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, cmd)
|
||||
}
|
||||
|
||||
checkDDMSync := func(d *mdmtest.TestAppleMDMClient) {
|
||||
cmd, err := d.Idle()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cmd)
|
||||
require.Equal(t, "DeclarativeManagement", cmd.Command.RequestType)
|
||||
cmd, err = d.Acknowledge(cmd.CommandUUID)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, cmd)
|
||||
_, err = d.DeclarativeManagement("tokens")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// create a windows host
|
||||
_, err := s.ds.NewHost(context.Background(), &fleet.Host{
|
||||
ID: 1,
|
||||
OsqueryHostID: ptr.String("non-macos-host"),
|
||||
NodeKey: ptr.String("non-macos-host"),
|
||||
UUID: uuid.New().String(),
|
||||
Hostname: fmt.Sprintf("%sfoo.local.non.macos", t.Name()),
|
||||
Platform: "windows",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// create a windows host that's enrolled in MDM
|
||||
_, _ = createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)
|
||||
|
||||
// create a linux host
|
||||
_, err = s.ds.NewHost(context.Background(), &fleet.Host{
|
||||
ID: 2,
|
||||
OsqueryHostID: ptr.String("linux-host"),
|
||||
NodeKey: ptr.String("linux-host"),
|
||||
UUID: uuid.New().String(),
|
||||
Hostname: fmt.Sprintf("%sfoo.local.linux", t.Name()),
|
||||
Platform: "linux",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// create a host that's not enrolled into MDM
|
||||
_, err = s.ds.NewHost(context.Background(), &fleet.Host{
|
||||
ID: 2,
|
||||
OsqueryHostID: ptr.String("not-mdm-enrolled"),
|
||||
NodeKey: ptr.String("not-mdm-enrolled"),
|
||||
UUID: uuid.New().String(),
|
||||
Hostname: fmt.Sprintf("%sfoo.local.not.enrolled", t.Name()),
|
||||
Platform: "darwin",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// create a host and then enroll in MDM.
|
||||
mdmHost, device := createHostThenEnrollMDM(s.ds, s.server.URL, t)
|
||||
|
||||
// trigger the reconciler, no error
|
||||
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
|
||||
require.NoError(t, err)
|
||||
|
||||
// declarativeManagement command is not sent.
|
||||
checkNoCommands(device)
|
||||
|
||||
// add global declarations
|
||||
addDeclaration("I1", 0)
|
||||
addDeclaration("I2", 0)
|
||||
|
||||
// reconcile again, this time new declarations were added
|
||||
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
|
||||
require.NoError(t, err)
|
||||
|
||||
// TODO: check command is pending
|
||||
|
||||
// declarativeManagement command is sent
|
||||
checkDDMSync(device)
|
||||
|
||||
// reconcile again, commands for the uploaded declarations are already sent
|
||||
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
|
||||
require.NoError(t, err)
|
||||
// no new commands are sent
|
||||
checkNoCommands(device)
|
||||
|
||||
// delete a declaration
|
||||
deleteDeclaration("I1", 0)
|
||||
// reconcile again
|
||||
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
|
||||
require.NoError(t, err)
|
||||
// a DDM sync is triggered
|
||||
checkDDMSync(device)
|
||||
|
||||
// add a new host
|
||||
_, deviceTwo := createHostThenEnrollMDM(s.ds, s.server.URL, t)
|
||||
// reconcile again
|
||||
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
|
||||
require.NoError(t, err)
|
||||
// DDM sync is triggered only for the new host
|
||||
checkNoCommands(device)
|
||||
checkDDMSync(deviceTwo)
|
||||
|
||||
// add device to the team
|
||||
s.Do("POST", "/api/v1/fleet/hosts/transfer",
|
||||
addHostsToTeamRequest{TeamID: &team.ID, HostIDs: []uint{mdmHost.ID}}, http.StatusOK)
|
||||
|
||||
// reconcile
|
||||
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
|
||||
require.NoError(t, err)
|
||||
|
||||
// DDM sync is triggered only for the transferred host
|
||||
// because the team doesn't have any declarations
|
||||
checkDDMSync(device)
|
||||
checkNoCommands(deviceTwo)
|
||||
|
||||
// reconcile
|
||||
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
|
||||
require.NoError(t, err)
|
||||
// nobody receives commands this time
|
||||
checkNoCommands(device)
|
||||
checkNoCommands(deviceTwo)
|
||||
|
||||
// add declarations to the team
|
||||
addDeclaration("I1", team.ID)
|
||||
addDeclaration("I2", team.ID)
|
||||
|
||||
// reconcile
|
||||
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
|
||||
require.NoError(t, err)
|
||||
// DDM sync is triggered for the host in the team
|
||||
checkDDMSync(device)
|
||||
checkNoCommands(deviceTwo)
|
||||
|
||||
// add a new host, this one belongs to the team
|
||||
mdmHostThree, deviceThree := createHostThenEnrollMDM(s.ds, s.server.URL, t)
|
||||
s.Do("POST", "/api/v1/fleet/hosts/transfer",
|
||||
addHostsToTeamRequest{TeamID: &team.ID, HostIDs: []uint{mdmHostThree.ID}}, http.StatusOK)
|
||||
|
||||
// reconcile
|
||||
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
|
||||
require.NoError(t, err)
|
||||
// DDM sync is triggered only for the new host
|
||||
checkNoCommands(device)
|
||||
checkNoCommands(deviceTwo)
|
||||
checkDDMSync(deviceThree)
|
||||
|
||||
// no new commands after another reconciliation
|
||||
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
|
||||
require.NoError(t, err)
|
||||
checkNoCommands(device)
|
||||
checkNoCommands(deviceTwo)
|
||||
checkNoCommands(deviceThree)
|
||||
|
||||
// TODO: use proper APIs for this
|
||||
// add a new label + label declaration
|
||||
addDeclaration("I3", team.ID)
|
||||
label, err := s.ds.NewLabel(ctx, &fleet.Label{Name: t.Name(), Query: "select 1;"})
|
||||
require.NoError(t, err)
|
||||
// update label with host membership
|
||||
mysql.ExecAdhocSQL(
|
||||
t, s.ds, func(db sqlx.ExtContext) error {
|
||||
_, err := db.ExecContext(
|
||||
context.Background(),
|
||||
"INSERT IGNORE INTO label_membership (host_id, label_id) VALUES (?, ?)",
|
||||
mdmHostThree.ID,
|
||||
label.ID,
|
||||
)
|
||||
return err
|
||||
},
|
||||
)
|
||||
|
||||
// update declaration <-> label mapping
|
||||
mysql.ExecAdhocSQL(
|
||||
t, s.ds, func(db sqlx.ExtContext) error {
|
||||
_, err := db.ExecContext(
|
||||
context.Background(),
|
||||
`INSERT INTO
|
||||
mdm_declaration_labels (apple_declaration_uuid, label_name, label_id)
|
||||
VALUES ((SELECT declaration_uuid FROM mdm_apple_declarations WHERE team_id = ? and identifier = ?), ?, ?)`,
|
||||
team.ID,
|
||||
"I3",
|
||||
label.Name,
|
||||
label.ID,
|
||||
)
|
||||
return err
|
||||
},
|
||||
)
|
||||
|
||||
// reconcile
|
||||
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
|
||||
require.NoError(t, err)
|
||||
// DDM sync is triggered only for the host with the label
|
||||
checkNoCommands(device)
|
||||
checkNoCommands(deviceTwo)
|
||||
checkDDMSync(deviceThree)
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) TestAppleDDMStatusReport() {
|
||||
t := s.T()
|
||||
ctx := context.Background()
|
||||
// TODO: use config logger or take into account FLEET_INTEGRATION_TESTS_DISABLE_LOG
|
||||
logger := kitlog.NewJSONLogger(os.Stdout)
|
||||
|
||||
// TODO: figure out the best way to do this. We might even consider
|
||||
// starting a different test suite.
|
||||
t.Cleanup(func() { s.cleanupDeclarations(t) })
|
||||
|
||||
assertHostDeclarations := func(hostUUID string, wantDecls []*fleet.MDMAppleHostDeclaration) {
|
||||
var gotDecls []*fleet.MDMAppleHostDeclaration
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
return sqlx.SelectContext(context.Background(), q, &gotDecls, `SELECT declaration_identifier, status, operation_type FROM host_mdm_apple_declarations WHERE host_uuid = ?`, hostUUID)
|
||||
})
|
||||
require.ElementsMatch(t, wantDecls, gotDecls)
|
||||
}
|
||||
|
||||
// create a host and then enroll in MDM.
|
||||
mdmHost, device := createHostThenEnrollMDM(s.ds, s.server.URL, t)
|
||||
|
||||
declarations := []fleet.MDMProfileBatchPayload{
|
||||
{Name: "N1.json", Contents: declarationForTest("I1")},
|
||||
{Name: "N2.json", Contents: declarationForTest("I2")},
|
||||
}
|
||||
// add global declarations
|
||||
s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: declarations}, http.StatusNoContent)
|
||||
|
||||
// reconcile profiles
|
||||
err := ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
|
||||
require.NoError(t, err)
|
||||
|
||||
// declarations are ("install", "pending") after the cron run
|
||||
assertHostDeclarations(mdmHost.UUID, []*fleet.MDMAppleHostDeclaration{
|
||||
{Identifier: "I1", Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall},
|
||||
{Identifier: "I2", Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall},
|
||||
})
|
||||
|
||||
// host gets a DDM sync call
|
||||
cmd, err := device.Idle()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "DeclarativeManagement", cmd.Command.RequestType)
|
||||
_, err = device.Acknowledge(cmd.CommandUUID)
|
||||
require.NoError(t, err)
|
||||
|
||||
r, err := device.DeclarativeManagement("declaration-items")
|
||||
require.NoError(t, err)
|
||||
body, err := io.ReadAll(r.Body)
|
||||
require.NoError(t, err)
|
||||
var items fleet.MDMAppleDDMDeclarationItemsResponse
|
||||
require.NoError(t, json.Unmarshal(body, &items))
|
||||
|
||||
var i1ServerToken, i2ServerToken string
|
||||
for _, d := range items.Declarations.Configurations {
|
||||
switch d.Identifier {
|
||||
case "I1":
|
||||
i1ServerToken = d.ServerToken
|
||||
case "I2":
|
||||
i2ServerToken = d.ServerToken
|
||||
}
|
||||
}
|
||||
|
||||
// declarations are ("install", "verifying") after the ack
|
||||
assertHostDeclarations(mdmHost.UUID, []*fleet.MDMAppleHostDeclaration{
|
||||
{Identifier: "I1", Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall},
|
||||
{Identifier: "I2", Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall},
|
||||
})
|
||||
|
||||
// host sends a partial DDM report
|
||||
report := fleet.MDMAppleDDMStatusReport{}
|
||||
report.StatusItems.Management.Declarations.Configurations = []fleet.MDMAppleDDMStatusDeclaration{
|
||||
{Active: true, Valid: fleet.MDMAppleDeclarationValid, Identifier: "I1", ServerToken: i1ServerToken},
|
||||
}
|
||||
_, err = device.DeclarativeManagement("status", report)
|
||||
require.NoError(t, err)
|
||||
assertHostDeclarations(mdmHost.UUID, []*fleet.MDMAppleHostDeclaration{
|
||||
{Identifier: "I1", Status: &fleet.MDMDeliveryVerified, OperationType: fleet.MDMOperationTypeInstall},
|
||||
{Identifier: "I2", Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall},
|
||||
})
|
||||
|
||||
// host sends a report with a wrong (could be old) server token for I2, nothing changes
|
||||
report = fleet.MDMAppleDDMStatusReport{}
|
||||
report.StatusItems.Management.Declarations.Configurations = []fleet.MDMAppleDDMStatusDeclaration{
|
||||
{Active: true, Valid: fleet.MDMAppleDeclarationValid, Identifier: "I2", ServerToken: "foo"},
|
||||
}
|
||||
_, err = device.DeclarativeManagement("status", report)
|
||||
require.NoError(t, err)
|
||||
assertHostDeclarations(mdmHost.UUID, []*fleet.MDMAppleHostDeclaration{
|
||||
{Identifier: "I1", Status: &fleet.MDMDeliveryVerified, OperationType: fleet.MDMOperationTypeInstall},
|
||||
{Identifier: "I2", Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall},
|
||||
})
|
||||
|
||||
// host sends a full report, declaration I2 is invalid
|
||||
report = fleet.MDMAppleDDMStatusReport{}
|
||||
report.StatusItems.Management.Declarations.Configurations = []fleet.MDMAppleDDMStatusDeclaration{
|
||||
{Active: true, Valid: fleet.MDMAppleDeclarationValid, Identifier: "I1", ServerToken: i1ServerToken},
|
||||
{Active: false, Valid: fleet.MDMAppleDeclarationInvalid, Identifier: "I2", ServerToken: i2ServerToken},
|
||||
}
|
||||
_, err = device.DeclarativeManagement("status", report)
|
||||
require.NoError(t, err)
|
||||
assertHostDeclarations(mdmHost.UUID, []*fleet.MDMAppleHostDeclaration{
|
||||
{Identifier: "I1", Status: &fleet.MDMDeliveryVerified, OperationType: fleet.MDMOperationTypeInstall},
|
||||
{Identifier: "I2", Status: &fleet.MDMDeliveryFailed, OperationType: fleet.MDMOperationTypeInstall},
|
||||
})
|
||||
|
||||
// do a batch request, this time I2 is deleted
|
||||
declarations = []fleet.MDMProfileBatchPayload{
|
||||
{Name: "N1.json", Contents: declarationForTest("I1")},
|
||||
}
|
||||
s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: declarations}, http.StatusNoContent)
|
||||
|
||||
// reconcile profiles
|
||||
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
|
||||
require.NoError(t, err)
|
||||
assertHostDeclarations(mdmHost.UUID, []*fleet.MDMAppleHostDeclaration{
|
||||
{Identifier: "I1", Status: &fleet.MDMDeliveryVerified, OperationType: fleet.MDMOperationTypeInstall},
|
||||
{Identifier: "I2", Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove},
|
||||
})
|
||||
|
||||
// host sends a report, declaration I2 is removed from the hosts_* table
|
||||
report = fleet.MDMAppleDDMStatusReport{}
|
||||
report.StatusItems.Management.Declarations.Configurations = []fleet.MDMAppleDDMStatusDeclaration{
|
||||
{Active: true, Valid: fleet.MDMAppleDeclarationValid, Identifier: "I1", ServerToken: i1ServerToken},
|
||||
}
|
||||
_, err = device.DeclarativeManagement("status", report)
|
||||
require.NoError(t, err)
|
||||
assertHostDeclarations(mdmHost.UUID, []*fleet.MDMAppleHostDeclaration{
|
||||
{Identifier: "I1", Status: &fleet.MDMDeliveryVerified, OperationType: fleet.MDMOperationTypeInstall},
|
||||
})
|
||||
|
||||
// host sends a report, declaration I1 is failing after a while
|
||||
report = fleet.MDMAppleDDMStatusReport{}
|
||||
report.StatusItems.Management.Declarations.Configurations = []fleet.MDMAppleDDMStatusDeclaration{
|
||||
{Active: false, Valid: fleet.MDMAppleDeclarationInvalid, Identifier: "I1", ServerToken: i1ServerToken},
|
||||
}
|
||||
_, err = device.DeclarativeManagement("status", report)
|
||||
require.NoError(t, err)
|
||||
assertHostDeclarations(mdmHost.UUID, []*fleet.MDMAppleHostDeclaration{
|
||||
{Identifier: "I1", Status: &fleet.MDMDeliveryFailed, OperationType: fleet.MDMOperationTypeInstall},
|
||||
})
|
||||
}
|
||||
|
||||
func declarationForTest(identifier string) []byte {
|
||||
return []byte(fmt.Sprintf(`
|
||||
{
|
||||
"Type": "com.apple.configuration.management.test",
|
||||
"Payload": {
|
||||
"Echo": "foo"
|
||||
},
|
||||
"Identifier": "%s"
|
||||
}`, identifier))
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) cleanupDeclarations(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
// TODO: figure out the best way to do this. We might even consider
|
||||
// starting a different test suite.
|
||||
// delete declarations to not affect other tests
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error {
|
||||
_, err := tx.ExecContext(ctx, "DELETE FROM mdm_apple_declarations")
|
||||
return err
|
||||
})
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error {
|
||||
_, err := tx.ExecContext(ctx, "DELETE FROM host_mdm_apple_declarations")
|
||||
return err
|
||||
})
|
||||
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/server/worker"
|
||||
kitlog "github.com/go-kit/log"
|
||||
"github.com/google/uuid"
|
||||
"github.com/groob/plist"
|
||||
"github.com/jmoiron/sqlx"
|
||||
micromdm "github.com/micromdm/micromdm/mdm/mdm"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
|
@ -264,7 +265,9 @@ func (s *integrationMDMTestSuite) runDEPEnrollReleaseDeviceTest(t *testing.T, de
|
|||
//default:
|
||||
// fmt.Println(">>>> device received command: ", cmd.CommandUUID, cmd.Command.RequestType)
|
||||
//}
|
||||
cmds = append(cmds, cmd)
|
||||
var fullCmd micromdm.CommandPayload
|
||||
require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
|
||||
cmds = append(cmds, &fullCmd)
|
||||
cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
|
@ -327,7 +330,9 @@ func (s *integrationMDMTestSuite) runDEPEnrollReleaseDeviceTest(t *testing.T, de
|
|||
cmd, err = mdmDevice.Idle()
|
||||
require.NoError(t, err)
|
||||
for cmd != nil {
|
||||
cmds = append(cmds, cmd)
|
||||
var fullCmd micromdm.CommandPayload
|
||||
require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
|
||||
cmds = append(cmds, &fullCmd)
|
||||
cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
|
@ -374,12 +379,14 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() {
|
|||
cmd, err := mdmDevice.Idle()
|
||||
require.NoError(t, err)
|
||||
for cmd != nil {
|
||||
if cmd.Command.RequestType == "InstallEnterpriseApplication" &&
|
||||
cmd.Command.InstallEnterpriseApplication.ManifestURL != nil &&
|
||||
strings.Contains(*cmd.Command.InstallEnterpriseApplication.ManifestURL, apple_mdm.FleetdPublicManifestURL) {
|
||||
fleetdCmd = cmd
|
||||
var fullCmd micromdm.CommandPayload
|
||||
require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
|
||||
if fullCmd.Command.RequestType == "InstallEnterpriseApplication" &&
|
||||
fullCmd.Command.InstallEnterpriseApplication.ManifestURL != nil &&
|
||||
strings.Contains(*fullCmd.Command.InstallEnterpriseApplication.ManifestURL, apple_mdm.FleetdPublicManifestURL) {
|
||||
fleetdCmd = &fullCmd
|
||||
} else if cmd.Command.RequestType == "InstallProfile" {
|
||||
installProfileCmd = cmd
|
||||
installProfileCmd = &fullCmd
|
||||
}
|
||||
cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
|
||||
require.NoError(t, err)
|
||||
|
|
|
|||
|
|
@ -1272,7 +1272,7 @@ func (s *integrationMDMTestSuite) TestWindowsProfileRetries() {
|
|||
}
|
||||
|
||||
func checkNextPayloads(t *testing.T, mdmDevice *mdmtest.TestAppleMDMClient, forceDeviceErr bool) ([][]byte, []string) {
|
||||
var cmd *micromdm.CommandPayload
|
||||
var cmd *mdm.Command
|
||||
var err error
|
||||
installs := [][]byte{}
|
||||
removes := []string{}
|
||||
|
|
@ -1297,11 +1297,13 @@ func checkNextPayloads(t *testing.T, mdmDevice *mdmtest.TestAppleMDMClient, forc
|
|||
break
|
||||
}
|
||||
|
||||
var fullCmd micromdm.CommandPayload
|
||||
require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
|
||||
switch cmd.Command.RequestType {
|
||||
case "InstallProfile":
|
||||
installs = append(installs, cmd.Command.InstallProfile.Payload)
|
||||
installs = append(installs, fullCmd.Command.InstallProfile.Payload)
|
||||
case "RemoveProfile":
|
||||
removes = append(removes, cmd.Command.RemoveProfile.Identifier)
|
||||
removes = append(removes, fullCmd.Command.RemoveProfile.Identifier)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -2048,9 +2050,14 @@ func (s *integrationMDMTestSuite) TestAppleMDMDeviceEnrollment() {
|
|||
mdmDeviceA := mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo)
|
||||
err := mdmDeviceA.Enroll()
|
||||
require.NoError(t, err)
|
||||
s.lastActivityOfTypeMatches(fleet.ActivityTypeMDMEnrolled{}.ActivityName(),
|
||||
fmt.Sprintf(`{"host_serial": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false, "mdm_platform": "apple"}`, mdmDeviceA.SerialNumber, mdmDeviceA.Model, mdmDeviceA.SerialNumber), 0)
|
||||
|
||||
mdmDeviceB := mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo)
|
||||
err = mdmDeviceB.Enroll()
|
||||
require.NoError(t, err)
|
||||
s.lastActivityOfTypeMatches(fleet.ActivityTypeMDMEnrolled{}.ActivityName(),
|
||||
fmt.Sprintf(`{"host_serial": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false, "mdm_platform": "apple"}`, mdmDeviceB.SerialNumber, mdmDeviceB.Model, mdmDeviceB.SerialNumber), 0)
|
||||
|
||||
// Find the ID of Fleet's MDM solution
|
||||
var mdmID uint
|
||||
|
|
@ -2079,23 +2086,6 @@ func (s *integrationMDMTestSuite) TestAppleMDMDeviceEnrollment() {
|
|||
}
|
||||
}
|
||||
|
||||
// Activities are generated for each device
|
||||
activities := listActivitiesResponse{}
|
||||
s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &activities, "order_key", "created_at")
|
||||
require.GreaterOrEqual(t, len(activities.Activities), 2)
|
||||
|
||||
details := []*json.RawMessage{}
|
||||
for _, activity := range activities.Activities {
|
||||
if activity.Type == "mdm_enrolled" {
|
||||
require.Nil(t, activity.ActorID)
|
||||
require.Nil(t, activity.ActorFullName)
|
||||
details = append(details, activity.Details)
|
||||
}
|
||||
}
|
||||
require.Len(t, details, 2)
|
||||
require.JSONEq(t, fmt.Sprintf(`{"host_serial": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false, "mdm_platform": "apple"}`, mdmDeviceA.SerialNumber, mdmDeviceA.Model, mdmDeviceA.SerialNumber), string(*details[len(details)-2]))
|
||||
require.JSONEq(t, fmt.Sprintf(`{"host_serial": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false, "mdm_platform": "apple"}`, mdmDeviceB.SerialNumber, mdmDeviceB.Model, mdmDeviceB.SerialNumber), string(*details[len(details)-1]))
|
||||
|
||||
// set an enroll secret
|
||||
var applyResp applyEnrollSecretSpecResponse
|
||||
s.DoJSON("POST", "/api/latest/fleet/spec/enroll_secret", applyEnrollSecretSpecRequest{
|
||||
|
|
@ -2132,7 +2122,7 @@ func (s *integrationMDMTestSuite) TestAppleMDMDeviceEnrollment() {
|
|||
require.NoError(t, err)
|
||||
|
||||
// An activity is created
|
||||
activities = listActivitiesResponse{}
|
||||
activities := listActivitiesResponse{}
|
||||
s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &activities)
|
||||
|
||||
found := false
|
||||
|
|
@ -2141,7 +2131,6 @@ func (s *integrationMDMTestSuite) TestAppleMDMDeviceEnrollment() {
|
|||
found = true
|
||||
require.Nil(t, activity.ActorID)
|
||||
require.Nil(t, activity.ActorFullName)
|
||||
details = append(details, activity.Details)
|
||||
require.JSONEq(t, fmt.Sprintf(`{"host_serial": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false}`, mdmDeviceA.SerialNumber, mdmDeviceA.Model, mdmDeviceA.SerialNumber), string(*activity.Details))
|
||||
}
|
||||
}
|
||||
|
|
@ -5168,9 +5157,13 @@ func (s *integrationMDMTestSuite) TestBootstrapPackageStatus() {
|
|||
cmd, err := d.device.Idle()
|
||||
require.NoError(t, err)
|
||||
for cmd != nil {
|
||||
var fullCmd micromdm.CommandPayload
|
||||
require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
|
||||
|
||||
// if the command is to install the bootstrap package
|
||||
if manifest := cmd.Command.InstallEnterpriseApplication.Manifest; manifest != nil {
|
||||
if manifest := fullCmd.Command.InstallEnterpriseApplication.Manifest; manifest != nil {
|
||||
require.Equal(t, "InstallEnterpriseApplication", cmd.Command.RequestType)
|
||||
require.NotNil(t, manifest)
|
||||
require.Equal(t, "software-package", (*manifest).ManifestItems[0].Assets[0].Kind)
|
||||
wantURL, err := bp.URL(s.server.URL)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -6867,7 +6860,7 @@ func (s *integrationMDMTestSuite) TestSSO() {
|
|||
s.runWorker()
|
||||
|
||||
// ask for commands and verify that we get AccountConfiguration
|
||||
var accCmd *micromdm.CommandPayload
|
||||
var accCmd *mdm.Command
|
||||
cmd, err := mdmDevice.Idle()
|
||||
require.NoError(t, err)
|
||||
for cmd != nil {
|
||||
|
|
@ -6879,9 +6872,12 @@ func (s *integrationMDMTestSuite) TestSSO() {
|
|||
}
|
||||
require.NotNil(t, accCmd)
|
||||
require.NotNil(t, accCmd.Command)
|
||||
require.True(t, accCmd.Command.AccountConfiguration.LockPrimaryAccountInfo)
|
||||
require.Equal(t, "SSO User 1", accCmd.Command.AccountConfiguration.PrimaryAccountFullName)
|
||||
require.Equal(t, "sso_user", accCmd.Command.AccountConfiguration.PrimaryAccountUserName)
|
||||
|
||||
var fullAccCmd *micromdm.CommandPayload
|
||||
require.NoError(t, plist.Unmarshal(accCmd.Raw, &fullAccCmd))
|
||||
require.True(t, fullAccCmd.Command.AccountConfiguration.LockPrimaryAccountInfo)
|
||||
require.Equal(t, "SSO User 1", fullAccCmd.Command.AccountConfiguration.PrimaryAccountFullName)
|
||||
require.Equal(t, "sso_user", fullAccCmd.Command.AccountConfiguration.PrimaryAccountUserName)
|
||||
|
||||
// report host details for the device
|
||||
var hostResp getHostResponse
|
||||
|
|
@ -8725,6 +8721,8 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() {
|
|||
testTeam, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "TestTeam"})
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() { s.cleanupDeclarations(t) })
|
||||
|
||||
assertAppleProfile := func(filename, name, ident string, teamID uint, labelNames []string, wantStatus int, wantErrMsg string) string {
|
||||
fields := map[string][]string{
|
||||
"labels": labelNames,
|
||||
|
|
@ -8750,6 +8748,39 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() {
|
|||
require.Equal(t, "a", string(resp.ProfileUUID[0]))
|
||||
return resp.ProfileUUID
|
||||
}
|
||||
assertAppleDeclaration := func(filename, ident string, teamID uint, labelNames []string, wantStatus int, wantErrMsg string) string {
|
||||
fields := map[string][]string{
|
||||
"labels": labelNames,
|
||||
}
|
||||
if teamID > 0 {
|
||||
fields["team_id"] = []string{fmt.Sprintf("%d", teamID)}
|
||||
}
|
||||
|
||||
bytes := []byte(fmt.Sprintf(`{
|
||||
"Type": "com.apple.configuration.foo",
|
||||
"Payload": {
|
||||
"Echo": "f1337"
|
||||
},
|
||||
"Identifier": "%s"
|
||||
}`, ident))
|
||||
|
||||
body, headers := generateNewProfileMultipartRequest(t, filename, bytes, s.token, fields)
|
||||
res := s.DoRawWithHeaders("POST", "/api/latest/fleet/configuration_profiles", body.Bytes(), wantStatus, headers)
|
||||
|
||||
if wantErrMsg != "" {
|
||||
errMsg := extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, wantErrMsg)
|
||||
return ""
|
||||
}
|
||||
|
||||
var resp newMDMConfigProfileResponse
|
||||
err := json.NewDecoder(res.Body).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, resp.ProfileUUID)
|
||||
require.Equal(t, fleet.MDMAppleDeclarationUUIDPrefix, string(resp.ProfileUUID[0]))
|
||||
return resp.ProfileUUID
|
||||
}
|
||||
|
||||
createAppleProfile := func(name, ident string, teamID uint, labelNames []string) string {
|
||||
uid := assertAppleProfile(name+".mobileconfig", name, ident, teamID, labelNames, http.StatusOK, "")
|
||||
|
||||
|
|
@ -8764,6 +8795,20 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() {
|
|||
return uid
|
||||
}
|
||||
|
||||
createAppleDeclaration := func(name, ident string, teamID uint, labelNames []string) string {
|
||||
uid := assertAppleDeclaration(name+".json", ident, teamID, labelNames, http.StatusOK, "")
|
||||
|
||||
var wantJSON string
|
||||
if teamID == 0 {
|
||||
wantJSON = fmt.Sprintf(`{"team_id": null, "team_name": null, "profile_name": %q, "identifier": %q}`, name, ident)
|
||||
} else {
|
||||
wantJSON = fmt.Sprintf(`{"team_id": %d, "team_name": %q, "profile_name": %q, "identifier": %q}`, teamID, testTeam.Name, name, ident)
|
||||
}
|
||||
s.lastActivityOfTypeMatches(fleet.ActivityTypeCreatedDeclarationProfile{}.ActivityName(), wantJSON, 0)
|
||||
|
||||
return uid
|
||||
}
|
||||
|
||||
assertWindowsProfile := func(filename, locURI string, teamID uint, labelNames []string, wantStatus int, wantErrMsg string) string {
|
||||
fields := map[string][]string{
|
||||
"labels": labelNames,
|
||||
|
|
@ -8831,9 +8876,27 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() {
|
|||
// but no conflict for no-team
|
||||
assertAppleProfile("win-team-profile.mobileconfig", "win-team-profile", "test-team-ident-2", 0, nil, http.StatusOK, "")
|
||||
|
||||
// add some macOS declarations
|
||||
createAppleDeclaration("apple-declaration", "test-declaration-ident", 0, nil)
|
||||
// identifier must be unique, it conflicts with existing declaration
|
||||
assertAppleDeclaration("apple-declaration.json", "test-declaration-ident", 0, nil, http.StatusConflict, "test-declaration-ident already exists")
|
||||
// name is pulled from filename, it conflicts with existing declaration
|
||||
assertAppleDeclaration("apple-declaration.json", "test-declaration-ident-2", 0, nil, http.StatusConflict, "apple-declaration already exists")
|
||||
// uniqueness is checked only within team, so it's fine to have the same name and identifier in different teams
|
||||
assertAppleDeclaration("apple-declaration.json", "test-declaration-ident", testTeam.ID, nil, http.StatusOK, "")
|
||||
// name is pulled from filename, it conflicts with existing macOS config profile
|
||||
assertAppleDeclaration("apple-global-profile.json", "test-declaration-ident-2", 0, nil, http.StatusConflict, "apple-global-profile already exists")
|
||||
// name is pulled from filename, it conflicts with existing macOS config profile
|
||||
assertAppleDeclaration("win-global-profile.json", "test-declaration-ident-2", 0, nil, http.StatusConflict, "win-global-profile already exists")
|
||||
// windows profile name conflicts with existing declaration
|
||||
assertWindowsProfile("apple-declaration.xml", "./Test", 0, nil, http.StatusConflict, "Couldn't upload. A configuration profile with this name already exists.")
|
||||
// macOS profile name conflicts with existing declaration
|
||||
assertAppleProfile("apple-declaration.mobileconfig", "apple-declaration", "test-declaration-ident", 0, nil, http.StatusConflict, "Couldn't upload. A configuration profile with this name already exists.")
|
||||
|
||||
// not an xml nor mobileconfig file
|
||||
assertWindowsProfile("foo.txt", "./Test", 0, nil, http.StatusBadRequest, "Couldn't upload. The file should be a .mobileconfig or .xml file.")
|
||||
assertAppleProfile("foo.txt", "foo", "foo-ident", 0, nil, http.StatusBadRequest, "Couldn't upload. The file should be a .mobileconfig or .xml file.")
|
||||
assertWindowsProfile("foo.txt", "./Test", 0, nil, http.StatusBadRequest, "Couldn't add profile. The file should be a .mobileconfig, XML, or JSON file.")
|
||||
assertAppleProfile("foo.txt", "foo", "foo-ident", 0, nil, http.StatusBadRequest, "Couldn't add profile. The file should be a .mobileconfig, XML, or JSON file.")
|
||||
assertAppleDeclaration("foo.txt", "foo-ident", 0, nil, http.StatusBadRequest, "Couldn't add profile. The file should be a .mobileconfig, XML, or JSON file.")
|
||||
|
||||
// Windows-reserved LocURI
|
||||
assertWindowsProfile("bitlocker.xml", syncml.FleetBitLockerTargetLocURI, 0, nil, http.StatusBadRequest, "Couldn't upload. Custom configuration profiles can't include BitLocker settings.")
|
||||
|
|
@ -8842,11 +8905,13 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() {
|
|||
// Fleet-reserved profiles
|
||||
for name := range servermdm.FleetReservedProfileNames() {
|
||||
assertAppleProfile(name+".mobileconfig", name, name+"-ident", 0, nil, http.StatusBadRequest, fmt.Sprintf(`name %s is not allowed`, name))
|
||||
assertAppleDeclaration(name+".json", name+"-ident", 0, nil, http.StatusBadRequest, fmt.Sprintf(`name %q is not allowed`, name))
|
||||
assertWindowsProfile(name+".xml", "./Test", 0, nil, http.StatusBadRequest, fmt.Sprintf(`Couldn't upload. Profile name %q is not allowed.`, name))
|
||||
}
|
||||
|
||||
// profiles with non-existent labels
|
||||
assertAppleProfile("apple-profile-with-labels.mobileconfig", "apple-profile-with-labels", "ident-with-labels", 0, []string{"does-not-exist"}, http.StatusBadRequest, "some or all the labels provided don't exist")
|
||||
assertAppleDeclaration("apple-declaration-with-labels.json", "ident-with-labels", 0, []string{"does-not-exist"}, http.StatusBadRequest, "some or all the labels provided don't exist")
|
||||
assertWindowsProfile("win-profile-with-labels.xml", "./Test", 0, []string{"does-not-exist"}, http.StatusBadRequest, "some or all the labels provided don't exist")
|
||||
|
||||
// create a couple of labels
|
||||
|
|
@ -8859,26 +8924,33 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() {
|
|||
|
||||
// profiles mixing existent and non-existent labels
|
||||
assertAppleProfile("apple-profile-with-labels.mobileconfig", "apple-profile-with-labels", "ident-with-labels", 0, []string{"does-not-exist", "foo"}, http.StatusBadRequest, "some or all the labels provided don't exist")
|
||||
assertAppleDeclaration("apple-declaration-with-labels.json", "ident-with-labels", 0, []string{"does-not-exist", "foo"}, http.StatusBadRequest, "some or all the labels provided don't exist")
|
||||
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 valid labels
|
||||
uuidAppleWithLabel := assertAppleProfile("apple-profile-with-labels.mobileconfig", "apple-profile-with-labels", "ident-with-labels", 0, []string{"foo"}, 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, "")
|
||||
|
||||
// verify that the label associations have been created
|
||||
// TODO: update when we have datastore methods to get this data
|
||||
var profileLabels []fleet.ConfigurationProfileLabel
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
stmt := `SELECT COALESCE(apple_profile_uuid, windows_profile_uuid) as profile_uuid, label_name, label_id FROM mdm_configuration_profile_labels`
|
||||
stmt := `
|
||||
SELECT COALESCE(apple_profile_uuid, windows_profile_uuid) as profile_uuid, label_name, COALESCE(label_id, 0) as label_id
|
||||
FROM mdm_configuration_profile_labels
|
||||
UNION SELECT apple_declaration_uuid as profile_uuid, label_name, COALESCE(label_id, 0) as label_id
|
||||
FROM mdm_declaration_labels ORDER BY profile_uuid, label_name;`
|
||||
return sqlx.SelectContext(context.Background(), q, &profileLabels, stmt)
|
||||
})
|
||||
|
||||
require.NotEmpty(t, profileLabels)
|
||||
require.Len(t, profileLabels, 3)
|
||||
require.Len(t, profileLabels, 4)
|
||||
require.ElementsMatch(
|
||||
t,
|
||||
[]fleet.ConfigurationProfileLabel{
|
||||
{ProfileUUID: uuidAppleWithLabel, LabelName: labelFoo.Name, LabelID: labelFoo.ID},
|
||||
{ProfileUUID: uuidAppleDDMWithLabel, LabelName: labelFoo.Name, LabelID: labelFoo.ID},
|
||||
{ProfileUUID: uuidWindowsWithLabel, LabelName: labelFoo.Name, LabelID: labelFoo.ID},
|
||||
{ProfileUUID: uuidWindowsWithLabel, LabelName: labelBar.Name, LabelID: labelBar.ID},
|
||||
},
|
||||
|
|
@ -8891,19 +8963,27 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() {
|
|||
errMsg := extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Couldn't upload. The file should include valid XML:")
|
||||
|
||||
// Apple invalid content
|
||||
// Apple invalid mobileconfig content
|
||||
body, headers = generateNewProfileMultipartRequest(t,
|
||||
"apple.mobileconfig", []byte("\x00\x01\x02"), s.token, nil)
|
||||
res = s.DoRawWithHeaders("POST", "/api/latest/fleet/configuration_profiles", body.Bytes(), http.StatusBadRequest, headers)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "mobileconfig is not XML nor PKCS7 parseable")
|
||||
|
||||
// Apple invalid json declaration
|
||||
body, headers = generateNewProfileMultipartRequest(t,
|
||||
"apple.json", []byte("{"), s.token, nil)
|
||||
res = s.DoRawWithHeaders("POST", "/api/latest/fleet/configuration_profiles", body.Bytes(), http.StatusBadRequest, headers)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Couldn't upload. The file should include valid JSON:")
|
||||
|
||||
// get the existing profiles work
|
||||
expectedProfiles := []fleet.MDMConfigProfilePayload{
|
||||
{ProfileUUID: noTeamAppleProfUUID, Platform: "darwin", Name: "apple-global-profile", Identifier: "test-global-ident", TeamID: nil},
|
||||
{ProfileUUID: teamAppleProfUUID, Platform: "darwin", Name: "apple-team-profile", Identifier: "test-team-ident", TeamID: &testTeam.ID},
|
||||
{ProfileUUID: noTeamWinProfUUID, Platform: "windows", Name: "win-global-profile", TeamID: nil},
|
||||
{ProfileUUID: teamWinProfUUID, Platform: "windows", Name: "win-team-profile", TeamID: &testTeam.ID},
|
||||
{ProfileUUID: uuidAppleDDMWithLabel, Platform: "darwin", Name: "apple-decl-with-labels", Identifier: "ident-decl-with-labels", TeamID: nil, Labels: []fleet.ConfigurationProfileLabel{{LabelID: labelFoo.ID, LabelName: labelFoo.Name}}},
|
||||
}
|
||||
for _, prof := range expectedProfiles {
|
||||
var getResp getMDMConfigProfileResponse
|
||||
|
|
@ -8922,8 +9002,10 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() {
|
|||
resp := s.Do("GET", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", prof.ProfileUUID), nil, http.StatusOK, "alt", "media")
|
||||
require.NotZero(t, resp.ContentLength)
|
||||
require.Contains(t, resp.Header.Get("Content-Disposition"), "attachment;")
|
||||
if getResp.Platform == "darwin" {
|
||||
if strings.HasPrefix(prof.ProfileUUID, "a") {
|
||||
require.Contains(t, resp.Header.Get("Content-Type"), "application/x-apple-aspen-config")
|
||||
} else if strings.HasPrefix(prof.ProfileUUID, fleet.MDMAppleDeclarationUUIDPrefix) {
|
||||
require.Contains(t, resp.Header.Get("Content-Type"), "application/json")
|
||||
} else {
|
||||
require.Contains(t, resp.Header.Get("Content-Type"), "application/octet-stream")
|
||||
}
|
||||
|
|
@ -8938,6 +9020,9 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() {
|
|||
// get an unknown Apple profile
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", "ano-such-profile"), nil, http.StatusNotFound, &getResp)
|
||||
s.Do("GET", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", "ano-such-profile"), nil, http.StatusNotFound, "alt", "media")
|
||||
// get an unknown Apple declaration
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", fmt.Sprintf("%sno-such-profile", fleet.MDMAppleDeclarationUUIDPrefix)), nil, http.StatusNotFound, &getResp)
|
||||
s.Do("GET", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", fmt.Sprintf("%sno-such-profile", fleet.MDMAppleDeclarationUUIDPrefix)), nil, http.StatusNotFound, "alt", "media")
|
||||
// get an unknown Windows profile
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", "wno-such-profile"), nil, http.StatusNotFound, &getResp)
|
||||
s.Do("GET", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", "wno-such-profile"), nil, http.StatusNotFound, "alt", "media")
|
||||
|
|
@ -8948,6 +9033,16 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() {
|
|||
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", teamAppleProfUUID), nil, http.StatusOK, &deleteResp)
|
||||
// delete non-existing Apple profile
|
||||
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", "ano-such-profile"), nil, http.StatusNotFound, &deleteResp)
|
||||
|
||||
// delete existing Apple declaration
|
||||
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", uuidAppleDDMWithLabel), nil, http.StatusOK, &deleteResp)
|
||||
s.lastActivityOfTypeMatches(
|
||||
fleet.ActivityTypeDeletedDeclarationProfile{}.ActivityName(),
|
||||
`{"profile_name": "apple-decl-with-labels", "identifier": "ident-decl-with-labels", "team_id": null, "team_name": null}`,
|
||||
0,
|
||||
)
|
||||
// delete non-existing Apple declaration
|
||||
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", fmt.Sprintf("%sno-such-profile", fleet.MDMAppleDeclarationUUIDPrefix)), nil, http.StatusNotFound, &deleteResp)
|
||||
// delete existing Windows profiles
|
||||
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", noTeamWinProfUUID), nil, http.StatusOK, &deleteResp)
|
||||
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", teamWinProfUUID), nil, http.StatusOK, &deleteResp)
|
||||
|
|
@ -8978,6 +9073,7 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() {
|
|||
return err
|
||||
})
|
||||
}
|
||||
// TODO: Add tests for create/delete forbidden declaration types?
|
||||
|
||||
// make fleet add a FileVault profile
|
||||
acResp := appConfigResponse{}
|
||||
|
|
@ -8999,6 +9095,8 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() {
|
|||
|
||||
// try to delete the profile
|
||||
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", profUUID), nil, http.StatusBadRequest, &deleteResp)
|
||||
|
||||
// TODO: Add tests for OS updates declaration when implemented.
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) TestListMDMConfigProfiles() {
|
||||
|
|
@ -11029,10 +11127,12 @@ func (s *integrationMDMTestSuite) TestManualEnrollmentCommands() {
|
|||
cmd, err := mdmDevice.Idle()
|
||||
require.NoError(t, err)
|
||||
for cmd != nil {
|
||||
if manifest := cmd.Command.InstallEnterpriseApplication.ManifestURL; manifest != nil {
|
||||
var fullCmd micromdm.CommandPayload
|
||||
require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
|
||||
if manifest := fullCmd.Command.InstallEnterpriseApplication.ManifestURL; manifest != nil {
|
||||
foundInstallFleetdCommand = true
|
||||
require.Equal(t, "InstallEnterpriseApplication", cmd.Command.RequestType)
|
||||
require.Contains(t, *cmd.Command.InstallEnterpriseApplication.ManifestURL, apple_mdm.FleetdPublicManifestURL)
|
||||
require.Contains(t, *fullCmd.Command.InstallEnterpriseApplication.ManifestURL, apple_mdm.FleetdPublicManifestURL)
|
||||
}
|
||||
cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -11706,7 +11806,10 @@ func (s *integrationMDMTestSuite) TestDontIgnoreAnyProfileErrors() {
|
|||
for cmd != nil {
|
||||
if cmd.Command.RequestType == "RemoveProfile" {
|
||||
var errChain []mdm.ErrorChain
|
||||
if cmd.Command.RemoveProfile.Identifier == "I1" {
|
||||
var fullCmd micromdm.CommandPayload
|
||||
require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
|
||||
|
||||
if fullCmd.Command.RemoveProfile.Identifier == "I1" {
|
||||
errChain = append(errChain, mdm.ErrorChain{ErrorCode: 89, ErrorDomain: "MDMClientError", USEnglishDescription: "Profile with identifier 'I1' not found."})
|
||||
} else {
|
||||
errChain = append(errChain, mdm.ErrorChain{ErrorCode: 96, ErrorDomain: "MDMClientError", USEnglishDescription: "Cannot replace profile 'I2' because it was not installed by the MDM server."})
|
||||
|
|
@ -11836,7 +11939,7 @@ func (s *integrationMDMTestSuite) TestSCEPCertExpiration() {
|
|||
require.NoError(t, err)
|
||||
|
||||
checkRenewCertCommand := func(device *mdmtest.TestAppleMDMClient, enrollRef string) {
|
||||
var renewCmd *micromdm.CommandPayload
|
||||
var renewCmd *mdm.Command
|
||||
cmd, err := device.Idle()
|
||||
require.NoError(t, err)
|
||||
for cmd != nil {
|
||||
|
|
@ -11847,7 +11950,9 @@ func (s *integrationMDMTestSuite) TestSCEPCertExpiration() {
|
|||
require.NoError(t, err)
|
||||
}
|
||||
require.NotNil(t, renewCmd)
|
||||
s.verifyEnrollmentProfile(renewCmd.Command.InstallProfile.Payload, enrollRef)
|
||||
var fullCmd micromdm.CommandPayload
|
||||
require.NoError(t, plist.Unmarshal(renewCmd.Raw, &fullCmd))
|
||||
s.verifyEnrollmentProfile(fullCmd.Command.InstallProfile.Payload, enrollRef)
|
||||
}
|
||||
|
||||
checkRenewCertCommand(manualEnrolledDevice, "")
|
||||
|
|
|
|||
|
|
@ -1019,6 +1019,25 @@ func getMDMConfigProfileEndpoint(ctx context.Context, request interface{}, svc f
|
|||
}, nil
|
||||
}
|
||||
|
||||
if isAppleDeclarationUUID(req.ProfileUUID) {
|
||||
// TODO: we could potentially combined with the other service methods
|
||||
decl, err := svc.GetMDMAppleDeclaration(ctx, req.ProfileUUID)
|
||||
if err != nil {
|
||||
return &getMDMConfigProfileResponse{Err: err}, nil
|
||||
}
|
||||
|
||||
if downloadRequested {
|
||||
return downloadFileResponse{
|
||||
content: decl.RawJSON,
|
||||
contentType: "application/json",
|
||||
filename: fmt.Sprintf("%s_%s.json", time.Now().Format("2006-01-02"), strings.ReplaceAll(decl.Name, " ", "_")),
|
||||
}, nil
|
||||
}
|
||||
return &getMDMConfigProfileResponse{
|
||||
MDMConfigProfilePayload: fleet.NewMDMConfigProfilePayloadFromAppleDDM(decl),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Windows config profile
|
||||
cp, err := svc.GetMDMWindowsConfigProfile(ctx, req.ProfileUUID)
|
||||
if err != nil {
|
||||
|
|
@ -1077,6 +1096,9 @@ func deleteMDMConfigProfileEndpoint(ctx context.Context, request interface{}, sv
|
|||
var err error
|
||||
if isAppleProfileUUID(req.ProfileUUID) {
|
||||
err = svc.DeleteMDMAppleConfigProfile(ctx, req.ProfileUUID)
|
||||
} else if isAppleDeclarationUUID(req.ProfileUUID) {
|
||||
// TODO: we could potentially combined with the other service methods
|
||||
err = svc.DeleteMDMAppleDeclaration(ctx, req.ProfileUUID)
|
||||
} else {
|
||||
err = svc.DeleteMDMWindowsConfigProfile(ctx, req.ProfileUUID)
|
||||
}
|
||||
|
|
@ -1157,6 +1179,10 @@ func isAppleProfileUUID(profileUUID string) bool {
|
|||
return strings.HasPrefix(profileUUID, "a")
|
||||
}
|
||||
|
||||
func isAppleDeclarationUUID(profileUUID string) bool {
|
||||
return strings.HasPrefix(profileUUID, fleet.MDMAppleDeclarationUUIDPrefix)
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// POST /mdm/profiles (Create Apple or Windows MDM Config Profile)
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
@ -1221,7 +1247,23 @@ func newMDMConfigProfileEndpoint(ctx context.Context, request interface{}, svc f
|
|||
defer ff.Close()
|
||||
|
||||
fileExt := filepath.Ext(req.Profile.Filename)
|
||||
if isApple := strings.EqualFold(fileExt, ".mobileconfig"); isApple {
|
||||
profileName := strings.TrimSuffix(filepath.Base(req.Profile.Filename), fileExt)
|
||||
isMobileConfig := strings.EqualFold(fileExt, ".mobileconfig")
|
||||
isJSON := strings.EqualFold(fileExt, ".json")
|
||||
if isMobileConfig || isJSON {
|
||||
// Then it's an Apple configuration file
|
||||
if isJSON {
|
||||
decl, err := svc.NewMDMAppleDeclaration(ctx, req.TeamID, ff, req.Labels, profileName)
|
||||
if err != nil {
|
||||
return &newMDMConfigProfileResponse{Err: err}, nil
|
||||
}
|
||||
|
||||
return &newMDMConfigProfileResponse{
|
||||
ProfileUUID: decl.DeclarationUUID,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
cp, err := svc.NewMDMAppleConfigProfile(ctx, req.TeamID, ff, req.Labels)
|
||||
if err != nil {
|
||||
return &newMDMConfigProfileResponse{Err: err}, nil
|
||||
|
|
@ -1232,7 +1274,6 @@ func newMDMConfigProfileEndpoint(ctx context.Context, request interface{}, svc f
|
|||
}
|
||||
|
||||
if isWindows := strings.EqualFold(fileExt, ".xml"); isWindows {
|
||||
profileName := strings.TrimSuffix(filepath.Base(req.Profile.Filename), fileExt)
|
||||
cp, err := svc.NewMDMWindowsConfigProfile(ctx, req.TeamID, profileName, ff, req.Labels)
|
||||
if err != nil {
|
||||
return &newMDMConfigProfileResponse{Err: err}, nil
|
||||
|
|
@ -1254,7 +1295,7 @@ func (svc *Service) NewMDMUnsupportedConfigProfile(ctx context.Context, teamID u
|
|||
// this is required because we need authorize to return the error, and
|
||||
// svc.authz is only available on the concrete Service struct, not on the
|
||||
// Service interface so it cannot be done in the endpoint itself.
|
||||
return &fleet.BadRequestError{Message: "Couldn't upload. The file should be a .mobileconfig or .xml file."}
|
||||
return &fleet.BadRequestError{Message: "Couldn't add profile. The file should be a .mobileconfig, XML, or JSON file."}
|
||||
}
|
||||
|
||||
func (svc *Service) NewMDMWindowsConfigProfile(ctx context.Context, teamID uint, profileName string, r io.Reader, labels []string) (*fleet.MDMWindowsConfigProfile, error) {
|
||||
|
|
@ -1476,7 +1517,7 @@ func (svc *Service) BatchSetMDMProfiles(
|
|||
return ctxerr.Wrap(ctx, err, "validating labels")
|
||||
}
|
||||
|
||||
appleProfiles, err := getAppleProfiles(ctx, tmID, appCfg, profiles, labelMap)
|
||||
appleProfiles, appleDecls, err := getAppleProfiles(ctx, tmID, appCfg, profiles, labelMap)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "validating macOS profiles")
|
||||
}
|
||||
|
|
@ -1490,7 +1531,7 @@ func (svc *Service) BatchSetMDMProfiles(
|
|||
return nil
|
||||
}
|
||||
|
||||
if err := svc.ds.BatchSetMDMProfiles(ctx, tmID, appleProfiles, windowsProfiles); err != nil {
|
||||
if err := svc.ds.BatchSetMDMProfiles(ctx, tmID, appleProfiles, windowsProfiles, appleDecls); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "setting config profiles")
|
||||
}
|
||||
|
||||
|
|
@ -1576,17 +1617,94 @@ func getAppleProfiles(
|
|||
appCfg *fleet.AppConfig,
|
||||
profiles []fleet.MDMProfileBatchPayload,
|
||||
labelMap map[string]fleet.ConfigurationProfileLabel,
|
||||
) ([]*fleet.MDMAppleConfigProfile, error) {
|
||||
) ([]*fleet.MDMAppleConfigProfile, []*fleet.MDMAppleDeclaration, error) {
|
||||
// any duplicate identifier or name in the provided set results in an error
|
||||
profs := make([]*fleet.MDMAppleConfigProfile, 0, len(profiles))
|
||||
byName, byIdent := make(map[string]bool, len(profiles)), make(map[string]bool, len(profiles))
|
||||
decls := make([]*fleet.MDMAppleDeclaration, 0, len(profiles))
|
||||
// we need to keep track of the names and identifiers to check for duplicates so we will use
|
||||
// a map where the key is the name oridentifier and the value is either "mobileconfig" or
|
||||
// "declaration" to differentiate between the two types of profiles
|
||||
byName, byIdent := make(map[string]string, len(profiles)), make(map[string]string, len(profiles))
|
||||
for _, prof := range profiles {
|
||||
if mdm.GetRawProfilePlatform(prof.Contents) != "darwin" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for DDM files
|
||||
|
||||
isJSON := func(b []byte) bool {
|
||||
var js json.RawMessage
|
||||
return json.Unmarshal(b, &js) == nil
|
||||
}
|
||||
|
||||
// TODO(roberto): As a mini optimization, GetRawDeclarationValues could replace isJSON.
|
||||
if isJSON(prof.Contents) {
|
||||
rawDecl, err := fleet.GetRawDeclarationValues(prof.Contents)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if err := rawDecl.ValidateUserProvided(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
mdmDecl := fleet.NewMDMAppleDeclaration(prof.Contents, tmID, prof.Name, rawDecl.Type, rawDecl.Identifier)
|
||||
for _, labelName := range prof.Labels {
|
||||
if lbl, ok := labelMap[labelName]; ok {
|
||||
declLabel := fleet.ConfigurationProfileLabel{
|
||||
LabelName: lbl.LabelName,
|
||||
LabelID: lbl.LabelID,
|
||||
}
|
||||
mdmDecl.Labels = append(mdmDecl.Labels, declLabel)
|
||||
}
|
||||
}
|
||||
|
||||
v, ok := byName[mdmDecl.Name]
|
||||
switch {
|
||||
case !ok:
|
||||
byName[mdmDecl.Name] = "declaration"
|
||||
case v == "mobileconfig":
|
||||
return nil, nil, ctxerr.Wrap(ctx,
|
||||
fleet.NewInvalidArgumentError(mdmDecl.Name, "A configuration profile with this name already exists."),
|
||||
"duplicate mobileconfig profile by name")
|
||||
case v == "declaration":
|
||||
return nil, nil, ctxerr.Wrap(ctx,
|
||||
fleet.NewInvalidArgumentError(mdmDecl.Name, "A declaration profile with this name already exists."),
|
||||
"duplicate declaration profile by name")
|
||||
default:
|
||||
// this should never happen but just in case
|
||||
return nil, nil, ctxerr.Wrap(ctx,
|
||||
fleet.NewInvalidArgumentError(mdmDecl.Name, "A profile with this name already exists."),
|
||||
"duplicate profile by name")
|
||||
}
|
||||
|
||||
v, ok = byIdent[mdmDecl.Identifier]
|
||||
switch {
|
||||
case !ok:
|
||||
byIdent[mdmDecl.Identifier] = "declaration"
|
||||
case v == "mobileconfig":
|
||||
return nil, nil, ctxerr.Wrap(ctx,
|
||||
fleet.NewInvalidArgumentError(mdmDecl.Identifier, "A configuration profile with this identifier already exists."),
|
||||
"duplicate mobileconfig profile by identifier")
|
||||
case v == "declaration":
|
||||
return nil, nil, ctxerr.Wrap(ctx,
|
||||
fleet.NewInvalidArgumentError(mdmDecl.Identifier, "A declaration profile with this identifier already exists."),
|
||||
"duplicate declaration profile by identifier")
|
||||
default:
|
||||
// this should never happen but just in case
|
||||
return nil, nil, ctxerr.Wrap(ctx,
|
||||
fleet.NewInvalidArgumentError(mdmDecl.Identifier, "A profile with this identifier already exists."),
|
||||
"duplicate identifier by identifier")
|
||||
}
|
||||
|
||||
decls = append(decls, mdmDecl)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
mdmProf, err := fleet.NewMDMAppleConfigProfile(prof.Contents, tmID)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx,
|
||||
return nil, nil, ctxerr.Wrap(ctx,
|
||||
fleet.NewInvalidArgumentError(prof.Name, err.Error()),
|
||||
"invalid mobileconfig profile")
|
||||
}
|
||||
|
|
@ -1598,29 +1716,31 @@ func getAppleProfiles(
|
|||
}
|
||||
|
||||
if err := mdmProf.ValidateUserProvided(); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx,
|
||||
return nil, nil, ctxerr.Wrap(ctx,
|
||||
fleet.NewInvalidArgumentError(prof.Name, err.Error()))
|
||||
}
|
||||
|
||||
if mdmProf.Name != prof.Name {
|
||||
return nil, ctxerr.Wrap(ctx,
|
||||
return nil, nil, ctxerr.Wrap(ctx,
|
||||
fleet.NewInvalidArgumentError(prof.Name, fmt.Sprintf("Couldn’t edit custom_settings. The name provided for the profile must match the profile PayloadDisplayName: %q", mdmProf.Name)),
|
||||
"duplicate mobileconfig profile by name")
|
||||
}
|
||||
|
||||
if byName[mdmProf.Name] {
|
||||
return nil, ctxerr.Wrap(ctx,
|
||||
// TODO: confirm error messages
|
||||
if _, ok := byName[mdmProf.Name]; ok {
|
||||
return nil, nil, ctxerr.Wrap(ctx,
|
||||
fleet.NewInvalidArgumentError(prof.Name, fmt.Sprintf("Couldn’t edit custom_settings. More than one configuration profile have the same name (PayloadDisplayName): %q", mdmProf.Name)),
|
||||
"duplicate mobileconfig profile by name")
|
||||
}
|
||||
byName[mdmProf.Name] = true
|
||||
byName[mdmProf.Name] = "mobileconfig"
|
||||
|
||||
if byIdent[mdmProf.Identifier] {
|
||||
return nil, ctxerr.Wrap(ctx,
|
||||
// TODO: confirm error messages
|
||||
if _, ok := byIdent[mdmProf.Identifier]; ok {
|
||||
return nil, nil, ctxerr.Wrap(ctx,
|
||||
fleet.NewInvalidArgumentError(prof.Name, fmt.Sprintf("Couldn’t edit custom_settings. More than one configuration profile have the same identifier (PayloadIdentifier): %q", mdmProf.Identifier)),
|
||||
"duplicate mobileconfig profile by identifier")
|
||||
}
|
||||
byIdent[mdmProf.Identifier] = true
|
||||
byIdent[mdmProf.Identifier] = "mobileconfig"
|
||||
|
||||
profs = append(profs, mdmProf)
|
||||
}
|
||||
|
|
@ -1632,13 +1752,13 @@ func getAppleProfiles(
|
|||
// custom_settings key, we just return a success response in this
|
||||
// situation.
|
||||
if len(profs) == 0 {
|
||||
return []*fleet.MDMAppleConfigProfile{}, nil
|
||||
return []*fleet.MDMAppleConfigProfile{}, []*fleet.MDMAppleDeclaration{}, nil
|
||||
}
|
||||
|
||||
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("mdm", "cannot set custom settings: Fleet MDM is not configured"))
|
||||
return nil, nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("mdm", "cannot set custom settings: Fleet MDM is not configured"))
|
||||
}
|
||||
|
||||
return profs, nil
|
||||
return profs, decls, nil
|
||||
}
|
||||
|
||||
func getWindowsProfiles(
|
||||
|
|
|
|||
|
|
@ -753,6 +753,8 @@ func TestGetMDMDiskEncryptionSummary(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
// TODO: Add tests for Apple DDM authz?
|
||||
|
||||
func TestMDMWindowsConfigProfileAuthz(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
// while the config profiles are not premium-only, teams are and we want to test with teams.
|
||||
|
|
@ -1093,7 +1095,7 @@ func TestMDMBatchSetProfiles(t *testing.T) {
|
|||
ds.TeamFunc = func(ctx context.Context, id uint) (*fleet.Team, error) {
|
||||
return &fleet.Team{ID: id, Name: "team"}, nil
|
||||
}
|
||||
ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile) error {
|
||||
ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration) error {
|
||||
return nil
|
||||
}
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
||||
|
|
@ -1300,6 +1302,7 @@ func TestMDMBatchSetProfiles(t *testing.T) {
|
|||
{Name: "N1", Contents: mobileconfigForTest("N1", "I1")},
|
||||
{Name: "N2", Contents: mobileconfigForTest("N2", "I2")},
|
||||
{Name: "N3", Contents: mobileconfigForTest("N3", "I3")},
|
||||
{Name: "N4", Contents: declBytesForTest("D1", "d1content")},
|
||||
},
|
||||
``,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -339,6 +339,10 @@ func RunServerForTestsWithDS(t *testing.T, ds fleet.Datastore, opts ...*TestServ
|
|||
commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPusher),
|
||||
logger: kitlog.NewNopLogger(),
|
||||
},
|
||||
&MDMAppleDDMService{
|
||||
ds: ds,
|
||||
logger: logger,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -13,7 +13,7 @@ variable "fleet_config" {
|
|||
type = object({
|
||||
mem = optional(number, 4096)
|
||||
cpu = optional(number, 512)
|
||||
image = optional(string, "fleetdm/fleet:v4.47.2")
|
||||
image = optional(string, "fleetdm/fleet:v4.47.3")
|
||||
family = optional(string, "fleet")
|
||||
sidecars = optional(list(any), [])
|
||||
depends_on = optional(list(any), [])
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ variable "fleet_config" {
|
|||
type = object({
|
||||
mem = optional(number, 4096)
|
||||
cpu = optional(number, 512)
|
||||
image = optional(string, "fleetdm/fleet:v4.47.2")
|
||||
image = optional(string, "fleetdm/fleet:v4.47.3")
|
||||
family = optional(string, "fleet")
|
||||
sidecars = optional(list(any), [])
|
||||
depends_on = optional(list(any), [])
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ provider "aws" {
|
|||
}
|
||||
|
||||
locals {
|
||||
fleet_image = "fleetdm/fleet:v4.47.2"
|
||||
fleet_image = "fleetdm/fleet:v4.47.3"
|
||||
domain_name = "example.com"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ variable "fleet_config" {
|
|||
type = object({
|
||||
mem = optional(number, 4096)
|
||||
cpu = optional(number, 512)
|
||||
image = optional(string, "fleetdm/fleet:v4.47.2")
|
||||
image = optional(string, "fleetdm/fleet:v4.47.3")
|
||||
family = optional(string, "fleet")
|
||||
sidecars = optional(list(any), [])
|
||||
depends_on = optional(list(any), [])
|
||||
|
|
|
|||
|
|
@ -59,8 +59,8 @@ module "fleet" {
|
|||
|
||||
fleet_config = {
|
||||
# To avoid pull-rate limiting from dockerhub, consider using our quay.io mirror
|
||||
# for the Fleet image. e.g. "quay.io/fleetdm/fleet:v4.47.2"
|
||||
image = "fleetdm/fleet:v4.47.2" # override default to deploy the image you desire
|
||||
# for the Fleet image. e.g. "quay.io/fleetdm/fleet:v4.47.3"
|
||||
image = "fleetdm/fleet:v4.47.3" # override default to deploy the image you desire
|
||||
# See https://fleetdm.com/docs/deploy/reference-architectures#aws for appropriate scaling
|
||||
# memory and cpu.
|
||||
autoscaling = {
|
||||
|
|
|
|||
|
|
@ -215,7 +215,7 @@ variable "fleet_config" {
|
|||
type = object({
|
||||
mem = optional(number, 4096)
|
||||
cpu = optional(number, 512)
|
||||
image = optional(string, "fleetdm/fleet:v4.47.2")
|
||||
image = optional(string, "fleetdm/fleet:v4.47.3")
|
||||
family = optional(string, "fleet")
|
||||
sidecars = optional(list(any), [])
|
||||
depends_on = optional(list(any), [])
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "fleetctl",
|
||||
"version": "v4.47.2",
|
||||
"version": "v4.47.3",
|
||||
"description": "Installer for the fleetctl CLI tool",
|
||||
"bin": {
|
||||
"fleetctl": "./run.js"
|
||||
|
|
|
|||
1
website/config/routes.js
vendored
1
website/config/routes.js
vendored
|
|
@ -511,6 +511,7 @@ module.exports.routes = {
|
|||
'GET /learn-more-about/enrolling-hosts': '/docs/using-fleet/adding-hosts',
|
||||
'GET /learn-more-about/setup-assistant': '/docs/using-fleet/mdm-macos-setup-experience#macos-setup-assistant',
|
||||
'GET /learn-more-about/policy-automations': '/docs/using-fleet/automations',
|
||||
'GET /install-wine': 'https://github.com/fleetdm/fleet/blob/main/scripts/macos-install-wine.sh',
|
||||
|
||||
// Sitemap
|
||||
// =============================================================================================================
|
||||
|
|
|
|||
|
|
@ -115,9 +115,10 @@
|
|||
</div>
|
||||
<div purpose="feature-text" class="d-flex flex-column">
|
||||
<h3>Up-to-date data without scans</h3>
|
||||
<p>Traditional network vulnerability scans can clog networks. Fleet does things differently.</p>
|
||||
<p>Traditional network vulnerability scans can clog your network and even haunt your printers with pages full of wingdings. Fleet does things differently.</p>
|
||||
<div purpose="checklist" class="flex-column d-flex">
|
||||
<p>Lightweight enough for the most brittle environments (OT, data centers, embedded/BTS, low-latency gaming servers).</p>
|
||||
<p>Eliminate the risk of side effects from scanning the network.</p>
|
||||
<p>Lightweight enough for the most brittle environments (OT, data centers, embedded/BTS, low-latency gaming servers).</p>
|
||||
<p>Quickly pull data about important CVEs and zero days during an incident or audit.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue