Skip to content

48342 edit config profile endpoint - #49141

Merged
JordanMontgomery merged 36 commits into
mainfrom
48342-edit-config-profile-endpoint
Jul 21, 2026
Merged

48342 edit config profile endpoint#49141
JordanMontgomery merged 36 commits into
mainfrom
48342-edit-config-profile-endpoint

Conversation

@andymFleet

@andymFleet andymFleet commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

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/ or ee/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

  • New Features
    • Added support for editing existing Apple, Windows, and Android configuration profiles through the API.
    • Supports updating profile content, names where applicable, label targeting, and Fleet variable associations without replacing the profile identity.
    • Added support for editing Apple DDM declarations.
    • Added activity tracking for configuration profile edits.
  • Bug Fixes
    • Added validation for unsupported edits, invalid labels, duplicate names, missing profiles, and protected Fleet-managed profiles.

andymFleet added 21 commits July 2, 2026 15:51
Copilot AI review requested due to automatic review settings July 10, 2026 16:28
@andymFleet
andymFleet requested a review from a team as a code owner July 10, 2026 16:28
@andymFleet
andymFleet marked this pull request as draft July 10, 2026 16:28
@andymFleet

Copy link
Copy Markdown
Contributor Author

Raise a draft for handover.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread server/datastore/mysql/apple_mdm.go
Comment thread server/service/apple_mdm.go Outdated
Comment thread server/service/windows_mdm_profiles.go Outdated
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds a PATCH endpoint for editing existing Apple .mobileconfig profiles, Apple DDM declarations, Windows profiles, and Android profiles. Updates may replace profile content, label targeting, Fleet variable associations, or related Windows OS-update tracking while preserving profile UUIDs. The service adds platform dispatch, validation, authorization, Fleet-managed profile protection, activity logging, and labels-only updates. Datastore methods and platform-specific tests cover persistence, validation errors, conflicts, licensing, authorization, and combined content and label updates.

Possibly related PRs

  • fleetdm/fleet#46437: Changes shared MDM profile label targeting and include/exclude validation used by these update paths.
  • fleetdm/fleet#46545: Adds OS-update tracking and validation logic used when editing Windows profiles.
  • fleetdm/fleet#47750: Updates Android Fleet-variable association handling used by profile persistence.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and matches the main change: adding an edit config profile endpoint.
Description check ✅ Passed The description includes the issue reference, validation notes, and testing, and covers the main template items for this PR.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 48342-edit-config-profile-endpoint

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
server/service/mdm.go (1)

2178-2207: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated label-validation logic; inconsistent default handling vs. parseAndValidateAndroidConfigProfile.

This labels-only branch re-implements the license check, fleet.LabelOverlap check, and validateProfileLabelSets call already present in parseAndValidateAndroidConfigProfile (lines ~2088-2107). The duplicated switch labelsMembershipMode here (2200-2205) lacks the default fallback that the shared function has, so if this mode ever takes on a value other than LabelsIncludeAll/LabelsIncludeAny, the resolved includeLabels would 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 win

Add coverage for label preservation on content-only updates.

newExistingProfile/newExistingDeclaration never populate LabelsIncludeAll/Any/ExcludeAny, so none of these content-only-update subtests would catch labels being unintentionally cleared (see the corresponding comment on updateMDMAppleConfigProfile/updateMDMAppleDeclaration in apple_mdm.go). Once that behavior is confirmed/fixed, add a case where existing already 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

📥 Commits

Reviewing files that changed from the base of the PR and between 9185174 and 36ad188.

📒 Files selected for processing (19)
  • changes/48342-edit-config-profile-endpoint
  • server/datastore/mysql/android.go
  • server/datastore/mysql/android_test.go
  • server/datastore/mysql/apple_mdm.go
  • server/datastore/mysql/apple_mdm_test.go
  • server/datastore/mysql/microsoft_mdm.go
  • server/datastore/mysql/microsoft_mdm_test.go
  • server/fleet/activities.go
  • server/fleet/datastore.go
  • server/fleet/service.go
  • server/mock/datastore_mock.go
  • server/mock/service/service_mock.go
  • server/service/apple_mdm.go
  • server/service/apple_mdm_test.go
  • server/service/handler.go
  • server/service/mdm.go
  • server/service/mdm_test.go
  • server/service/windows_mdm_profiles.go
  • server/service/windows_mdm_profiles_test.go

Comment thread server/datastore/mysql/apple_mdm.go
Comment thread server/service/apple_mdm.go
Comment thread server/service/apple_mdm.go
Comment thread server/service/mdm_test.go
Comment thread server/service/mdm.go
}
if existing.Identifier != cp.Identifier {
return ctxerr.Wrap(ctx, &fleet.BadRequestError{
Message: "The new profile's PayloadIdentifier must match the existing profile's.",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@noahtalerman this is the error you asked about

…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.
@JordanMontgomery
JordanMontgomery marked this pull request as ready for review July 17, 2026 18:21
Comment thread server/service/mdm.go Outdated

type updateMDMConfigProfileResponse struct {
ProfileUUID string `json:"profile_uuid"`
HasProfile bool `json:"has_profile"`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed and updated the response to match the actual API docs

@MagnusHJensen MagnusHJensen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't we technically calling this, even if the contents didn't change? The above rowsAffected=0 check only checks for not found.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment thread server/fleet/activities.go Outdated
Comment thread server/service/apple_mdm.go Outdated
TeamName: actTeamName,
ProfileName: decl.Name,
ProfileIdentifier: decl.Identifier,
Platform: "darwin",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

darwin seems to be a backwards compatible key, why not just use apple here? And avoid supporting darwin at all for this activity type?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this activity entirely and updated existing gitops activities for this feature

Comment thread server/service/apple_mdm.go Outdated
TeamName: actTeamName,
ProfileName: cp.Name,
ProfileIdentifier: cp.Identifier,
Platform: "darwin",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same comment here as declarations

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above

Comment thread server/service/mdm.go Outdated
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")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be formatted with the constant, so if we change it the error message won't drift.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Comment thread server/service/mdm.go
Comment thread server/service/windows_mdm_profiles.go Outdated
Comment thread server/service/windows_mdm_profiles.go Outdated
Comment on lines 3285 to +3287
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),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread server/service/mdm.go

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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here as above

Comment thread server/service/mdm.go
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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here as above

@JordanMontgomery
JordanMontgomery merged commit 15a0f4b into main Jul 21, 2026
44 checks passed
@JordanMontgomery
JordanMontgomery deleted the 48342-edit-config-profile-endpoint branch July 21, 2026 12:20
JordanMontgomery added a commit that referenced this pull request Jul 21, 2026
<!-- 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ECP: Backend

4 participants