Handle android certificates on deletion events (#37481)
This commit is contained in:
@@ -703,6 +703,17 @@ func (svc *Service) DeleteTeam(ctx context.Context, teamID uint) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle certificate templates associated with the team
|
||||
certTemplates, _, err := svc.ds.GetCertificateTemplatesByTeamID(ctx, teamID, fleet.ListOptions{})
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get certificate templates for team")
|
||||
}
|
||||
for _, ct := range certTemplates {
|
||||
if err := svc.ds.SetHostCertificateTemplatesToPendingRemove(ctx, ct.ID); err != nil {
|
||||
return ctxerr.Wrapf(ctx, err, "set hosts to pending remove for certificate template %d", ct.ID)
|
||||
}
|
||||
}
|
||||
|
||||
if err := svc.ds.DeleteTeam(ctx, teamID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
@@ -28,6 +29,9 @@ func (ds *Datastore) GetCertificateTemplateById(ctx context.Context, id uint) (*
|
||||
INNER JOIN certificate_authorities ON certificate_templates.certificate_authority_id = certificate_authorities.id
|
||||
WHERE certificate_templates.id = ?
|
||||
`, id); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ctxerr.Wrap(ctx, notFound("CertificateTemplate").WithID(id))
|
||||
}
|
||||
return nil, ctxerr.Wrap(ctx, err, "getting certificate_template by id")
|
||||
}
|
||||
|
||||
|
||||
@@ -457,3 +457,34 @@ func (ds *Datastore) RevertStaleCertificateTemplates(
|
||||
}
|
||||
return result.RowsAffected()
|
||||
}
|
||||
|
||||
// SetHostCertificateTemplatesToPendingRemove prepares certificate templates for removal.
|
||||
// For a given certificate template ID, it deletes any rows with status=pending and
|
||||
// updates all other rows to status=pending, operation_type=remove.
|
||||
func (ds *Datastore) SetHostCertificateTemplatesToPendingRemove(
|
||||
ctx context.Context,
|
||||
certificateTemplateID uint,
|
||||
) error {
|
||||
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
||||
// Delete rows with status=pending
|
||||
deleteStmt := fmt.Sprintf(`
|
||||
DELETE FROM host_certificate_templates
|
||||
WHERE certificate_template_id = ? AND status = '%s'
|
||||
`, fleet.CertificateTemplatePending)
|
||||
if _, err := tx.ExecContext(ctx, deleteStmt, certificateTemplateID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "delete pending host certificate templates")
|
||||
}
|
||||
|
||||
// Update all remaining rows to status=pending, operation_type=remove
|
||||
updateStmt := fmt.Sprintf(`
|
||||
UPDATE host_certificate_templates
|
||||
SET status = '%s', operation_type = '%s'
|
||||
WHERE certificate_template_id = ?
|
||||
`, fleet.CertificateTemplatePending, fleet.MDMOperationTypeRemove)
|
||||
if _, err := tx.ExecContext(ctx, updateStmt, certificateTemplateID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "update host certificate templates to pending remove")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ func TestHostCertificateTemplates(t *testing.T) {
|
||||
{"ListAndroidHostUUIDsWithPendingCertificateTemplates", testListAndroidHostUUIDsWithPendingCertificateTemplates},
|
||||
{"CertificateTemplateFullStateMachine", testCertificateTemplateFullStateMachine},
|
||||
{"RevertStaleCertificateTemplates", testRevertStaleCertificateTemplates},
|
||||
{"SetHostCertificateTemplatesToPendingRemove", testSetHostCertificateTemplatesToPendingRemove},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
@@ -965,3 +966,110 @@ func testRevertStaleCertificateTemplates(t *testing.T, ds *Datastore) {
|
||||
require.Equal(t, int64(0), affected)
|
||||
})
|
||||
}
|
||||
|
||||
func testSetHostCertificateTemplatesToPendingRemove(t *testing.T, ds *Datastore) {
|
||||
ctx := t.Context()
|
||||
|
||||
t.Run("deletes pending rows and updates others to pending remove", func(t *testing.T) {
|
||||
defer TruncateTables(t, ds)
|
||||
setup := createCertTemplateTestSetup(t, ctx, ds, "")
|
||||
|
||||
// Insert records with various statuses for the same template
|
||||
_, err := ds.writer(ctx).ExecContext(ctx, `
|
||||
INSERT INTO host_certificate_templates (host_uuid, certificate_template_id, status, operation_type, fleet_challenge, name) VALUES
|
||||
(?, ?, ?, ?, ?, ?),
|
||||
(?, ?, ?, ?, ?, ?),
|
||||
(?, ?, ?, ?, ?, ?),
|
||||
(?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
"host-pending", setup.template.ID, fleet.CertificateTemplatePending, fleet.MDMOperationTypeInstall, nil, setup.template.Name,
|
||||
"host-delivered", setup.template.ID, fleet.CertificateTemplateDelivered, fleet.MDMOperationTypeInstall, "challenge1", setup.template.Name,
|
||||
"host-verified", setup.template.ID, fleet.CertificateTemplateVerified, fleet.MDMOperationTypeInstall, "challenge2", setup.template.Name,
|
||||
"host-failed", setup.template.ID, fleet.CertificateTemplateFailed, fleet.MDMOperationTypeInstall, "challenge3", setup.template.Name,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.SetHostCertificateTemplatesToPendingRemove(ctx, setup.template.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the pending row was deleted
|
||||
var count int
|
||||
err = ds.writer(ctx).GetContext(ctx, &count,
|
||||
"SELECT COUNT(*) FROM host_certificate_templates WHERE host_uuid = ?", "host-pending")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, count)
|
||||
|
||||
// Verify remaining rows have status=pending and operation_type=remove
|
||||
var remaining []struct {
|
||||
HostUUID string `db:"host_uuid"`
|
||||
Status string `db:"status"`
|
||||
OperationType fleet.MDMOperationType `db:"operation_type"`
|
||||
}
|
||||
err = ds.writer(ctx).SelectContext(ctx, &remaining,
|
||||
"SELECT host_uuid, status, operation_type FROM host_certificate_templates ORDER BY host_uuid")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, remaining, 3)
|
||||
|
||||
for _, r := range remaining {
|
||||
require.Equal(t, string(fleet.CertificateTemplatePending), r.Status)
|
||||
require.Equal(t, fleet.MDMOperationTypeRemove, r.OperationType)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("only affects rows for the specified template", func(t *testing.T) {
|
||||
defer TruncateTables(t, ds)
|
||||
setup := createCertTemplateTestSetup(t, ctx, ds, "")
|
||||
|
||||
// Create a second template
|
||||
templateTwo, err := ds.CreateCertificateTemplate(ctx, &fleet.CertificateTemplate{
|
||||
Name: "Cert2",
|
||||
TeamID: setup.team.ID,
|
||||
CertificateAuthorityID: setup.ca.ID,
|
||||
SubjectName: "CN=Test2",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Insert records for both templates
|
||||
_, err = ds.writer(ctx).ExecContext(ctx, `
|
||||
INSERT INTO host_certificate_templates (host_uuid, certificate_template_id, status, operation_type, fleet_challenge, name) VALUES
|
||||
(?, ?, ?, ?, ?, ?),
|
||||
(?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
"host-1", setup.template.ID, fleet.CertificateTemplateDelivered, fleet.MDMOperationTypeInstall, "challenge1", setup.template.Name,
|
||||
"host-1", templateTwo.ID, fleet.CertificateTemplateDelivered, fleet.MDMOperationTypeInstall, "challenge2", templateTwo.Name,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Call the method for template one only
|
||||
err = ds.SetHostCertificateTemplatesToPendingRemove(ctx, setup.template.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify template one was updated
|
||||
var row struct {
|
||||
Status string `db:"status"`
|
||||
OperationType fleet.MDMOperationType `db:"operation_type"`
|
||||
}
|
||||
err = ds.writer(ctx).GetContext(ctx, &row,
|
||||
"SELECT status, operation_type FROM host_certificate_templates WHERE certificate_template_id = ?",
|
||||
setup.template.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, string(fleet.CertificateTemplatePending), row.Status)
|
||||
require.Equal(t, fleet.MDMOperationTypeRemove, row.OperationType)
|
||||
|
||||
// Verify template two was NOT affected
|
||||
err = ds.writer(ctx).GetContext(ctx, &row,
|
||||
"SELECT status, operation_type FROM host_certificate_templates WHERE certificate_template_id = ?",
|
||||
templateTwo.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, string(fleet.CertificateTemplateDelivered), row.Status)
|
||||
require.Equal(t, fleet.MDMOperationTypeInstall, row.OperationType)
|
||||
})
|
||||
|
||||
t.Run("handles no matching rows gracefully", func(t *testing.T) {
|
||||
defer TruncateTables(t, ds)
|
||||
|
||||
// Call with a non-existent template ID
|
||||
err := ds.SetHostCertificateTemplatesToPendingRemove(ctx, 99999)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -132,6 +132,7 @@ var teamRefs = []string{
|
||||
"mdm_windows_configuration_profiles",
|
||||
"mdm_apple_declarations",
|
||||
"mdm_android_configuration_profiles",
|
||||
"certificate_templates",
|
||||
"software_title_icons",
|
||||
"software_title_display_names",
|
||||
}
|
||||
|
||||
@@ -2593,6 +2593,11 @@ type Datastore interface {
|
||||
// RevertHostCertificateTemplatesToPending reverts specific host certificate templates from 'delivering' back to 'pending'.
|
||||
RevertHostCertificateTemplatesToPending(ctx context.Context, hostUUID string, certificateTemplateIDs []uint) error
|
||||
|
||||
// SetHostCertificateTemplatesToPendingRemove prepares certificate templates for removal.
|
||||
// For a given certificate template ID, it deletes any rows with status=pending and
|
||||
// updates all other rows to status=pending, operation_type=remove.
|
||||
SetHostCertificateTemplatesToPendingRemove(ctx context.Context, certificateTemplateID uint) error
|
||||
|
||||
// GetCurrentTime gets the current time from the database
|
||||
GetCurrentTime(ctx context.Context) (time.Time, error)
|
||||
|
||||
|
||||
@@ -1695,6 +1695,8 @@ type TransitionCertificateTemplatesToDeliveredFunc func(ctx context.Context, hos
|
||||
|
||||
type RevertHostCertificateTemplatesToPendingFunc func(ctx context.Context, hostUUID string, certificateTemplateIDs []uint) error
|
||||
|
||||
type SetHostCertificateTemplatesToPendingRemoveFunc func(ctx context.Context, certificateTemplateID uint) error
|
||||
|
||||
type GetCurrentTimeFunc func(ctx context.Context) (time.Time, error)
|
||||
|
||||
type UpdateOrDeleteHostMDMWindowsProfileFunc func(ctx context.Context, profile *fleet.HostMDMWindowsProfile) error
|
||||
@@ -4212,6 +4214,9 @@ type DataStore struct {
|
||||
RevertHostCertificateTemplatesToPendingFunc RevertHostCertificateTemplatesToPendingFunc
|
||||
RevertHostCertificateTemplatesToPendingFuncInvoked bool
|
||||
|
||||
SetHostCertificateTemplatesToPendingRemoveFunc SetHostCertificateTemplatesToPendingRemoveFunc
|
||||
SetHostCertificateTemplatesToPendingRemoveFuncInvoked bool
|
||||
|
||||
GetCurrentTimeFunc GetCurrentTimeFunc
|
||||
GetCurrentTimeFuncInvoked bool
|
||||
|
||||
@@ -10079,6 +10084,13 @@ func (s *DataStore) RevertHostCertificateTemplatesToPending(ctx context.Context,
|
||||
return s.RevertHostCertificateTemplatesToPendingFunc(ctx, hostUUID, certificateTemplateIDs)
|
||||
}
|
||||
|
||||
func (s *DataStore) SetHostCertificateTemplatesToPendingRemove(ctx context.Context, certificateTemplateID uint) error {
|
||||
s.mu.Lock()
|
||||
s.SetHostCertificateTemplatesToPendingRemoveFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.SetHostCertificateTemplatesToPendingRemoveFunc(ctx, certificateTemplateID)
|
||||
}
|
||||
|
||||
func (s *DataStore) GetCurrentTime(ctx context.Context) (time.Time, error) {
|
||||
s.mu.Lock()
|
||||
s.GetCurrentTimeFuncInvoked = true
|
||||
|
||||
@@ -283,6 +283,10 @@ func (svc *Service) DeleteCertificateTemplate(ctx context.Context, certificateTe
|
||||
return ctxerr.Wrap(ctx, err, "deleting certificate template")
|
||||
}
|
||||
|
||||
if err := svc.ds.SetHostCertificateTemplatesToPendingRemove(ctx, certificateTemplateID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "setting host certificate templates to pending remove")
|
||||
}
|
||||
|
||||
activity := fleet.ActivityTypeDeletedCertificate{
|
||||
Name: certificate.Name,
|
||||
}
|
||||
|
||||
@@ -8102,7 +8102,7 @@ func (s *integrationTestSuite) TestCertificatesSpecs() {
|
||||
})
|
||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&getCertResp))
|
||||
require.NoError(t, resp.Body.Close())
|
||||
require.Equal(t, getCertResp.Certificate.Status, fleet.CertificateTemplateFailed)
|
||||
require.Equal(t, getCertResp.Certificate.Status, fleet.CertificateTemplateFailed) // failed because no IDP user to replace variables
|
||||
|
||||
// Add an IDP user for the host
|
||||
err = s.ds.ReplaceHostDeviceMapping(ctx, host.ID, []*fleet.HostDeviceMapping{
|
||||
@@ -15065,3 +15065,170 @@ INSERT INTO host_certificate_templates (
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *integrationTestSuite) TestDeleteCertificateTemplate() {
|
||||
t := s.T()
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a test team
|
||||
team, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "TestDeleteCertificateTemplate Team"})
|
||||
require.NoError(t, err)
|
||||
teamID := team.ID
|
||||
|
||||
// Create a test certificate authority
|
||||
ca, err := s.ds.NewCertificateAuthority(ctx, &fleet.CertificateAuthority{
|
||||
Type: string(fleet.CATypeCustomSCEPProxy),
|
||||
Name: ptr.String("TestDeleteCertificateTemplate SCEP CA"),
|
||||
URL: ptr.String("http://localhost:8080/scep"),
|
||||
Challenge: ptr.String("test-challenge"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
caID := ca.ID
|
||||
|
||||
certTemplate := &fleet.CertificateTemplate{
|
||||
Name: "TestDeleteCertificateTemplate-Cert",
|
||||
TeamID: teamID,
|
||||
CertificateAuthorityID: caID,
|
||||
SubjectName: "CN=Test Subject",
|
||||
}
|
||||
savedTemplate, err := s.ds.CreateCertificateTemplate(ctx, certTemplate)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, savedTemplate)
|
||||
certificateTemplateID := savedTemplate.ID
|
||||
certTemplateName := savedTemplate.Name
|
||||
|
||||
// Create hosts with different certificate template statuses
|
||||
hostPending, err := s.ds.NewHost(ctx, &fleet.Host{
|
||||
UUID: uuid.New().String(),
|
||||
Hostname: "test-delete-cert-template-host-pending",
|
||||
Platform: "android",
|
||||
TeamID: &teamID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
hostDelivered, err := s.ds.NewHost(ctx, &fleet.Host{
|
||||
UUID: uuid.New().String(),
|
||||
Hostname: "test-delete-cert-template-host-delivered",
|
||||
Platform: "android",
|
||||
TeamID: &teamID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
hostVerified, err := s.ds.NewHost(ctx, &fleet.Host{
|
||||
UUID: uuid.New().String(),
|
||||
Hostname: "test-delete-cert-template-host-verified",
|
||||
Platform: "android",
|
||||
TeamID: &teamID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
hostFailed, err := s.ds.NewHost(ctx, &fleet.Host{
|
||||
UUID: uuid.New().String(),
|
||||
Hostname: "test-delete-cert-template-host-failed",
|
||||
Platform: "android",
|
||||
TeamID: &teamID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Insert host_certificate_templates with various statuses
|
||||
insertSQL := `
|
||||
INSERT INTO host_certificate_templates (host_uuid, certificate_template_id, status, operation_type, fleet_challenge, name)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
// Pending status - should be deleted
|
||||
_, err := q.ExecContext(ctx, insertSQL, hostPending.UUID, certificateTemplateID, "pending", "install", nil, certTemplateName)
|
||||
require.NoError(t, err)
|
||||
// Delivered status - should be updated to pending/remove
|
||||
_, err = q.ExecContext(ctx, insertSQL, hostDelivered.UUID, certificateTemplateID, "delivered", "install", "challenge1", certTemplateName)
|
||||
require.NoError(t, err)
|
||||
// Verified status - should be updated to pending/remove
|
||||
_, err = q.ExecContext(ctx, insertSQL, hostVerified.UUID, certificateTemplateID, "verified", "install", "challenge2", certTemplateName)
|
||||
require.NoError(t, err)
|
||||
// Failed status - should be updated to pending/remove
|
||||
_, err = q.ExecContext(ctx, insertSQL, hostFailed.UUID, certificateTemplateID, "failed", "install", "challenge3", certTemplateName)
|
||||
require.NoError(t, err)
|
||||
return nil
|
||||
})
|
||||
|
||||
// Enable Android MDM so GetHost returns certificate template profiles
|
||||
appCfg, err := s.ds.AppConfig(ctx)
|
||||
require.NoError(t, err)
|
||||
origAndroidEnabled := appCfg.MDM.AndroidEnabledAndConfigured
|
||||
appCfg.MDM.AndroidEnabledAndConfigured = true
|
||||
err = s.ds.SaveAppConfig(ctx, appCfg)
|
||||
require.NoError(t, err)
|
||||
err = s.ds.SetAndroidEnabledAndConfigured(ctx, true)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
appCfg.MDM.AndroidEnabledAndConfigured = origAndroidEnabled
|
||||
_ = s.ds.SaveAppConfig(ctx, appCfg)
|
||||
_ = s.ds.SetAndroidEnabledAndConfigured(ctx, origAndroidEnabled)
|
||||
}()
|
||||
|
||||
// Helper to find the certificate template profile by name
|
||||
findProfile := func(profiles *[]fleet.HostMDMProfile, name string) *fleet.HostMDMProfile {
|
||||
if profiles == nil {
|
||||
return nil
|
||||
}
|
||||
for _, p := range *profiles {
|
||||
if p.Name == name {
|
||||
return &p
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Verify the records exist before deletion via GetHost API
|
||||
var getHostResp getHostResponse
|
||||
for _, tc := range []struct {
|
||||
host *fleet.Host
|
||||
hostName string
|
||||
expectedStatus string
|
||||
}{
|
||||
{hostPending, "hostPending", string(fleet.CertificateTemplatePending)},
|
||||
{hostDelivered, "hostDelivered", string(fleet.CertificateTemplateDelivered)},
|
||||
{hostVerified, "hostVerified", string(fleet.CertificateTemplateVerified)},
|
||||
{hostFailed, "hostFailed", string(fleet.CertificateTemplateFailed)},
|
||||
} {
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", tc.host.ID), nil, http.StatusOK, &getHostResp)
|
||||
require.NotNil(t, getHostResp.Host.MDM.Profiles, "%s should have MDM profiles before deletion", tc.hostName)
|
||||
|
||||
profile := findProfile(getHostResp.Host.MDM.Profiles, certTemplateName)
|
||||
require.NotNil(t, profile, "%s should have certificate template profile %s before deletion", tc.hostName, certTemplateName)
|
||||
require.NotNil(t, profile.Status, "%s profile status should not be nil", tc.hostName)
|
||||
require.Equal(t, tc.expectedStatus, *profile.Status, "%s profile status should be %s before deletion", tc.hostName, tc.expectedStatus)
|
||||
require.Equal(t, fleet.MDMOperationTypeInstall, profile.OperationType, "%s profile operation_type should be install before deletion", tc.hostName)
|
||||
}
|
||||
|
||||
// Delete the certificate template via API
|
||||
var deleteResp deleteCertificateTemplateResponse
|
||||
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/certificates/%d", certificateTemplateID), nil, http.StatusOK, &deleteResp)
|
||||
|
||||
// After deletion:
|
||||
// - hostPending (pending/install) should have NO profile (record was deleted)
|
||||
// - hostDelivered, hostVerified, hostFailed should have pending/remove profiles
|
||||
// (kept for cron job to process removal from devices)
|
||||
|
||||
// Verify hostPending has no profile after deletion
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", hostPending.ID), nil, http.StatusOK, &getHostResp)
|
||||
profile := findProfile(getHostResp.Host.MDM.Profiles, certTemplateName)
|
||||
require.Nil(t, profile, "hostPending should not have certificate template profile after deletion")
|
||||
|
||||
// Verify hosts that had delivered/verified/failed status now have pending/remove profiles
|
||||
for _, tc := range []struct {
|
||||
host *fleet.Host
|
||||
hostName string
|
||||
}{
|
||||
{hostDelivered, "hostDelivered"},
|
||||
{hostVerified, "hostVerified"},
|
||||
{hostFailed, "hostFailed"},
|
||||
} {
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", tc.host.ID), nil, http.StatusOK, &getHostResp)
|
||||
profile := findProfile(getHostResp.Host.MDM.Profiles, certTemplateName)
|
||||
require.NotNil(t, profile, "%s should have pending remove profile after deletion", tc.hostName)
|
||||
require.NotNil(t, profile.Status, "%s profile status should not be nil", tc.hostName)
|
||||
require.Equal(t, string(fleet.CertificateTemplatePending), *profile.Status, "%s profile status should be pending after deletion", tc.hostName)
|
||||
require.Equal(t, fleet.MDMOperationTypeRemove, profile.OperationType, "%s profile operation_type should be remove after deletion", tc.hostName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22981,3 +22981,199 @@ func (s *integrationEnterpriseTestSuite) TestTeamLabelsDistributedReadWrite() {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestDeleteTeamCertificateTemplates() {
|
||||
t := s.T()
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a test team
|
||||
team, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "TestDeleteTeamCertificateTemplates Team"})
|
||||
require.NoError(t, err)
|
||||
teamID := team.ID
|
||||
|
||||
// Create a test certificate authority
|
||||
ca, err := s.ds.NewCertificateAuthority(ctx, &fleet.CertificateAuthority{
|
||||
Type: string(fleet.CATypeCustomSCEPProxy),
|
||||
Name: ptr.String("TestDeleteTeamCertificateTemplates SCEP CA"),
|
||||
URL: ptr.String("http://localhost:8080/scep"),
|
||||
Challenge: ptr.String("test-challenge"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
caID := ca.ID
|
||||
|
||||
// Create two certificate templates on the team
|
||||
certTemplate1 := &fleet.CertificateTemplate{
|
||||
Name: "TestDeleteTeamCertificateTemplates-Cert1",
|
||||
TeamID: teamID,
|
||||
CertificateAuthorityID: caID,
|
||||
SubjectName: "CN=Test Subject 1",
|
||||
}
|
||||
savedTemplate1, err := s.ds.CreateCertificateTemplate(ctx, certTemplate1)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, savedTemplate1)
|
||||
certificateTemplateID1 := savedTemplate1.ID
|
||||
certTemplateName1 := savedTemplate1.Name
|
||||
|
||||
certTemplate2 := &fleet.CertificateTemplate{
|
||||
Name: "TestDeleteTeamCertificateTemplates-Cert2",
|
||||
TeamID: teamID,
|
||||
CertificateAuthorityID: caID,
|
||||
SubjectName: "CN=Test Subject 2",
|
||||
}
|
||||
savedTemplate2, err := s.ds.CreateCertificateTemplate(ctx, certTemplate2)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, savedTemplate2)
|
||||
certificateTemplateID2 := savedTemplate2.ID
|
||||
certTemplateName2 := savedTemplate2.Name
|
||||
|
||||
// Create hosts with different certificate template statuses
|
||||
hostPending, err := s.ds.NewHost(ctx, &fleet.Host{
|
||||
UUID: uuid.New().String(),
|
||||
Hostname: "test-delete-team-cert-template-host-pending",
|
||||
Platform: "android",
|
||||
TeamID: &teamID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
hostDelivered, err := s.ds.NewHost(ctx, &fleet.Host{
|
||||
UUID: uuid.New().String(),
|
||||
Hostname: "test-delete-team-cert-template-host-delivered",
|
||||
Platform: "android",
|
||||
TeamID: &teamID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
hostVerified, err := s.ds.NewHost(ctx, &fleet.Host{
|
||||
UUID: uuid.New().String(),
|
||||
Hostname: "test-delete-team-cert-template-host-verified",
|
||||
Platform: "android",
|
||||
TeamID: &teamID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
hostFailed, err := s.ds.NewHost(ctx, &fleet.Host{
|
||||
UUID: uuid.New().String(),
|
||||
Hostname: "test-delete-team-cert-template-host-failed",
|
||||
Platform: "android",
|
||||
TeamID: &teamID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Insert host_certificate_templates with various statuses for both templates
|
||||
insertSQL := `
|
||||
INSERT INTO host_certificate_templates (host_uuid, certificate_template_id, status, operation_type, fleet_challenge, name)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
// Template 1 - Various statuses
|
||||
// Pending status - should be deleted
|
||||
_, err := q.ExecContext(ctx, insertSQL, hostPending.UUID, certificateTemplateID1, "pending", "install", nil, certTemplateName1)
|
||||
require.NoError(t, err)
|
||||
// Delivered status - should be updated to pending/remove
|
||||
_, err = q.ExecContext(ctx, insertSQL, hostDelivered.UUID, certificateTemplateID1, "delivered", "install", "challenge1", certTemplateName1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Template 2 - Various statuses
|
||||
// Verified status - should be updated to pending/remove
|
||||
_, err = q.ExecContext(ctx, insertSQL, hostVerified.UUID, certificateTemplateID2, "verified", "install", "challenge2", certTemplateName2)
|
||||
require.NoError(t, err)
|
||||
// Failed status - should be updated to pending/remove
|
||||
_, err = q.ExecContext(ctx, insertSQL, hostFailed.UUID, certificateTemplateID2, "failed", "install", "challenge3", certTemplateName2)
|
||||
require.NoError(t, err)
|
||||
return nil
|
||||
})
|
||||
|
||||
// Enable Android MDM so GetHost returns certificate template profiles
|
||||
appCfg, err := s.ds.AppConfig(ctx)
|
||||
require.NoError(t, err)
|
||||
origAndroidEnabled := appCfg.MDM.AndroidEnabledAndConfigured
|
||||
appCfg.MDM.AndroidEnabledAndConfigured = true
|
||||
err = s.ds.SaveAppConfig(ctx, appCfg)
|
||||
require.NoError(t, err)
|
||||
err = s.ds.SetAndroidEnabledAndConfigured(ctx, true)
|
||||
require.NoError(t, err)
|
||||
// Wait for cache to expire (default 1 second)
|
||||
time.Sleep(2 * time.Second)
|
||||
defer func() {
|
||||
appCfg.MDM.AndroidEnabledAndConfigured = origAndroidEnabled
|
||||
_ = s.ds.SaveAppConfig(ctx, appCfg)
|
||||
_ = s.ds.SetAndroidEnabledAndConfigured(ctx, origAndroidEnabled)
|
||||
}()
|
||||
|
||||
// Helper to find the certificate template profile by name
|
||||
findProfile := func(profiles *[]fleet.HostMDMProfile, name string) *fleet.HostMDMProfile {
|
||||
if profiles == nil {
|
||||
return nil
|
||||
}
|
||||
for _, p := range *profiles {
|
||||
if p.Name == name {
|
||||
return &p
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Verify the records exist before team deletion via GetHost API
|
||||
var getHostResp getHostResponse
|
||||
for _, tc := range []struct {
|
||||
host *fleet.Host
|
||||
hostName string
|
||||
expectedStatus string
|
||||
templateName string
|
||||
}{
|
||||
{hostPending, "hostPending", string(fleet.CertificateTemplatePending), certTemplateName1},
|
||||
{hostDelivered, "hostDelivered", string(fleet.CertificateTemplateDelivered), certTemplateName1},
|
||||
{hostVerified, "hostVerified", string(fleet.CertificateTemplateVerified), certTemplateName2},
|
||||
{hostFailed, "hostFailed", string(fleet.CertificateTemplateFailed), certTemplateName2},
|
||||
} {
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", tc.host.ID), nil, http.StatusOK, &getHostResp)
|
||||
require.NotNil(t, getHostResp.Host.MDM.Profiles, "%s should have MDM profiles before deletion", tc.hostName)
|
||||
|
||||
profile := findProfile(getHostResp.Host.MDM.Profiles, tc.templateName)
|
||||
require.NotNil(t, profile, "%s should have certificate template profile %s before deletion", tc.hostName, tc.templateName)
|
||||
require.NotNil(t, profile.Status, "%s profile status should not be nil", tc.hostName)
|
||||
require.Equal(t, tc.expectedStatus, *profile.Status, "%s profile status should be %s before deletion", tc.hostName, tc.expectedStatus)
|
||||
require.Equal(t, fleet.MDMOperationTypeInstall, profile.OperationType, "%s profile operation_type should be install before deletion", tc.hostName)
|
||||
}
|
||||
|
||||
// Delete the team via API (this should delete all certificate templates on the team)
|
||||
var deleteResp deleteTeamResponse
|
||||
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/teams/%d", teamID), nil, http.StatusOK, &deleteResp)
|
||||
|
||||
// Verify the certificate templates were deleted
|
||||
_, err = s.ds.GetCertificateTemplateById(ctx, certificateTemplateID1)
|
||||
require.Error(t, err)
|
||||
require.True(t, fleet.IsNotFound(err), "certificate template 1 should be deleted")
|
||||
|
||||
_, err = s.ds.GetCertificateTemplateById(ctx, certificateTemplateID2)
|
||||
require.Error(t, err)
|
||||
require.True(t, fleet.IsNotFound(err), "certificate template 2 should be deleted")
|
||||
|
||||
// After team deletion:
|
||||
// - hostPending (pending/install) should have NO profile (record was deleted)
|
||||
// - hostDelivered, hostVerified, hostFailed should have pending/remove profiles
|
||||
// (kept for cron job to process removal from devices)
|
||||
|
||||
// Verify hostPending has no profile after deletion
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", hostPending.ID), nil, http.StatusOK, &getHostResp)
|
||||
profile := findProfile(getHostResp.Host.MDM.Profiles, certTemplateName1)
|
||||
require.Nil(t, profile, "hostPending should not have certificate template profile after deletion")
|
||||
|
||||
// Verify hosts that had delivered/verified/failed status now have pending/remove profiles
|
||||
for _, tc := range []struct {
|
||||
host *fleet.Host
|
||||
hostName string
|
||||
templateName string
|
||||
}{
|
||||
{hostDelivered, "hostDelivered", certTemplateName1},
|
||||
{hostVerified, "hostVerified", certTemplateName2},
|
||||
{hostFailed, "hostFailed", certTemplateName2},
|
||||
} {
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", tc.host.ID), nil, http.StatusOK, &getHostResp)
|
||||
profile := findProfile(getHostResp.Host.MDM.Profiles, tc.templateName)
|
||||
require.NotNil(t, profile, "%s should have pending remove profile after deletion", tc.hostName)
|
||||
require.NotNil(t, profile.Status, "%s profile status should not be nil", tc.hostName)
|
||||
require.Equal(t, string(fleet.CertificateTemplatePending), *profile.Status, "%s profile status should be pending after deletion", tc.hostName)
|
||||
require.Equal(t, fleet.MDMOperationTypeRemove, profile.OperationType, "%s profile operation_type should be remove after deletion", tc.hostName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,14 @@ func TestTeamAuth(t *testing.T) {
|
||||
return nil, ¬FoundError{}
|
||||
}
|
||||
|
||||
ds.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, opts fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
|
||||
return []*fleet.CertificateTemplateResponseSummary{}, nil, nil
|
||||
}
|
||||
|
||||
ds.SetHostCertificateTemplatesToPendingRemoveFunc = func(ctx context.Context, teamID uint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
user *fleet.User
|
||||
|
||||
Reference in New Issue
Block a user