48342 edit config profile endpoint - #49141
Conversation
… OS-update LocURI
|
Raise a draft for handover. |
There was a problem hiding this comment.
Warning
- Copilot's review of this pull request may be incomplete because some of the changed files are excluded by your Copilot content exclusion settings. See Excluding content from Copilot for details.
Pull request overview
Adds a new PATCH endpoint to edit existing MDM configuration profiles (Apple .mobileconfig, Apple declarations, Windows, Android) in place, including optional label-scope changes, with platform-specific validation and new datastore update methods.
Changes:
- Added
PATCH /api/_version_/fleet/configuration_profiles/{profile_uuid}request/response + service dispatch (UpdateMDMConfigProfile). - Implemented per-platform update flows (service + MySQL) for Apple config profiles, Apple declarations, Windows profiles, and Android profiles.
- Added extensive service-layer and datastore integration tests for update behaviors (authz, licensing, OS-update restrictions, labels, Fleet vars, and tracking).
Reviewed changes
Copilot reviewed 18 out of 19 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| server/service/handler.go | Registers the new PATCH route for configuration profile updates. |
| server/service/mdm.go | Adds request decoding, endpoint handler, and service dispatch (UpdateMDMConfigProfile). |
| server/fleet/service.go | Extends the Service interface with UpdateMDMConfigProfile. |
| server/service/apple_mdm.go | Refactors create validation and adds Apple profile/declaration update implementations + edit activity logging. |
| server/service/windows_mdm_profiles.go | Refactors create validation and adds Windows profile update implementation + edit activity logging. |
| server/datastore/mysql/apple_mdm.go | Adds UpdateMDMAppleConfigProfile datastore method (in-place update + labels + variables). |
| server/datastore/mysql/microsoft_mdm.go | Adds UpdateMDMWindowsConfigProfile datastore method (in-place update + labels + vars + OS-update tracking reconciliation). |
| server/datastore/mysql/android.go | Adds UpdateMDMAndroidConfigProfile datastore method (in-place update + labels). |
| server/fleet/datastore.go | Extends the Datastore interface with update methods for Apple/Windows/Android profiles. |
| server/fleet/activities.go | Adds ActivityTypeEditedConfigurationProfile activity type. |
| server/mock/datastore_mock.go | Mocks new datastore update methods. |
| server/mock/service/service_mock.go | Mocks new service method UpdateMDMConfigProfile. |
| server/service/mdm_test.go | Adds decode-request tests and dispatch coverage for the new endpoint/service routing. |
| server/service/apple_mdm_test.go | Adds service tests for updating Apple config profiles + declarations. |
| server/service/windows_mdm_profiles_test.go | Adds service tests for updating Windows profiles. |
| server/datastore/mysql/apple_mdm_test.go | Adds integration tests for Apple profile update semantics (content/name/labels/checksum). |
| server/datastore/mysql/microsoft_mdm_test.go | Adds integration tests for Windows profile update semantics (content/labels/vars/tracking). |
| server/datastore/mysql/android_test.go | Adds integration tests for Android profile update semantics (content/labels). |
Files excluded by content exclusion policy (1)
- changes/48342-edit-config-profile-endpoint
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
WalkthroughAdds a PATCH endpoint for editing existing Apple Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (2)
server/service/mdm.go (1)
2178-2207: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicated label-validation logic; inconsistent
defaulthandling vs.parseAndValidateAndroidConfigProfile.This labels-only branch re-implements the license check,
fleet.LabelOverlapcheck, andvalidateProfileLabelSetscall already present inparseAndValidateAndroidConfigProfile(lines ~2088-2107). The duplicatedswitch labelsMembershipModehere (2200-2205) lacks thedefaultfallback that the shared function has, so if this mode ever takes on a value other thanLabelsIncludeAll/LabelsIncludeAny, the resolvedincludeLabelswould be silently discarded here but not in the create path.Consider extracting the shared "validate + resolve label sets" logic into a single helper used by both code paths to avoid this kind of drift.
♻️ Sketch of a shared helper
// resolveLabelScoping runs the shared label-scoping validation (license // check, overlap check, and label-set resolution) used by both create and // labels-only update paths, and applies the result to cp according to mode. func (svc *Service) resolveLabelScoping(ctx context.Context, teamID *uint, labelsInclude, labelsExcludeAny []string, mode fleet.MDMLabelsMode) (includeAll, includeAny, excludeAny []fleet.ConfigurationProfileLabel, err error) { lic, _ := license.FromContext(ctx) if len(labelsInclude) > 0 || len(labelsExcludeAny) > 0 { if lic == nil || !lic.IsPremium() { return nil, nil, nil, ctxerr.Wrap(ctx, fleet.NewLicenseErrorWithCause(fleet.ConfigProfileLabelScopingPremiumCauseMsg), "checking license for profile label scoping") } } if overlap := fleet.LabelOverlap(labelsInclude, labelsExcludeAny); overlap != "" { return nil, nil, nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("labels", fmt.Sprintf("label %q cannot appear in both include and exclude lists", overlap))) } includeLabels, excludeLabels, err := svc.validateProfileLabelSets(ctx, teamID, labelsInclude, labelsExcludeAny) if err != nil { return nil, nil, nil, ctxerr.Wrap(ctx, err, "validating labels") } switch mode { case fleet.LabelsIncludeAny: return nil, includeLabels, excludeLabels, nil default: return includeLabels, nil, excludeLabels, nil } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/service/mdm.go` around lines 2178 - 2207, Consolidate the duplicated license, overlap, and label-set validation used by parseAndValidateAndroidConfigProfile and the labels-only update branch into one shared helper, such as resolveLabelScoping. Have it resolve membership modes with LabelsIncludeAny explicitly and a default fallback to include-all, then use its returned label sets to populate MDMAndroidConfigProfile in both paths while preserving existing error wrapping.server/service/apple_mdm_test.go (1)
955-1007: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd coverage for label preservation on content-only updates.
newExistingProfile/newExistingDeclarationnever populateLabelsIncludeAll/Any/ExcludeAny, so none of these content-only-update subtests would catch labels being unintentionally cleared (see the corresponding comment onupdateMDMAppleConfigProfile/updateMDMAppleDeclarationinapple_mdm.go). Once that behavior is confirmed/fixed, add a case whereexistingalready has labels and assert they survive a content-only update.Also applies to: 1336-1430
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/service/apple_mdm_test.go` around lines 955 - 1007, Add regression coverage for label preservation in the content-only update tests around UpdateMDMConfigProfile and the corresponding declaration update cases: initialize existing profiles/declarations with non-empty LabelsIncludeAll, LabelsIncludeAny, and LabelsExcludeAny, perform the update, and assert all label fields remain unchanged. If assertions fail, update updateMDMAppleConfigProfile and updateMDMAppleDeclaration to retain existing labels when applying content-only changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@server/datastore/mysql/apple_mdm.go`:
- Around line 297-330: The final GetMDMAppleConfigProfile read after the update
may use a stale replica. Ensure that read is routed to the primary by wrapping
its context with ctxdb.RequirePrimary(ctx, true), or reuse the already updated
transaction result; update the relevant write flow in the MDM Apple
configuration profile method.
In `@server/service/apple_mdm.go`:
- Around line 1062-1107: The content-update branch of updateMDMAppleDeclaration
loses existing label assignments when a content-only PATCH omits labels. Before
calling parseAndValidateAppleDeclaration, preserve existing.LabelsIncludeAll or
existing.LabelsIncludeAny according to labelsMembershipMode and
existing.LabelsExcludeAny whenever the corresponding request values are omitted,
while still honoring explicitly supplied labels and membership-mode changes.
- Around line 1616-1658: Content-only PATCH requests clear existing label
targeting because omitted label fields remain nil and are treated as empty
updates. In updateMDMConfigProfileRequest.DecodeRequest, preserve existing
LabelsIncludeAll, LabelsIncludeAny, and LabelsExcludeAny when the corresponding
multipart label fields are omitted; only replace them when labels were
explicitly provided, so the update path around
parseAndValidateAppleConfigProfile retains associations.
In `@server/service/mdm_test.go`:
- Around line 3702-3707: Update the newExistingProfile helper to represent
global profiles with a nil TeamID when teamID is 0, while retaining a pointer
for nonzero team IDs; ensure the returned fleet.MDMAndroidConfigProfile matches
the analogous MDM model’s no-team representation so authorization and activity
tests use the correct scope.
In `@server/service/mdm.go`:
- Around line 1879-1921: Preserve whether each label field was omitted in
updateMDMConfigProfileRequest.DecodeRequest instead of converting omitted fields
to empty slices. Add tri-state/presence tracking for labels_include_all,
labels_include_any, and labels_exclude_any, then update
UpdateMDMAndroidConfigProfile (and related label-association handling such as
batchSetProfileLabelAssociationsDB) to retain existing associations when all
label fields are omitted while still applying explicit empty values as
replacements.
---
Nitpick comments:
In `@server/service/apple_mdm_test.go`:
- Around line 955-1007: Add regression coverage for label preservation in the
content-only update tests around UpdateMDMConfigProfile and the corresponding
declaration update cases: initialize existing profiles/declarations with
non-empty LabelsIncludeAll, LabelsIncludeAny, and LabelsExcludeAny, perform the
update, and assert all label fields remain unchanged. If assertions fail, update
updateMDMAppleConfigProfile and updateMDMAppleDeclaration to retain existing
labels when applying content-only changes.
In `@server/service/mdm.go`:
- Around line 2178-2207: Consolidate the duplicated license, overlap, and
label-set validation used by parseAndValidateAndroidConfigProfile and the
labels-only update branch into one shared helper, such as resolveLabelScoping.
Have it resolve membership modes with LabelsIncludeAny explicitly and a default
fallback to include-all, then use its returned label sets to populate
MDMAndroidConfigProfile in both paths while preserving existing error wrapping.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 1f1a026d-0130-42f0-87d5-dbce2aa042c3
📒 Files selected for processing (19)
changes/48342-edit-config-profile-endpointserver/datastore/mysql/android.goserver/datastore/mysql/android_test.goserver/datastore/mysql/apple_mdm.goserver/datastore/mysql/apple_mdm_test.goserver/datastore/mysql/microsoft_mdm.goserver/datastore/mysql/microsoft_mdm_test.goserver/fleet/activities.goserver/fleet/datastore.goserver/fleet/service.goserver/mock/datastore_mock.goserver/mock/service/service_mock.goserver/service/apple_mdm.goserver/service/apple_mdm_test.goserver/service/handler.goserver/service/mdm.goserver/service/mdm_test.goserver/service/windows_mdm_profiles.goserver/service/windows_mdm_profiles_test.go
| } | ||
| if existing.Identifier != cp.Identifier { | ||
| return ctxerr.Wrap(ctx, &fleet.BadRequestError{ | ||
| Message: "The new profile's PayloadIdentifier must match the existing profile's.", |
…file-endpoint # Conflicts: # server/service/apple_mdm.go # server/service/windows_mdm_profiles.go
The Apple/Windows/Android edit paths bumped uploaded_at unconditionally, while every upsert (GitOps batch, SetOrUpdate) preserves it when the content is unchanged. Match that convention so a no-op edit doesn't read as a fresh upload. The DDM edit path already goes through the conditional upsert.
…-profile-endpoint
|
|
||
| type updateMDMConfigProfileResponse struct { | ||
| ProfileUUID string `json:"profile_uuid"` | ||
| HasProfile bool `json:"has_profile"` |
There was a problem hiding this comment.
I'm not sure I really understand this response key? It's not used for anything in the frontend as far as I can tell. And it's keyed off the incoming request?
There was a problem hiding this comment.
Removed and updated the response to match the actual API docs
MagnusHJensen
left a comment
There was a problem hiding this comment.
Overall, looks and functions well, just some small comments
| // re-inserting. On a labels-only update the content -- and thus its | ||
| // variables -- didn't change, and clearing them here would break | ||
| // variable-driven redelivery. | ||
| if _, err := batchSetProfileVariableAssociationsDB(ctx, tx, []fleet.MDMProfileUUIDFleetVariables{ |
There was a problem hiding this comment.
Aren't we technically calling this, even if the contents didn't change? The above rowsAffected=0 check only checks for not found.
There was a problem hiding this comment.
WE are but I think it's probably OK. We can't super easily check for affected rows because we set the DB to return affectedRows=matchedRows(even if not updated) in our connections. So this is a tiny bit less performant perhaps but given it's a single profile endpoint I think probably OK?
| TeamName: actTeamName, | ||
| ProfileName: decl.Name, | ||
| ProfileIdentifier: decl.Identifier, | ||
| Platform: "darwin", |
There was a problem hiding this comment.
darwin seems to be a backwards compatible key, why not just use apple here? And avoid supporting darwin at all for this activity type?
There was a problem hiding this comment.
Removed this activity entirely and updated existing gitops activities for this feature
| TeamName: actTeamName, | ||
| ProfileName: cp.Name, | ||
| ProfileIdentifier: cp.Identifier, | ||
| Platform: "darwin", |
There was a problem hiding this comment.
same comment here as declarations
| if fhs, ok := r.MultipartForm.File["profile"]; ok && len(fhs) > 0 { | ||
| decoded.Profile = fhs[0] | ||
| if decoded.Profile.Size > fleet.MaxProfileSize { | ||
| return nil, fleet.NewInvalidArgumentError("mdm", "maximum configuration profile file size is 1 MB") |
There was a problem hiding this comment.
This should be formatted with the constant, so if we change it the error message won't drift.
| if len(prof) > 1024*1024 { | ||
| return ctxerr.Wrap(ctx, | ||
| fleet.NewInvalidArgumentError(fmt.Sprintf("profiles[%d]", i), "maximum configuration profile file size is 1 MB"), | ||
| fleet.NewInvalidArgumentError(fmt.Sprintf("profiles[%d]", i), fleet.MaxProfileSizeErrMsg), |
There was a problem hiding this comment.
Could we make the len(prof) > 1024*1024 -> len(prof) > fleet.MaxProifleSize
and then the error message fmt.Sprintf("maximum configuration profile file size is %s", units.HumanSize(float64(fleet.MaxProfileSize)))
|
|
||
| if decoded.Profile.Size > fleet.MaxProfileSize { | ||
| return nil, fleet.NewInvalidArgumentError("mdm", "maximum configuration profile file size is 1 MB") | ||
| return nil, fleet.NewInvalidArgumentError("mdm", fleet.MaxProfileSizeErrMsg) |
| decoded.Profile = fhs[0] | ||
| if decoded.Profile.Size > fleet.MaxProfileSize { | ||
| return nil, fleet.NewInvalidArgumentError("mdm", "maximum configuration profile file size is 1 MB") | ||
| return nil, fleet.NewInvalidArgumentError("mdm", fleet.MaxProfileSizeErrMsg) |
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #48342 & #48343 Cherrypick of #49141 and #49333 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. - [x] Timeouts are implemented and retries are limited to avoid infinite loops - [x] If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added the ability to edit existing Apple, Windows, Android, and Apple DDM configuration profiles. - Profile content can be replaced, or label targeting can be updated without changing the profile file. - Added an edit action and modal to the configuration profile list. - Improved activity feed messages for individual and batch profile edits. - **Bug Fixes** - Improved validation, error messages, profile size handling, and activity tracking during edits. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Andrew Mellor <andrewmellor@fleetdm.com>
Related issue: Resolves #48342
Checklist for submitter
If some of the following don't apply, delete the relevant line.
Changes file added for user-visible changes in
changes/,orbit/changes/oree/fleetd-chrome/changes.See Changes files for more information.
Input data is properly validated,
SELECT *is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters.Testing
Added/updated automated tests
QA'd all new/changed functionality manually
Summary by CodeRabbit