mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
41542 android cert resend backend (#42099)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #41542
This commit is contained in:
parent
d55d2571bb
commit
4df9ae01a6
13 changed files with 426 additions and 2 deletions
1
changes/41542-android-cert-resend-backend
Normal file
1
changes/41542-android-cert-resend-backend
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Add backend to resend Android certificates
|
||||
|
|
@ -400,3 +400,30 @@ func (ds *Datastore) CreatePendingCertificateTemplatesForNewHost(
|
|||
}
|
||||
return result.RowsAffected()
|
||||
}
|
||||
|
||||
func (ds *Datastore) ResendHostCertificateTemplate(ctx context.Context, hostID uint, templateID uint) error {
|
||||
const stmt = `
|
||||
UPDATE
|
||||
host_certificate_templates hct
|
||||
INNER JOIN
|
||||
hosts h ON h.uuid = hct.host_uuid
|
||||
SET
|
||||
hct.uuid = UUID_TO_BIN(UUID(), true),
|
||||
hct.status = ?
|
||||
WHERE
|
||||
h.id = ? AND
|
||||
hct.certificate_template_id = ?
|
||||
`
|
||||
|
||||
results, err := ds.writer(ctx).ExecContext(ctx, stmt, fleet.CertificateTemplatePending, hostID, templateID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "updating host certificate template uuid")
|
||||
}
|
||||
|
||||
affected, _ := results.RowsAffected()
|
||||
if affected == 0 {
|
||||
return ctxerr.Wrapf(ctx, notFound("HostCertificateTemplate"), "template %d does not exist for host %d", templateID, hostID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ func TestCertificates(t *testing.T) {
|
|||
{"GetHostCertificateTemplates", testGetHostCertificateTemplates},
|
||||
{"GetCertificateTemplateForHost", testGetCertificateTemplateForHost},
|
||||
{"GetHostCertificateTemplateRecord", testGetHostCertificateTemplateRecord},
|
||||
{"ResendHostCertificateTemplate", testResendHostCertificateTemplate},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
|
|
@ -1325,3 +1326,96 @@ func testGetHostCertificateTemplateRecord(t *testing.T, ds *Datastore) {
|
|||
require.Equal(t, fleet.CertificateTemplateDelivered, result.Status)
|
||||
})
|
||||
}
|
||||
|
||||
func testResendHostCertificateTemplate(t *testing.T, ds *Datastore) {
|
||||
defer TruncateTables(t, ds)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "Team 1"})
|
||||
require.NoError(t, err)
|
||||
|
||||
h1 := test.NewHost(t, ds, "host_1", "127.0.0.1", "1", "1", time.Now())
|
||||
h1.TeamID = &team1.ID
|
||||
err = ds.UpdateHost(ctx, h1)
|
||||
require.NoError(t, err)
|
||||
|
||||
ca, err := ds.NewCertificateAuthority(ctx, &fleet.CertificateAuthority{
|
||||
Type: string(fleet.CATypeCustomSCEPProxy),
|
||||
Name: ptr.String("Test SCEP CA"),
|
||||
URL: ptr.String("http://localhost:8080/scep"),
|
||||
Challenge: ptr.String("test-challenge"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
ct1, err := ds.CreateCertificateTemplate(ctx, &fleet.CertificateTemplate{
|
||||
Name: "Template1",
|
||||
TeamID: team1.ID,
|
||||
CertificateAuthorityID: ca.ID,
|
||||
SubjectName: "CN=Test Subject 1",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
initialStatus fleet.CertificateTemplateStatus
|
||||
}{
|
||||
{"from verified", fleet.CertificateTemplateVerified},
|
||||
{"from delivered", fleet.CertificateTemplateDelivered},
|
||||
{"from failed", fleet.CertificateTemplateFailed},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err = ds.BulkInsertHostCertificateTemplates(ctx, []fleet.HostCertificateTemplate{
|
||||
{
|
||||
HostUUID: h1.UUID,
|
||||
CertificateTemplateID: ct1.ID,
|
||||
Status: tc.initialStatus,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
Name: "Template1",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
originalRecord, err := ds.GetHostCertificateTemplateRecord(ctx, h1.UUID, ct1.ID)
|
||||
require.NoError(t, err)
|
||||
originalUUID := originalRecord.UUID
|
||||
|
||||
err = ds.ResendHostCertificateTemplate(ctx, h1.ID, ct1.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
updated, err := ds.GetHostCertificateTemplateRecord(ctx, h1.UUID, ct1.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fleet.CertificateTemplatePending, updated.Status)
|
||||
require.NotEqual(t, originalUUID, updated.UUID, "UUID should change after resend")
|
||||
require.NotEmpty(t, updated.UUID)
|
||||
|
||||
// Clean up for next subtest
|
||||
err = ds.DeleteHostCertificateTemplate(ctx, h1.UUID, ct1.ID)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("returns error for non-existent host", func(t *testing.T) {
|
||||
err := ds.ResendHostCertificateTemplate(ctx, 99999, ct1.ID)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("returns error for non-existent template", func(t *testing.T) {
|
||||
// Insert a record first so the host exists with some template
|
||||
err = ds.BulkInsertHostCertificateTemplates(ctx, []fleet.HostCertificateTemplate{
|
||||
{
|
||||
HostUUID: h1.UUID,
|
||||
CertificateTemplateID: ct1.ID,
|
||||
Status: fleet.CertificateTemplateVerified,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
Name: "Template1",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err := ds.ResendHostCertificateTemplate(ctx, h1.ID, 99999)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,6 +166,7 @@ var ActivityDetailsList = []ActivityDetails{
|
|||
ActivityTypeDeletedAndroidProfile{},
|
||||
ActivityTypeEditedAndroidProfile{},
|
||||
ActivityTypeEditedAndroidCertificate{},
|
||||
ActivityTypeResentCertificate{},
|
||||
|
||||
ActivityTypeResentConfigurationProfile{},
|
||||
ActivityTypeResentConfigurationProfileBatch{},
|
||||
|
|
@ -1749,6 +1750,35 @@ func (a ActivityTypeEditedAndroidCertificate) ActivityName() string {
|
|||
return "edited_android_certificate"
|
||||
}
|
||||
|
||||
type ActivityTypeResentCertificate struct {
|
||||
HostID uint `json:"host_id"`
|
||||
HostDisplayName string `json:"host_display_name"`
|
||||
CertificateTemplateID uint `json:"certificate_template_id"`
|
||||
CertificateName string `json:"certificate_name"`
|
||||
}
|
||||
|
||||
func (a ActivityTypeResentCertificate) ActivityName() string {
|
||||
return "resent_certificate"
|
||||
}
|
||||
|
||||
func (a ActivityTypeResentCertificate) HostIDs() []uint {
|
||||
return []uint{a.HostID}
|
||||
}
|
||||
|
||||
func (a ActivityTypeResentCertificate) Documentation() (activity string, details string, detailsExample string) {
|
||||
return `Generated when a user resends a certificate to a host.`,
|
||||
`This activity contains the following fields:
|
||||
- "host_id": The ID of the host.
|
||||
- "host_display_name": The display name of the host.
|
||||
- "certificate_template_id": The ID of the certificate template
|
||||
- "certificate_name": The name of the certificate`, `{
|
||||
"host_id": 1,
|
||||
"host_display_name": "Anna's MacBook Pro",
|
||||
"certificate_template_id": 123,
|
||||
"certificate_name": "Zero trust certificate"
|
||||
}`
|
||||
}
|
||||
|
||||
type ActivityTypeEditedHostIdpData struct {
|
||||
HostID uint `json:"host_id"`
|
||||
HostDisplayName string `json:"host_display_name"`
|
||||
|
|
|
|||
|
|
@ -2744,6 +2744,8 @@ type Datastore interface {
|
|||
// DeleteHostCertificateTemplate deletes a single host_certificate_template record
|
||||
// identified by host_uuid and certificate_template_id.
|
||||
DeleteHostCertificateTemplate(ctx context.Context, hostUUID string, certificateTemplateID uint) error
|
||||
// ResendHostCertificateTemplate queues a certificate template to be resent to a device
|
||||
ResendHostCertificateTemplate(ctx context.Context, hostID uint, templateID uint) error
|
||||
|
||||
// ListAndroidHostUUIDsWithPendingCertificateTemplates returns hosts that have
|
||||
// certificate templates in 'pending' status ready for delivery.
|
||||
|
|
|
|||
|
|
@ -507,6 +507,7 @@ type Service interface {
|
|||
|
||||
// ListHostCertificates lists the certificates installed on the specified host.
|
||||
ListHostCertificates(ctx context.Context, hostID uint, opts ListOptions) ([]*HostCertificatePayload, *PaginationMetadata, error)
|
||||
|
||||
// GetHostRecoveryLockPassword retrieves the recovery lock password for the specified host.
|
||||
// Requires admin or maintainer role and MDM to be enabled.
|
||||
GetHostRecoveryLockPassword(ctx context.Context, hostID uint) (*HostRecoveryLockPassword, error)
|
||||
|
|
@ -688,6 +689,7 @@ type Service interface {
|
|||
ApplyCertificateTemplateSpecs(ctx context.Context, specs []*CertificateRequestSpec) error
|
||||
DeleteCertificateTemplateSpecs(ctx context.Context, certificateTemplateIDs []uint, teamID uint) error
|
||||
UpdateCertificateStatus(ctx context.Context, update *CertificateStatusUpdate) error
|
||||
ResendHostCertificateTemplate(ctx context.Context, hostID uint, templateID uint) error
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////////
|
||||
// GlobalScheduleService
|
||||
|
|
|
|||
|
|
@ -1797,6 +1797,8 @@ type DeleteHostCertificateTemplatesFunc func(ctx context.Context, hostCertTempla
|
|||
|
||||
type DeleteHostCertificateTemplateFunc func(ctx context.Context, hostUUID string, certificateTemplateID uint) error
|
||||
|
||||
type ResendHostCertificateTemplateFunc func(ctx context.Context, hostID uint, templateID uint) error
|
||||
|
||||
type ListAndroidHostUUIDsWithPendingCertificateTemplatesFunc func(ctx context.Context, offset int, limit int) ([]string, error)
|
||||
|
||||
type GetAndTransitionCertificateTemplatesToDeliveringFunc func(ctx context.Context, hostUUID string) (*fleet.HostCertificateTemplatesForDelivery, error)
|
||||
|
|
@ -4493,6 +4495,9 @@ type DataStore struct {
|
|||
DeleteHostCertificateTemplateFunc DeleteHostCertificateTemplateFunc
|
||||
DeleteHostCertificateTemplateFuncInvoked bool
|
||||
|
||||
ResendHostCertificateTemplateFunc ResendHostCertificateTemplateFunc
|
||||
ResendHostCertificateTemplateFuncInvoked bool
|
||||
|
||||
ListAndroidHostUUIDsWithPendingCertificateTemplatesFunc ListAndroidHostUUIDsWithPendingCertificateTemplatesFunc
|
||||
ListAndroidHostUUIDsWithPendingCertificateTemplatesFuncInvoked bool
|
||||
|
||||
|
|
@ -10756,6 +10761,13 @@ func (s *DataStore) DeleteHostCertificateTemplate(ctx context.Context, hostUUID
|
|||
return s.DeleteHostCertificateTemplateFunc(ctx, hostUUID, certificateTemplateID)
|
||||
}
|
||||
|
||||
func (s *DataStore) ResendHostCertificateTemplate(ctx context.Context, hostID uint, templateID uint) error {
|
||||
s.mu.Lock()
|
||||
s.ResendHostCertificateTemplateFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.ResendHostCertificateTemplateFunc(ctx, hostID, templateID)
|
||||
}
|
||||
|
||||
func (s *DataStore) ListAndroidHostUUIDsWithPendingCertificateTemplates(ctx context.Context, offset int, limit int) ([]string, error) {
|
||||
s.mu.Lock()
|
||||
s.ListAndroidHostUUIDsWithPendingCertificateTemplatesFuncInvoked = true
|
||||
|
|
|
|||
|
|
@ -421,6 +421,8 @@ type DeleteCertificateTemplateSpecsFunc func(ctx context.Context, certificateTem
|
|||
|
||||
type UpdateCertificateStatusFunc func(ctx context.Context, update *fleet.CertificateStatusUpdate) error
|
||||
|
||||
type ResendHostCertificateTemplateFunc func(ctx context.Context, hostID uint, templateID uint) error
|
||||
|
||||
type GlobalScheduleQueryFunc func(ctx context.Context, sq *fleet.ScheduledQuery) (*fleet.ScheduledQuery, error)
|
||||
|
||||
type GetGlobalScheduledQueriesFunc func(ctx context.Context, opts fleet.ListOptions) ([]*fleet.ScheduledQuery, error)
|
||||
|
|
@ -1499,6 +1501,9 @@ type Service struct {
|
|||
UpdateCertificateStatusFunc UpdateCertificateStatusFunc
|
||||
UpdateCertificateStatusFuncInvoked bool
|
||||
|
||||
ResendHostCertificateTemplateFunc ResendHostCertificateTemplateFunc
|
||||
ResendHostCertificateTemplateFuncInvoked bool
|
||||
|
||||
GlobalScheduleQueryFunc GlobalScheduleQueryFunc
|
||||
GlobalScheduleQueryFuncInvoked bool
|
||||
|
||||
|
|
@ -3620,6 +3625,13 @@ func (s *Service) UpdateCertificateStatus(ctx context.Context, update *fleet.Cer
|
|||
return s.UpdateCertificateStatusFunc(ctx, update)
|
||||
}
|
||||
|
||||
func (s *Service) ResendHostCertificateTemplate(ctx context.Context, hostID uint, templateID uint) error {
|
||||
s.mu.Lock()
|
||||
s.ResendHostCertificateTemplateFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.ResendHostCertificateTemplateFunc(ctx, hostID, templateID)
|
||||
}
|
||||
|
||||
func (s *Service) GlobalScheduleQuery(ctx context.Context, sq *fleet.ScheduledQuery) (*fleet.ScheduledQuery, error) {
|
||||
s.mu.Lock()
|
||||
s.GlobalScheduleQueryFuncInvoked = true
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
activity_api "github.com/fleetdm/fleet/v4/server/activity/api"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mock"
|
||||
|
|
@ -379,3 +380,79 @@ func TestApplyCertificateTemplateSpecs(t *testing.T) {
|
|||
require.Contains(t, err.Error(), "Template 2")
|
||||
})
|
||||
}
|
||||
|
||||
func TestResendHostCertificateTemplate(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
opts := &TestServerOpts{}
|
||||
svc, ctx := newTestService(t, ds, nil, nil, opts)
|
||||
|
||||
const (
|
||||
hostID = uint(1)
|
||||
templateID = uint(42)
|
||||
teamID = uint(10)
|
||||
templateName = "My Cert"
|
||||
)
|
||||
|
||||
ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
|
||||
|
||||
ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
|
||||
if id == hostID {
|
||||
tid := teamID
|
||||
return &fleet.Host{ID: id, TeamID: &tid}, nil
|
||||
}
|
||||
return nil, errors.New("host not found")
|
||||
}
|
||||
|
||||
ds.GetCertificateTemplateByIdFunc = func(ctx context.Context, id uint) (*fleet.CertificateTemplateResponse, error) {
|
||||
return &fleet.CertificateTemplateResponse{
|
||||
CertificateTemplateResponseSummary: fleet.CertificateTemplateResponseSummary{
|
||||
ID: id,
|
||||
Name: templateName,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
t.Run("succeeds and creates activity", func(t *testing.T) {
|
||||
ds.ResendHostCertificateTemplateFunc = func(ctx context.Context, hID uint, tID uint) error {
|
||||
require.Equal(t, hostID, hID)
|
||||
require.Equal(t, templateID, tID)
|
||||
return nil
|
||||
}
|
||||
|
||||
var capturedActivity fleet.ActivityTypeResentCertificate
|
||||
opts.ActivityMock.NewActivityFunc = func(_ context.Context, _ *activity_api.User, activity activity_api.ActivityDetails) error {
|
||||
act, ok := activity.(fleet.ActivityTypeResentCertificate)
|
||||
require.True(t, ok, "expected ActivityTypeResentCertificate, got %T", activity)
|
||||
capturedActivity = act
|
||||
return nil
|
||||
}
|
||||
|
||||
err := svc.ResendHostCertificateTemplate(ctx, hostID, templateID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, ds.ResendHostCertificateTemplateFuncInvoked)
|
||||
require.True(t, opts.ActivityMock.NewActivityFuncInvoked)
|
||||
require.Equal(t, hostID, capturedActivity.HostID)
|
||||
require.Equal(t, templateID, capturedActivity.CertificateTemplateID)
|
||||
require.Equal(t, templateName, capturedActivity.CertificateName)
|
||||
|
||||
ds.ResendHostCertificateTemplateFuncInvoked = false
|
||||
opts.ActivityMock.NewActivityFuncInvoked = false
|
||||
})
|
||||
|
||||
t.Run("returns error when host not found", func(t *testing.T) {
|
||||
err := svc.ResendHostCertificateTemplate(ctx, 99999, templateID)
|
||||
require.Error(t, err)
|
||||
require.False(t, opts.ActivityMock.NewActivityFuncInvoked)
|
||||
})
|
||||
|
||||
t.Run("returns error when datastore fails", func(t *testing.T) {
|
||||
ds.ResendHostCertificateTemplateFunc = func(ctx context.Context, hID uint, tID uint) error {
|
||||
return errors.New("db error")
|
||||
}
|
||||
|
||||
err := svc.ResendHostCertificateTemplate(ctx, hostID, templateID)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "db error")
|
||||
require.False(t, opts.ActivityMock.NewActivityFuncInvoked)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -697,3 +697,60 @@ func (svc *Service) UpdateCertificateStatus(ctx context.Context, update *fleet.C
|
|||
update.HostUUID = host.UUID
|
||||
return svc.ds.UpsertCertificateStatus(ctx, update)
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Resend Host Certificate Template
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type resendHostCertificateTemplateRequest struct {
|
||||
ID uint `url:"id"`
|
||||
TemplateID uint `url:"template_id"`
|
||||
}
|
||||
|
||||
type resendHostCertificateTemplateResponse struct {
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r resendHostCertificateTemplateResponse) Error() error { return r.Err }
|
||||
|
||||
func resendHostCertificateTemplateEndpoint(ctx context.Context, request any, svc fleet.Service) (fleet.Errorer, error) {
|
||||
req := request.(*resendHostCertificateTemplateRequest)
|
||||
err := svc.ResendHostCertificateTemplate(ctx, req.ID, req.TemplateID)
|
||||
if err != nil {
|
||||
return resendHostCertificateTemplateResponse{Err: err}, nil
|
||||
}
|
||||
|
||||
return resendHostCertificateTemplateResponse{}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) ResendHostCertificateTemplate(ctx context.Context, hostID uint, templateID uint) error {
|
||||
host, err := svc.ds.HostLite(ctx, hostID)
|
||||
if err != nil {
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
return ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
|
||||
if err := svc.authz.Authorize(ctx, &fleet.MDMConfigProfileAuthz{TeamID: host.TeamID}, fleet.ActionResend); err != nil {
|
||||
return ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
|
||||
if err := svc.ds.ResendHostCertificateTemplate(ctx, hostID, templateID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "resending certificate template")
|
||||
}
|
||||
|
||||
certificate, err := svc.ds.GetCertificateTemplateById(ctx, templateID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "getting certificate details")
|
||||
}
|
||||
|
||||
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityTypeResentCertificate{
|
||||
HostID: host.ID,
|
||||
HostDisplayName: host.DisplayName(),
|
||||
CertificateTemplateID: certificate.ID,
|
||||
CertificateName: certificate.Name,
|
||||
}); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "creating activity")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -478,6 +478,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
|
|||
ue.DELETE("/api/_version_/fleet/hosts/{id:[0-9]+}/labels", removeLabelsFromHostEndpoint, removeLabelsFromHostRequest{})
|
||||
ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/software", getHostSoftwareEndpoint, getHostSoftwareRequest{})
|
||||
ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/certificates", listHostCertificatesEndpoint, listHostCertificatesRequest{})
|
||||
ue.POST("/api/_version_/fleet/hosts/{id:[0-9]+}/certificates/{template_id:[0-9]+}/resend", resendHostCertificateTemplateEndpoint, resendHostCertificateTemplateRequest{})
|
||||
ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/recovery_lock_password", getHostRecoveryLockPasswordEndpoint, getHostRecoveryLockPasswordRequest{})
|
||||
|
||||
ue.GET("/api/_version_/fleet/hosts/summary/mdm", getHostMDMSummary, getHostMDMSummaryRequest{})
|
||||
|
|
|
|||
|
|
@ -3815,9 +3815,9 @@ func (svc *Service) ListHostCertificates(ctx context.Context, hostID uint, opts
|
|||
return payload, meta, nil
|
||||
}
|
||||
|
||||
// //////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Get Host Recovery Lock Password
|
||||
// //////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type getHostRecoveryLockPasswordRequest struct {
|
||||
ID uint `url:"id"`
|
||||
|
|
|
|||
|
|
@ -1363,3 +1363,112 @@ func (s *integrationMDMTestSuite) TestCertificateTemplateAuthorizationForTeamUse
|
|||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/certificates?team_id=%d", otherTeamID), nil, http.StatusForbidden, &listCertificateTemplatesResponse{})
|
||||
})
|
||||
}
|
||||
|
||||
// TestCertificateTemplateResend tests the resend endpoint for Android certificate templates:
|
||||
// 1. After a certificate reaches 'verified' status, calling resend resets it to 'pending'
|
||||
// 2. The UUID changes after resend (signals the device to re-fetch)
|
||||
// 3. The reconcile cron picks up the pending template and re-delivers it
|
||||
func (s *integrationMDMTestSuite) TestCertificateTemplateResend() {
|
||||
t := s.T()
|
||||
ctx := t.Context()
|
||||
enterpriseID := s.enableAndroidMDM(t)
|
||||
|
||||
// Create a test team
|
||||
teamName := t.Name() + "-team"
|
||||
var createTeamResp teamResponse
|
||||
s.DoJSON("POST", "/api/latest/fleet/teams", createTeamRequest{
|
||||
TeamPayload: fleet.TeamPayload{
|
||||
Name: ptr.String(teamName),
|
||||
},
|
||||
}, http.StatusOK, &createTeamResp)
|
||||
teamID := createTeamResp.Team.ID
|
||||
|
||||
// Create a test certificate authority
|
||||
caID, _ := s.createTestCertificateAuthority(t, ctx)
|
||||
|
||||
// Create an enrolled Android host in the team
|
||||
host, orbitNodeKey := s.createEnrolledAndroidHost(t, ctx, enterpriseID, &teamID, "1")
|
||||
|
||||
// Create a certificate template for the team
|
||||
certTemplateName := strings.ReplaceAll(t.Name(), "/", "-") + "-CertTemplate"
|
||||
var createResp createCertificateTemplateResponse
|
||||
s.DoJSON("POST", "/api/latest/fleet/certificates", createCertificateTemplateRequest{
|
||||
Name: certTemplateName,
|
||||
TeamID: teamID,
|
||||
CertificateAuthorityId: caID,
|
||||
SubjectName: "CN=$FLEET_VAR_HOST_HARDWARE_SERIAL",
|
||||
}, http.StatusOK, &createResp)
|
||||
require.NotZero(t, createResp.ID)
|
||||
certTemplateID := createResp.ID
|
||||
|
||||
// Verify initial pending status
|
||||
s.verifyCertificateStatus(t, host, orbitNodeKey, certTemplateID, certTemplateName, caID,
|
||||
fleet.CertificateTemplatePending, "")
|
||||
|
||||
// Trigger reconciliation to deliver the certificate template
|
||||
s.awaitTriggerAndroidProfileSchedule(t)
|
||||
|
||||
// Verify status is now 'delivered'
|
||||
s.verifyCertificateStatus(t, host, orbitNodeKey, certTemplateID, certTemplateName, caID,
|
||||
fleet.CertificateTemplateDelivered, "")
|
||||
|
||||
// Simulate device reporting certificate enrollment as verified
|
||||
updateReq, err := json.Marshal(updateCertificateStatusRequest{
|
||||
Status: string(fleet.CertificateTemplateVerified),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
resp := s.DoRawWithHeaders("PUT", fmt.Sprintf("/api/fleetd/certificates/%d/status", certTemplateID), updateReq, http.StatusOK, map[string]string{
|
||||
"Authorization": fmt.Sprintf("Node key %s", orbitNodeKey),
|
||||
})
|
||||
_ = resp.Body.Close()
|
||||
|
||||
// Verify status is 'verified'
|
||||
s.verifyCertificateStatus(t, host, orbitNodeKey, certTemplateID, certTemplateName, caID,
|
||||
fleet.CertificateTemplateVerified, "")
|
||||
|
||||
// Record UUID before resend
|
||||
originalRecord, err := s.ds.GetHostCertificateTemplateRecord(ctx, host.UUID, certTemplateID)
|
||||
require.NoError(t, err)
|
||||
originalUUID := originalRecord.UUID
|
||||
|
||||
// Call the resend endpoint
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/certificates/%d/resend", host.ID, certTemplateID),
|
||||
nil, http.StatusOK, &struct{}{})
|
||||
|
||||
// Verify activity was created for the resend
|
||||
s.lastActivityOfTypeMatches(
|
||||
fleet.ActivityTypeResentCertificate{}.ActivityName(),
|
||||
fmt.Sprintf(
|
||||
`{"host_id": %d, "host_display_name": %q, "certificate_template_id": %d, "certificate_name": %q}`,
|
||||
host.ID,
|
||||
host.DisplayName(),
|
||||
certTemplateID,
|
||||
certTemplateName,
|
||||
),
|
||||
0)
|
||||
|
||||
// Verify status is reset to 'pending' and UUID changed
|
||||
updatedRecord, err := s.ds.GetHostCertificateTemplateRecord(ctx, host.UUID, certTemplateID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fleet.CertificateTemplatePending, updatedRecord.Status)
|
||||
require.NotEqual(t, originalUUID, updatedRecord.UUID, "UUID should change after resend")
|
||||
|
||||
// Verify the host API reflects pending status
|
||||
s.verifyCertificateStatus(t, host, orbitNodeKey, certTemplateID, certTemplateName, caID,
|
||||
fleet.CertificateTemplatePending, "")
|
||||
|
||||
// Trigger reconciliation again - should re-deliver with new UUID
|
||||
s.awaitTriggerAndroidProfileSchedule(t)
|
||||
|
||||
// Verify status is 'delivered' again after reconciliation
|
||||
s.verifyCertificateStatus(t, host, orbitNodeKey, certTemplateID, certTemplateName, caID,
|
||||
fleet.CertificateTemplateDelivered, "")
|
||||
|
||||
// Resend for a non-existent host should return 404
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/certificates/%d/resend", 99999, certTemplateID),
|
||||
nil, http.StatusNotFound, &struct{}{})
|
||||
|
||||
// Resend for a non-existent template returns 404
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/certificates/%d/resend", host.ID, 99999),
|
||||
nil, http.StatusNotFound, &struct{}{})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue