39265: Add API/gitops support for Microsoft Tenant IDs (#39631)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #39265

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

## New Fleet configuration settings

- [ ] Setting(s) is/are explicitly excluded from GitOps

If you didn't check the box above, follow this checklist for
GitOps-enabled settings:

- [x] Verified that the setting is exported via `fleetctl
generate-gitops`
- [x] Verified the setting is documented in a separate PR to [the GitOps
documentation](https://github.com/fleetdm/fleet/blob/main/docs/Configuration/yaml-files.md#L485)
- [x] Verified that the setting is cleared on the server if it is not
supplied in a YAML file (or that it is documented as being optional)
- [x] Verified that any relevant UI is disabled when GitOps mode is
enabled
This commit is contained in:
Jordan Montgomery
2026-02-12 09:38:10 -05:00
committed by GitHub
parent 86e4c029ce
commit 41146843f9
15 changed files with 169 additions and 27 deletions
+32
View File
@@ -3326,6 +3326,38 @@ func (a ActivityTypeDeletedCertificate) Documentation() (activity string, detail
}`
}
type ActivityTypeAddedMicrosoftEntraTenant struct {
TenantID string `json:"tenant_id"`
}
func (a ActivityTypeAddedMicrosoftEntraTenant) ActivityName() string {
return "added_microsoft_entra_tenant"
}
func (a ActivityTypeAddedMicrosoftEntraTenant) Documentation() (activity string, details string, detailsExample string) {
return `Generated when Entra tenant is added.`,
`This activity contains the following field:
- "tenant_id": the ID of the Entra tenant.`, `{
"tenant_id": "ada00076-06f6-459b-8c45-88a843a2271f"
}`
}
type ActivityTypeDeletedMicrosoftEntraTenant struct {
TenantID string `json:"tenant_id"`
}
func (a ActivityTypeDeletedMicrosoftEntraTenant) ActivityName() string {
return "deleted_microsoft_entra_tenant"
}
func (a ActivityTypeDeletedMicrosoftEntraTenant) Documentation() (activity string, details string, detailsExample string) {
return `Generated when Entra tenant is deleted.`,
`This activity contains the following field:
- "tenant_id": the ID of the Entra tenant.`, `{
"tenant_id": "ada00076-06f6-459b-8c45-88a843a2271f"
}`
}
type ActivityTypeEditedEnrollSecrets struct {
TeamID *uint `json:"team_id"`
TeamName *string `json:"team_name"`
+54
View File
@@ -427,6 +427,11 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle
appConfig.MDM.WindowsMigrationEnabled = false
}
if oldAppConfig.MDM.WindowsEnabledAndConfigured != appConfig.MDM.WindowsEnabledAndConfigured &&
!appConfig.MDM.WindowsEnabledAndConfigured && len(newAppConfig.MDM.WindowsEntraTenantIDs.Value) == 0 {
appConfig.MDM.WindowsEntraTenantIDs.Value = []string{}
}
// EnableDiskEncryption is an optjson.Bool field in order to support the
// legacy field under "mdm.macos_settings". If the field provided to the
// PATCH endpoint is set but invalid (that is, "enable_disk_encryption":
@@ -800,6 +805,38 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle
return nil, err
}
addedEntraTenantIDs := make([]string, 0)
removedEntraTenantIDs := make([]string, 0)
oldTenantIDSet := make(map[string]struct{})
newTenantIDSet := make(map[string]struct{})
for _, tenantID := range oldAppConfig.MDM.WindowsEntraTenantIDs.Value {
oldTenantIDSet[tenantID] = struct{}{}
}
for _, tenantID := range appConfig.MDM.WindowsEntraTenantIDs.Value {
newTenantIDSet[tenantID] = struct{}{}
if _, found := oldTenantIDSet[tenantID]; !found {
addedEntraTenantIDs = append(addedEntraTenantIDs, tenantID)
}
}
for _, tenantID := range oldAppConfig.MDM.WindowsEntraTenantIDs.Value {
if _, found := newTenantIDSet[tenantID]; !found {
removedEntraTenantIDs = append(removedEntraTenantIDs, tenantID)
}
}
for _, tenantID := range addedEntraTenantIDs {
act := fleet.ActivityTypeAddedMicrosoftEntraTenant{TenantID: tenantID}
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), act); err != nil {
return nil, ctxerr.Wrap(ctx, err, "create activity for added Microsoft Entra tenant")
}
}
for _, tenantID := range removedEntraTenantIDs {
act := fleet.ActivityTypeDeletedMicrosoftEntraTenant{TenantID: tenantID}
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), act); err != nil {
return nil, ctxerr.Wrap(ctx, err, "create activity for deleted Microsoft Entra tenant")
}
}
// only create activities when config change has been persisted
switch {
@@ -1277,6 +1314,9 @@ func (svc *Service) validateMDM(
if mdm.EnableTurnOnWindowsMDMManually && !lic.IsPremium() {
invalid.Append("enable_turn_on_windows_mdm_manually", ErrMissingLicense.Error())
}
if len(mdm.WindowsEntraTenantIDs.Value) > 0 && !lic.IsPremium() {
invalid.Append("windows_entra_tenant_ids", ErrMissingLicense.Error())
}
// we want to use `oldMdm` here as this boolean is set by the fleet
// server at startup and can't be modified by the user
@@ -1471,6 +1511,20 @@ func (svc *Service) validateMDM(
invalid.Append("mdm.enable_turn_on_windows_mdm_manually", "Couldn't enable Turn on Windows MDM Manually, Windows MDM is not enabled.")
}
if !mdm.WindowsEnabledAndConfigured && len(mdm.WindowsEntraTenantIDs.Value) > 0 {
invalid.Append("mdm.windows_entra_tenant_ids", "Couldn't set Windows Entra tenant IDs, Windows MDM is not enabled.")
}
// validate Windows Entra tenant IDs are in the correct format (GUIDs). We can't use the standard UUID parser here
// as Azure tenants should be in the 8-4-4-4-12 format but the usual UUID parser will allow certain non-standard
// forms
guidRegex := regexp.MustCompile("^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$")
for _, tenantID := range mdm.WindowsEntraTenantIDs.Value {
if !guidRegex.MatchString(tenantID) {
invalid.Append("mdm.windows_entra_tenant_ids", fmt.Sprintf("Invalid Entra tenant ID: %s", tenantID))
}
}
if mdm.WindowsMigrationEnabled && mdm.EnableTurnOnWindowsMDMManually {
invalid.Append("mdm.enable_turn_on_windows_mdm_manually", "Couldn't enable Turn on Windows MDM Manually, Windows MDM migration is also enabled. Please enable only one.")
}
+4
View File
@@ -2030,6 +2030,10 @@ func (c *Client) DoGitOps(
if incoming.Controls.WindowsMigrationEnabled == nil {
mdmAppConfig["windows_migration_enabled"] = false
}
mdmAppConfig["windows_entra_tenant_ids"] = incoming.Controls.WindowsEntraTenantIDs
if incoming.Controls.WindowsEntraTenantIDs == nil {
mdmAppConfig["windows_entra_tenant_ids"] = []any{}
}
// Put in default values for enable_turn_on_windows_mdm_manually
mdmAppConfig["enable_turn_on_windows_mdm_manually"] = incoming.Controls.EnableTurnOnWindowsMDMManually
if incoming.Controls.EnableTurnOnWindowsMDMManually == nil {
+32
View File
@@ -7473,6 +7473,38 @@ func (s *integrationMDMTestSuite) TestAppConfigWindowsMDM() {
}
}
// Set an Entra tenant ID
acResp = appConfigResponse{}
// Should fail with invalid value
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
"mdm": { "windows_entra_tenant_ids": ["tenant-id-1234"] }
}`), http.StatusUnprocessableEntity, &acResp)
// Should pass and create activity with valid value
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
"mdm": { "windows_entra_tenant_ids": ["1a86b496-e2a4-43ef-ba00-20004e29b13b"] }
}`), http.StatusOK, &acResp)
s.lastActivityMatches(fleet.ActivityTypeAddedMicrosoftEntraTenant{}.ActivityName(), `{"tenant_id": "1a86b496-e2a4-43ef-ba00-20004e29b13b"}`, 0)
// Should pass and create activity with multiple valid values
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
"mdm": { "windows_entra_tenant_ids": ["1a86b496-e2a4-43ef-ba00-20004e29b13b", "6dca58c4-c817-4730-831b-f3348931df05"] }
}`), http.StatusOK, &acResp)
s.lastActivityMatches(fleet.ActivityTypeAddedMicrosoftEntraTenant{}.ActivityName(), `{"tenant_id": "6dca58c4-c817-4730-831b-f3348931df05"}`, 0)
// Should fail and not create a new activity with multiple valid values and one new invalid one
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
"mdm": { "windows_entra_tenant_ids": ["1a86b496-e2a4-43ef-ba00-20004e29b13b", "6dca58c4-c817-4730-831b-f3348931df05", "fleetie-was-here"] }
}`), http.StatusUnprocessableEntity, &acResp)
// Same activity as before
s.lastActivityMatches(fleet.ActivityTypeAddedMicrosoftEntraTenant{}.ActivityName(), `{"tenant_id": "6dca58c4-c817-4730-831b-f3348931df05"}`, 0)
// Should pass and create a deleted activity with less valid values than before
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
"mdm": { "windows_entra_tenant_ids": ["6dca58c4-c817-4730-831b-f3348931df05"] }
}`), http.StatusOK, &acResp)
s.lastActivityMatches(fleet.ActivityTypeDeletedMicrosoftEntraTenant{}.ActivityName(), `{"tenant_id": "1a86b496-e2a4-43ef-ba00-20004e29b13b"}`, 0)
// enable Windows MDM manual enrollment
acResp = appConfigResponse{}
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{