mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Clear Android cert records on unenroll. (#42920)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #42600 # 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`. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Fixed an issue where Android device certificate template records were not properly cleared during unenrollment, which previously resulted in stale certificate statuses after re-enrollment. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
83651ce49f
commit
2118dcb0d9
4 changed files with 81 additions and 15 deletions
1
changes/42600-android-cert-templates-cleared-on-reenroll
Normal file
1
changes/42600-android-cert-templates-cleared-on-reenroll
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Fixed a bug where Android host certificate template records were not cleared when a device unenrolled, causing stale certificate statuses after re-enrollment.
|
||||
|
|
@ -393,6 +393,14 @@ UPDATE host_mdm
|
|||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "delete Android custom OS settings for unenrolled hosts in bulk")
|
||||
}
|
||||
// Delete all certificate template records for Android hosts so they get re-created on re-enrollment.
|
||||
_, err = ds.writer(ctx).ExecContext(ctx, `
|
||||
DELETE hct FROM host_certificate_templates hct
|
||||
INNER JOIN hosts h ON h.uuid = hct.host_uuid
|
||||
WHERE h.platform = 'android'`)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "delete certificate templates for unenrolled android hosts in bulk")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -413,13 +421,21 @@ UPDATE host_mdm
|
|||
return ctxerr.Wrap(ctx, err, "get rows affected for set host_mdm unenrolled for android host")
|
||||
}
|
||||
if rows > 0 {
|
||||
var uuid string
|
||||
err = sqlx.GetContext(ctx, tx, &uuid, `SELECT uuid FROM hosts WHERE id = ?`, hostID)
|
||||
var hostUUID string
|
||||
err = sqlx.GetContext(ctx, tx, &hostUUID, `SELECT uuid FROM hosts WHERE id = ?`, hostID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get host uuid")
|
||||
}
|
||||
err = ds.deleteMDMOSCustomSettingsForHost(ctx, tx, uuid, "android")
|
||||
return ctxerr.Wrap(ctx, err, "delete Android custom OS settings for unenrolled host")
|
||||
err = ds.deleteMDMOSCustomSettingsForHost(ctx, tx, hostUUID, "android")
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "delete Android custom OS settings for unenrolled host")
|
||||
}
|
||||
// Delete certificate template records so they get re-created fresh on re-enrollment.
|
||||
// The device no longer has these certificates after unenrolling.
|
||||
_, err = tx.ExecContext(ctx, `DELETE FROM host_certificate_templates WHERE host_uuid = ?`, hostUUID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "delete certificate templates for unenrolled android host")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2268,6 +2268,18 @@ func testSetAndroidHostUnenrolled(t *testing.T, ds *Datastore) {
|
|||
upsertAndroidHostProfileStatus(t, ds, res.Host.UUID, "profile-1", &fleet.MDMDeliveryPending)
|
||||
upsertAndroidHostProfileStatus(t, ds, res.Host.UUID, "profile-2", &fleet.MDMDeliveryPending)
|
||||
|
||||
// Insert a certificate template record for this host to verify it gets deleted on unenroll.
|
||||
err = ds.BulkInsertHostCertificateTemplates(testCtx(), []fleet.HostCertificateTemplate{
|
||||
{
|
||||
HostUUID: res.Host.UUID,
|
||||
CertificateTemplateID: 1,
|
||||
Status: fleet.CertificateTemplateVerified,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
Name: "test-cert",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Perform single-host unenroll
|
||||
didUnenroll, err := ds.SetAndroidHostUnenrolled(testCtx(), res.Host.ID)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -2290,7 +2302,7 @@ func testSetAndroidHostUnenrolled(t *testing.T, ds *Datastore) {
|
|||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
return sqlx.GetContext(testCtx(), q, &mdmIDIsNull, `SELECT CASE WHEN mdm_id IS NULL THEN 1 ELSE 0 END FROM host_mdm WHERE host_id = ?`, res.Host.ID)
|
||||
})
|
||||
// validate profile records deleted
|
||||
// Validate profile records deleted
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
return sqlx.GetContext(testCtx(), q, &profileCountForHost, `SELECT COUNT(*) FROM host_mdm_android_profiles WHERE host_uuid=?`, res.Host.UUID)
|
||||
})
|
||||
|
|
@ -2298,6 +2310,11 @@ func testSetAndroidHostUnenrolled(t *testing.T, ds *Datastore) {
|
|||
assert.Equal(t, "", serverURL)
|
||||
assert.Equal(t, 1, mdmIDIsNull)
|
||||
assert.Equal(t, 0, profileCountForHost)
|
||||
|
||||
// Validate certificate template records deleted
|
||||
certRecords, err := ds.GetHostCertificateTemplates(testCtx(), res.Host.UUID)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, certRecords)
|
||||
}
|
||||
|
||||
func testBulkSetAndroidHostsUnenrolled(t *testing.T, ds *Datastore) {
|
||||
|
|
@ -2310,6 +2327,7 @@ func testBulkSetAndroidHostsUnenrolled(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, ds.SaveAppConfig(testCtx(), appCfg))
|
||||
|
||||
// Create 5 android hosts
|
||||
var androidHostUUIDs []string
|
||||
for i := 0; i < 5; i++ {
|
||||
esid := "enterprise-" + uuid.NewString()
|
||||
h := createAndroidHost(esid)
|
||||
|
|
@ -2318,6 +2336,19 @@ func testBulkSetAndroidHostsUnenrolled(t *testing.T, ds *Datastore) {
|
|||
|
||||
upsertAndroidHostProfileStatus(t, ds, res.Host.UUID, "profile-1", &fleet.MDMDeliveryPending)
|
||||
upsertAndroidHostProfileStatus(t, ds, res.Host.UUID, "profile-2", &fleet.MDMDeliveryPending)
|
||||
|
||||
// Insert a certificate template record for each host.
|
||||
err = ds.BulkInsertHostCertificateTemplates(testCtx(), []fleet.HostCertificateTemplate{
|
||||
{
|
||||
HostUUID: res.Host.UUID,
|
||||
CertificateTemplateID: 1,
|
||||
Status: fleet.CertificateTemplateVerified,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
Name: "test-cert",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
androidHostUUIDs = append(androidHostUUIDs, res.Host.UUID)
|
||||
}
|
||||
|
||||
// Create a macOS host (to verify we don't unenroll non-Android hosts)
|
||||
|
|
@ -2345,6 +2376,12 @@ func testBulkSetAndroidHostsUnenrolled(t *testing.T, ds *Datastore) {
|
|||
})
|
||||
assert.Equal(t, 10, androidHostProfileCount)
|
||||
require.Equal(t, 6, enrolledCount) // 5 android + 1 macOS
|
||||
// Verify each android host has a certificate template record.
|
||||
for _, hostUUID := range androidHostUUIDs {
|
||||
records, err := ds.GetHostCertificateTemplates(testCtx(), hostUUID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, records, 1)
|
||||
}
|
||||
|
||||
err = ds.BulkSetAndroidHostsUnenrolled(testCtx())
|
||||
require.NoError(t, err)
|
||||
|
|
@ -2353,11 +2390,18 @@ func testBulkSetAndroidHostsUnenrolled(t *testing.T, ds *Datastore) {
|
|||
})
|
||||
require.Equal(t, 1, enrolledCount)
|
||||
|
||||
// validate profile records deleted
|
||||
// Validate profile records deleted
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
return sqlx.GetContext(testCtx(), q, &androidHostProfileCount, `SELECT COUNT(*) FROM host_mdm_android_profiles`)
|
||||
})
|
||||
assert.Equal(t, 0, androidHostProfileCount)
|
||||
|
||||
// Validate certificate template records deleted for all android hosts
|
||||
for _, hostUUID := range androidHostUUIDs {
|
||||
records, err := ds.GetHostCertificateTemplates(testCtx(), hostUUID)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, records)
|
||||
}
|
||||
}
|
||||
|
||||
// setupTestApp creates a test Android app in vpp_apps table
|
||||
|
|
|
|||
|
|
@ -649,6 +649,17 @@ func (s *integrationMDMTestSuite) TestCertificateTemplateUnenrollReenroll() {
|
|||
s.verifyCertificateStatusWithSubject(t, host, orbitNodeKey, certTemplateID, certTemplateName, caID,
|
||||
fleet.CertificateTemplatePending, "", "CN="+host.HardwareSerial)
|
||||
|
||||
// Step: Simulate the certificate being successfully installed on the device (status = verified).
|
||||
// This is critical for testing that verified records are cleared on unenroll (issue #42600).
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
_, err := q.ExecContext(ctx,
|
||||
"UPDATE host_certificate_templates SET status = ?, uuid = UUID_TO_BIN(UUID(), true) WHERE host_uuid = ? AND certificate_template_id = ?",
|
||||
fleet.CertificateTemplateVerified, host.UUID, certTemplateID)
|
||||
return err
|
||||
})
|
||||
s.verifyCertificateStatusWithSubject(t, host, orbitNodeKey, certTemplateID, certTemplateName, caID,
|
||||
fleet.CertificateTemplateVerified, "", "CN="+host.HardwareSerial)
|
||||
|
||||
// Step: Unenroll the host (simulates pubsub DELETED message)
|
||||
unenrolled, err := s.ds.SetAndroidHostUnenrolled(ctx, host.ID)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -672,17 +683,11 @@ func (s *integrationMDMTestSuite) TestCertificateTemplateUnenrollReenroll() {
|
|||
require.NotZero(t, createResp.ID)
|
||||
certTemplateID2 := createResp.ID
|
||||
|
||||
// Step: Verify that the unenrolled host did NOT get a host_certificate_templates record for the second template.
|
||||
// The host API only returns profiles that have host_certificate_templates records, so the second
|
||||
// template should not appear in the profiles list.
|
||||
// Step: Verify that unenrolling cleared all certificate template records for this host.
|
||||
// Neither the previously verified first template nor the newly created second template should appear.
|
||||
var getHostResp getHostResponse
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
|
||||
require.NotNil(t, getHostResp.Host.MDM.Profiles)
|
||||
require.Len(t, *getHostResp.Host.MDM.Profiles, 1, "Only first template should appear (second was created while host was unenrolled)")
|
||||
profile := (*getHostResp.Host.MDM.Profiles)[0]
|
||||
require.Equal(t, certTemplateName, profile.Name, "First certificate template should be present")
|
||||
require.NotNil(t, profile.Status)
|
||||
require.Equal(t, string(fleet.CertificateTemplatePending), *profile.Status)
|
||||
require.Nil(t, getHostResp.Host.MDM.Profiles, "All certificate template records should be cleared on unenroll")
|
||||
|
||||
// Step: Re-enroll the host (simulates pubsub status report triggering UpdateAndroidHost with fromEnroll=true)
|
||||
err = s.ds.UpdateAndroidHost(ctx, createdAndroidHost, true, false)
|
||||
|
|
|
|||
Loading…
Reference in a new issue