Adds activities for certificate templates (#36903)

**Related issue:** Resolves #36701

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)

## 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 activity tracking for Android certificate template edits and
deletions via GitOps.

* **Chores**
* Updated certificate template batch operations to track which teams
were affected by changes.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Konstantin Sykulev
2025-12-17 11:08:40 -06:00
committed by GitHub
parent c654c9ee87
commit c39a5b2e2d
12 changed files with 260 additions and 54 deletions
+20 -14
View File
@@ -3908,20 +3908,22 @@ func TestGitOpsAndroidCertificatesAdd(t *testing.T) {
// Track certificate templates that are created
var createdCertificates []fleet.CertificateTemplate
ds.BatchUpsertCertificateTemplatesFunc = func(ctx context.Context, certificates []*fleet.CertificateTemplate) error {
ds.BatchUpsertCertificateTemplatesFunc = func(ctx context.Context, certificates []*fleet.CertificateTemplate) ([]uint, error) {
createdCertificates = nil
createdMap := make([]uint, 0, len(certificates))
for _, cert := range certificates {
createdCertificates = append(createdCertificates, *cert)
createdMap = append(createdMap, cert.TeamID)
}
return nil
return createdMap, nil
}
ds.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, options fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
return []*fleet.CertificateTemplateResponseSummary{}, &fleet.PaginationMetadata{}, nil
}
ds.BatchDeleteCertificateTemplatesFunc = func(ctx context.Context, ids []uint) error {
return nil
ds.BatchDeleteCertificateTemplatesFunc = func(ctx context.Context, ids []uint) (bool, error) {
return false, nil
}
// Create team config
@@ -3995,12 +3997,14 @@ func TestGitOpsAndroidCertificatesChange(t *testing.T) {
var updatedCertificates []fleet.CertificateTemplate
var deletedCertificateIDs []uint
ds.BatchUpsertCertificateTemplatesFunc = func(ctx context.Context, certificates []*fleet.CertificateTemplate) error {
ds.BatchUpsertCertificateTemplatesFunc = func(ctx context.Context, certificates []*fleet.CertificateTemplate) ([]uint, error) {
updatedCertificates = nil
createdMap := make([]uint, 0, len(certificates))
for _, cert := range certificates {
updatedCertificates = append(updatedCertificates, *cert)
createdMap = append(createdMap, cert.TeamID)
}
return nil
return createdMap, nil
}
// Simulate existing certificates
@@ -4022,9 +4026,9 @@ func TestGitOpsAndroidCertificatesChange(t *testing.T) {
return existing, &fleet.PaginationMetadata{}, nil
}
ds.BatchDeleteCertificateTemplatesFunc = func(ctx context.Context, ids []uint) error {
ds.BatchDeleteCertificateTemplatesFunc = func(ctx context.Context, ids []uint) (bool, error) {
deletedCertificateIDs = append(deletedCertificateIDs, ids...)
return nil
return true, nil
}
// Create team config with modified subjectNames
@@ -4216,17 +4220,19 @@ func TestGitOpsAndroidCertificatesDeleteOne(t *testing.T) {
var deletedCertificateIDs []uint
var remainingCertificates []fleet.CertificateTemplate
ds.BatchDeleteCertificateTemplatesFunc = func(ctx context.Context, ids []uint) error {
ds.BatchDeleteCertificateTemplatesFunc = func(ctx context.Context, ids []uint) (bool, error) {
deletedCertificateIDs = ids
return nil
return true, nil
}
ds.BatchUpsertCertificateTemplatesFunc = func(ctx context.Context, certificates []*fleet.CertificateTemplate) error {
ds.BatchUpsertCertificateTemplatesFunc = func(ctx context.Context, certificates []*fleet.CertificateTemplate) ([]uint, error) {
remainingCertificates = nil
createdMap := make([]uint, 0, len(certificates))
for _, cert := range certificates {
remainingCertificates = append(remainingCertificates, *cert)
createdMap = append(createdMap, cert.TeamID)
}
return nil
return createdMap, nil
}
// Simulate existing certificates
@@ -4318,9 +4324,9 @@ func TestGitOpsAndroidCertificatesDeleteAll(t *testing.T) {
// Track what was deleted
var deletedCertificateIDs []uint
ds.BatchDeleteCertificateTemplatesFunc = func(ctx context.Context, ids []uint) error {
ds.BatchDeleteCertificateTemplatesFunc = func(ctx context.Context, ids []uint) (bool, error) {
deletedCertificateIDs = ids
return nil
return true, nil
}
// Simulate existing certificates
+17
View File
@@ -1352,6 +1352,23 @@ This activity contains the following fields:
}
```
## edited_android_certificate
Generated when a user adds or removes Android certificate templates of a team (or no team) via the fleetctl CLI.
This activity contains the following fields:
- "team_id": The ID of the team that the certificate templates apply to, `null` if they apply to devices that are not in a team.
- "team_name": The name of the team that the certificate templates apply to, `null` if they apply to devices that are not in a team.
#### Example
```json
{
"team_id": 123,
"team_name": "Workstations"
}
```
## resent_configuration_profile
Generated when a user resends a configuration profile to a host.
+3
View File
@@ -79,6 +79,7 @@ export enum ActivityType {
CreatedAndroidProfile = "created_android_profile",
DeletedAndroidProfile = "deleted_android_profile",
EditedAndroidProfile = "edited_android_profile",
EditedAndroidCertificate = "edited_android_certificate",
// Note: Both "enabled_disk_encryption" and "enabled_macos_disk_encryption" display the same
// message. The latter is deprecated in the API but it is retained here for backwards compatibility.
EnabledDiskEncryption = "enabled_disk_encryption",
@@ -416,6 +417,8 @@ export const ACTIVITY_TYPE_TO_FILTER_LABEL: Record<ActivityType, string> = {
"Deleted configuration profile: Android",
[ActivityType.EditedAndroidProfile]:
"GitOps: edited configuration profiles: Android",
[ActivityType.EditedAndroidCertificate]:
"GitOps: edited certificate templates: Android",
[ActivityType.AddedConditionalAccessOkta]: "Added conditional access: Okta",
[ActivityType.DeletedConditionalAccessOkta]:
"Deleted conditional access: Okta",
@@ -604,6 +604,20 @@ const TAGGED_TEMPLATES = {
</>
);
},
editedAndroidCertificate: (activity: IActivity, isPremiumTier: boolean) => {
return (
<>
{" "}
edited certificate templates for{" "}
{getProfileMessageSuffix(
isPremiumTier,
"android",
activity.details?.team_name
)}{" "}
via fleetctl.
</>
);
},
addedCertificateAuthority: (name = "") => {
return name ? (
<>
@@ -1792,6 +1806,9 @@ const getDetail = (activity: IActivity, isPremiumTier: boolean) => {
case ActivityType.EditedAndroidProfile: {
return TAGGED_TEMPLATES.editedAndroidProfile(activity, isPremiumTier);
}
case ActivityType.EditedAndroidCertificate: {
return TAGGED_TEMPLATES.editedAndroidCertificate(activity, isPremiumTier);
}
case ActivityType.AddedNdesScepProxy: {
return TAGGED_TEMPLATES.addedCertificateAuthority("NDES");
}
+23 -20
View File
@@ -197,45 +197,46 @@ func (ds *Datastore) DeleteCertificateTemplate(ctx context.Context, id uint) err
return nil
}
func (ds *Datastore) BatchUpsertCertificateTemplates(ctx context.Context, certificateTemplates []*fleet.CertificateTemplate) error {
func (ds *Datastore) BatchUpsertCertificateTemplates(ctx context.Context, certificateTemplates []*fleet.CertificateTemplate) ([]uint, error) {
if len(certificateTemplates) == 0 {
return nil
return nil, nil
}
const argsCountInsertCertificate = 4
const sqlInsertCertificate = `
INSERT INTO certificate_templates (
name,
team_id,
certificate_authority_id,
subject_name
) VALUES %s
) VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
name = VALUES(name),
team_id = VALUES(team_id)
`
var placeholders strings.Builder
args := make([]interface{}, 0, len(certificateTemplates)*argsCountInsertCertificate)
teamsModifiedSet := make(map[uint]struct{})
for _, cert := range certificateTemplates {
args = append(args, cert.Name, cert.TeamID, cert.CertificateAuthorityID, cert.SubjectName)
placeholders.WriteString("(?,?,?,?),")
result, err := ds.writer(ctx).ExecContext(ctx, sqlInsertCertificate, cert.Name, cert.TeamID, cert.CertificateAuthorityID, cert.SubjectName)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "upserting certificate_template")
}
if insertOnDuplicateDidInsertOrUpdate(result) {
teamsModifiedSet[cert.TeamID] = struct{}{}
}
}
stmt := fmt.Sprintf(sqlInsertCertificate, strings.TrimSuffix(placeholders.String(), ","))
if _, err := ds.writer(ctx).ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "upserting certificate_templates")
teamsModified := make([]uint, 0, len(teamsModifiedSet))
for teamID := range teamsModifiedSet {
teamsModified = append(teamsModified, teamID)
}
return nil
return teamsModified, nil
}
func (ds *Datastore) BatchDeleteCertificateTemplates(ctx context.Context, certificateTemplateIDs []uint) error {
func (ds *Datastore) BatchDeleteCertificateTemplates(ctx context.Context, certificateTemplateIDs []uint) (bool, error) {
if len(certificateTemplateIDs) == 0 {
return nil
return false, nil
}
const sqlDeleteCertificateTemplates = `
@@ -252,11 +253,13 @@ func (ds *Datastore) BatchDeleteCertificateTemplates(ctx context.Context, certif
stmt := fmt.Sprintf(sqlDeleteCertificateTemplates, strings.TrimSuffix(placeholders.String(), ","))
if _, err := ds.writer(ctx).ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "deleting certificate_templates")
result, err := ds.writer(ctx).ExecContext(ctx, stmt, args...)
if err != nil {
return false, ctxerr.Wrap(ctx, err, "deleting certificate_templates")
}
return nil
rowsAffected, _ := result.RowsAffected()
return rowsAffected > 0, nil
}
func (ds *Datastore) GetHostCertificateTemplates(ctx context.Context, hostUUID string) ([]fleet.HostCertificateTemplate, error) {
@@ -629,7 +629,7 @@ func testBatchUpsertCertificates(t *testing.T, ds *Datastore) {
var teamID, caID uint
var certificates []*fleet.CertificateTemplate
var err error
testCases := []struct {
name string
before func(ds *Datastore)
@@ -640,8 +640,10 @@ func testBatchUpsertCertificates(t *testing.T, ds *Datastore) {
func(ds *Datastore) {},
func(t *testing.T, ds *Datastore) {
// Test with empty slice
err = ds.BatchUpsertCertificateTemplates(ctx, []*fleet.CertificateTemplate{})
teamsModified, err := ds.BatchUpsertCertificateTemplates(ctx, []*fleet.CertificateTemplate{})
require.NoError(t, err)
require.Empty(t, teamsModified)
},
},
{
@@ -678,13 +680,16 @@ func testBatchUpsertCertificates(t *testing.T, ds *Datastore) {
},
}
err = ds.BatchUpsertCertificateTemplates(ctx, certificates)
teamsModified, err := ds.BatchUpsertCertificateTemplates(ctx, certificates)
require.NoError(t, err)
var count int
err = ds.writer(ctx).GetContext(ctx, &count, "SELECT COUNT(*) FROM certificate_templates")
require.NoError(t, err)
require.Equal(t, 2, count)
require.Len(t, teamsModified, 1)
require.Equal(t, teamsModified[0], teamID)
},
},
{
@@ -724,18 +729,20 @@ func testBatchUpsertCertificates(t *testing.T, ds *Datastore) {
},
func(t *testing.T, ds *Datastore) {
var count int
err = ds.writer(ctx).GetContext(ctx, &count, "SELECT COUNT(*) FROM certificate_templates")
err := ds.writer(ctx).GetContext(ctx, &count, "SELECT COUNT(*) FROM certificate_templates")
require.NoError(t, err)
require.Equal(t, 1, count)
certificates[0].SubjectName = "Updated Subject"
err = ds.BatchUpsertCertificateTemplates(ctx, certificates)
teamsModified, err := ds.BatchUpsertCertificateTemplates(ctx, certificates)
require.NoError(t, err)
err = ds.writer(ctx).GetContext(ctx, &count, "SELECT COUNT(*) FROM certificate_templates")
require.NoError(t, err)
require.Equal(t, 1, count)
require.Len(t, teamsModified, 0)
var subjectName string
err = ds.writer(ctx).GetContext(ctx, &subjectName, "SELECT subject_name FROM certificate_templates WHERE name = ?", "Cert1")
require.NoError(t, err)
@@ -760,7 +767,7 @@ func testBatchDeleteCertificateTemplates(t *testing.T, ds *Datastore) {
var teamID, caID uint
var certificateTemplateIDs []uint
var err error
testCases := []struct {
name string
before func(ds *Datastore)
@@ -771,8 +778,10 @@ func testBatchDeleteCertificateTemplates(t *testing.T, ds *Datastore) {
func(ds *Datastore) {},
func(t *testing.T, ds *Datastore) {
// Test with empty slice
err = ds.BatchDeleteCertificateTemplates(ctx, []uint{})
generateActivity, err := ds.BatchDeleteCertificateTemplates(ctx, []uint{})
require.NoError(t, err)
require.False(t, generateActivity)
},
},
{
@@ -832,13 +841,15 @@ func testBatchDeleteCertificateTemplates(t *testing.T, ds *Datastore) {
},
func(t *testing.T, ds *Datastore) {
var count int
err = ds.writer(ctx).GetContext(ctx, &count, "SELECT COUNT(*) FROM certificate_templates")
err := ds.writer(ctx).GetContext(ctx, &count, "SELECT COUNT(*) FROM certificate_templates")
require.NoError(t, err)
require.Equal(t, 2, count)
err = ds.BatchDeleteCertificateTemplates(ctx, certificateTemplateIDs)
generateActivity, err := ds.BatchDeleteCertificateTemplates(ctx, certificateTemplateIDs)
require.NoError(t, err)
require.True(t, generateActivity)
err = ds.writer(ctx).GetContext(ctx, &count, "SELECT COUNT(*) FROM certificate_templates")
require.NoError(t, err)
require.Equal(t, 0, count)
+20
View File
@@ -189,6 +189,7 @@ var ActivityDetailsList = []ActivityDetails{
ActivityTypeCreatedAndroidProfile{},
ActivityTypeDeletedAndroidProfile{},
ActivityTypeEditedAndroidProfile{},
ActivityTypeEditedAndroidCertificate{},
ActivityTypeResentConfigurationProfile{},
ActivityTypeResentConfigurationProfileBatch{},
@@ -3178,6 +3179,25 @@ func (a ActivityTypeEditedAndroidProfile) Documentation() (activity, details, de
}`
}
type ActivityTypeEditedAndroidCertificate struct {
TeamID *uint `json:"team_id"`
TeamName *string `json:"team_name"`
}
func (a ActivityTypeEditedAndroidCertificate) ActivityName() string {
return "edited_android_certificate"
}
func (a ActivityTypeEditedAndroidCertificate) Documentation() (activity, details, detailsExample string) {
return `Generated when a user adds or removes Android certificate templates of a team (or no team) via the fleetctl CLI.`,
`This activity contains the following fields:
- "team_id": The ID of the team that the certificate templates apply to, ` + "`null`" + ` if they apply to devices that are not in a team.
- "team_name": The name of the team that the certificate templates apply to, ` + "`null`" + ` if they apply to devices that are not in a team.`, `{
"team_id": 123,
"team_name": "Workstations"
}`
}
type ActivityTypeEditedHostIdpData struct {
HostID uint `json:"host_id"`
HostDisplayName string `json:"host_display_name"`
+4 -2
View File
@@ -2542,9 +2542,11 @@ type Datastore interface {
UpsertCertificateStatus(ctx context.Context, hostUUID string, certificateTemplateID uint, status MDMDeliveryStatus, detail *string) error
// BatchUpsertCertificateTemplates upserts a batch of certificates.
BatchUpsertCertificateTemplates(ctx context.Context, certificates []*CertificateTemplate) error
// Returns a map of team IDs that had certificates inserted or updated.
BatchUpsertCertificateTemplates(ctx context.Context, certificates []*CertificateTemplate) ([]uint, error)
// BatchDeleteCertificateTemplates deletes a batch of certificates.
BatchDeleteCertificateTemplates(ctx context.Context, certificateTemplateIDs []uint) error
// Returns true if any rows were deleted.
BatchDeleteCertificateTemplates(ctx context.Context, certificateTemplateIDs []uint) (bool, error)
// CreateCertificateTemplate creates a new certificate template.
CreateCertificateTemplate(ctx context.Context, certificateTemplate *CertificateTemplate) (*CertificateTemplateResponse, error)
// DeleteCertificateTemplate deletes a certificate template by its ID.
+4 -4
View File
@@ -1657,9 +1657,9 @@ type BatchApplyCertificateAuthoritiesFunc func(ctx context.Context, ops fleet.Ce
type UpsertCertificateStatusFunc func(ctx context.Context, hostUUID string, certificateTemplateID uint, status fleet.MDMDeliveryStatus, detail *string) error
type BatchUpsertCertificateTemplatesFunc func(ctx context.Context, certificates []*fleet.CertificateTemplate) error
type BatchUpsertCertificateTemplatesFunc func(ctx context.Context, certificates []*fleet.CertificateTemplate) ([]uint, error)
type BatchDeleteCertificateTemplatesFunc func(ctx context.Context, certificateTemplateIDs []uint) error
type BatchDeleteCertificateTemplatesFunc func(ctx context.Context, certificateTemplateIDs []uint) (bool, error)
type CreateCertificateTemplateFunc func(ctx context.Context, certificateTemplate *fleet.CertificateTemplate) (*fleet.CertificateTemplateResponse, error)
@@ -9936,14 +9936,14 @@ func (s *DataStore) UpsertCertificateStatus(ctx context.Context, hostUUID string
return s.UpsertCertificateStatusFunc(ctx, hostUUID, certificateTemplateID, status, detail)
}
func (s *DataStore) BatchUpsertCertificateTemplates(ctx context.Context, certificates []*fleet.CertificateTemplate) error {
func (s *DataStore) BatchUpsertCertificateTemplates(ctx context.Context, certificates []*fleet.CertificateTemplate) ([]uint, error) {
s.mu.Lock()
s.BatchUpsertCertificateTemplatesFuncInvoked = true
s.mu.Unlock()
return s.BatchUpsertCertificateTemplatesFunc(ctx, certificates)
}
func (s *DataStore) BatchDeleteCertificateTemplates(ctx context.Context, certificateTemplateIDs []uint) error {
func (s *DataStore) BatchDeleteCertificateTemplates(ctx context.Context, certificateTemplateIDs []uint) (bool, error) {
s.mu.Lock()
s.BatchDeleteCertificateTemplatesFuncInvoked = true
s.mu.Unlock()
+19 -2
View File
@@ -93,6 +93,21 @@ func TestApplyCertificateTemplateSpecs(t *testing.T) {
ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{}, nil
}
ds.TeamLiteFunc = func(ctx context.Context, id uint) (*fleet.TeamLite, error) {
return &fleet.TeamLite{
ID: id,
Name: "Test Team",
}, nil
}
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
return nil
}
// Set up certificate authority mocks
certAuthorities := []*fleet.CertificateAuthority{
{
@@ -139,12 +154,14 @@ func TestApplyCertificateTemplateSpecs(t *testing.T) {
var createdCertificates []fleet.CertificateTemplate
var nextTemplateID uint = 100
ds.BatchUpsertCertificateTemplatesFunc = func(ctx context.Context, certificates []*fleet.CertificateTemplate) error {
ds.BatchUpsertCertificateTemplatesFunc = func(ctx context.Context, certificates []*fleet.CertificateTemplate) ([]uint, error) {
createdCertificates = nil
createdMap := make([]uint, 0, len(certificates))
for _, cert := range certificates {
createdCertificates = append(createdCertificates, *cert)
createdMap = append(createdMap, cert.TeamID)
}
return nil
return createdMap, nil
}
ds.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, opts fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
+55 -2
View File
@@ -408,7 +408,8 @@ func (svc *Service) ApplyCertificateTemplateSpecs(ctx context.Context, specs []*
certificates = append(certificates, cert)
}
if err := svc.ds.BatchUpsertCertificateTemplates(ctx, certificates); err != nil {
teamsModified, err := svc.ds.BatchUpsertCertificateTemplates(ctx, certificates)
if err != nil {
return err
}
@@ -425,6 +426,28 @@ func (svc *Service) ApplyCertificateTemplateSpecs(ctx context.Context, specs []*
}
}
// Only create activity for teams that actually had certificates affected
for _, teamID := range teamsModified {
var tmID *uint
var tmName *string
if teamID != 0 {
team, err := svc.ds.TeamLite(ctx, teamID)
if err != nil {
return ctxerr.Wrap(ctx, err, "getting team for activity")
}
tmID = &team.ID
tmName = &team.Name
}
if err := svc.NewActivity(
ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeEditedAndroidCertificate{
TeamID: tmID,
TeamName: tmName,
}); err != nil {
return ctxerr.Wrap(ctx, err, "logging activity for edited android certificate")
}
}
return nil
}
@@ -452,7 +475,37 @@ func (svc *Service) DeleteCertificateTemplateSpecs(ctx context.Context, certific
if err := svc.authz.Authorize(ctx, &fleet.CertificateTemplate{TeamID: teamID}, fleet.ActionWrite); err != nil {
return err
}
return svc.ds.BatchDeleteCertificateTemplates(ctx, certificateTemplateIDs)
deletedRows, err := svc.ds.BatchDeleteCertificateTemplates(ctx, certificateTemplateIDs)
if err != nil {
return err
}
if !deletedRows {
return nil
}
// Only create activity if rows were actually deleted
var tmID *uint
var tmName *string
if teamID != 0 {
team, err := svc.ds.TeamLite(ctx, teamID)
if err != nil {
return ctxerr.Wrap(ctx, err, "getting team for activity")
}
tmID = &team.ID
tmName = &team.Name
}
if err := svc.NewActivity(
ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeEditedAndroidCertificate{
TeamID: tmID,
TeamName: tmName,
}); err != nil {
return ctxerr.Wrap(ctx, err, "logging activity for edited android certificate")
}
return nil
}
type updateCertificateStatusRequest struct {
+58 -1
View File
@@ -8001,6 +8001,9 @@ func (s *integrationTestSuite) TestCertificatesSpecs() {
},
}, http.StatusNotFound, &applyResp)
activitiesBeforeInsert, _, err := s.ds.ListActivities(ctx, fleet.ListActivitiesOptions{})
require.NoError(t, err)
// valid templates - test team name (not team ID)
s.DoJSON("POST", "/api/latest/fleet/spec/certificates", applyCertificateTemplateSpecsRequest{
Specs: []*fleet.CertificateRequestSpec{
@@ -8019,12 +8022,45 @@ func (s *integrationTestSuite) TestCertificatesSpecs() {
},
}, http.StatusOK, &applyResp)
// Only one activity per team
activitiesAfterInsert, _, err := s.ds.ListActivities(ctx, fleet.ListActivitiesOptions{})
require.NoError(t, err)
require.Len(t, activitiesAfterInsert, len(activitiesBeforeInsert)+1, "expected exactly one new activity for the team")
s.lastActivityMatches(
fleet.ActivityTypeEditedAndroidCertificate{}.ActivityName(),
fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, team.ID, team.Name),
0,
)
// list specs
var listCertifcatesResp listCertificateTemplatesResponse
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/certificates?team_id=%d", team.ID), nil, http.StatusOK, &listCertifcatesResp)
require.Len(t, listCertifcatesResp.Certificates, 2)
assert.ElementsMatch(t, []string{"Template 1", "Template 2"}, []string{listCertifcatesResp.Certificates[0].Name, listCertifcatesResp.Certificates[1].Name})
lastActivityID := s.lastActivityMatches("", "", 0)
s.DoJSON("POST", "/api/latest/fleet/spec/certificates", applyCertificateTemplateSpecsRequest{
Specs: []*fleet.CertificateRequestSpec{
{
Name: "Template 1",
Team: team.Name,
CertificateAuthorityId: ca.ID,
SubjectName: "CN=$FLEET_VAR_HOST_END_USER_IDP_USERNAME/OU=$FLEET_VAR_HOST_UUID/ST=$FLEET_VAR_HOST_HARDWARE_SERIAL",
},
{
Name: "Template 2",
Team: team.Name,
CertificateAuthorityId: ca.ID,
SubjectName: "CN=$FLEET_VAR_HOST_END_USER_IDP_USERNAME/OU=$FLEET_VAR_HOST_UUID",
},
},
}, http.StatusOK, &applyResp)
// No new activities created
currentActivityID := s.lastActivityMatches("", "", 0)
assert.Equal(t, lastActivityID, currentActivityID, "no new activity should be created when re-applying same certificates")
// Create a host to get certificate get by id endpoint
host, err := s.ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
@@ -8105,13 +8141,24 @@ func (s *integrationTestSuite) TestCertificatesSpecs() {
// batch delete certificate templates
var delBatchResp deleteCertificateTemplateSpecsResponse
s.DoJSON("DELETE", "/api/latest/fleet/spec/certificates", map[string]interface{}{
"ids": []uint{listCertifcatesResp.Certificates[0].ID, listCertifcatesResp.Certificates[1].ID},
"ids": []uint{listCertifcatesResp.Certificates[0].ID, listCertifcatesResp.Certificates[1].ID},
"team_id": team.ID,
}, http.StatusOK, &delBatchResp)
// Verify activity was created for deleting certificates
s.lastActivityMatches(
fleet.ActivityTypeEditedAndroidCertificate{}.ActivityName(),
fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, team.ID, team.Name),
0,
)
// list specs
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/certificates?team_id=%d", team.ID), nil, http.StatusOK, &listCertifcatesResp)
require.Len(t, listCertifcatesResp.Certificates, 0)
activitiesBeforeNoTeam, _, err := s.ds.ListActivities(ctx, fleet.ListActivitiesOptions{})
require.NoError(t, err)
// certificate templates for "No team"
s.DoJSON("POST", "/api/latest/fleet/spec/certificates", applyCertificateTemplateSpecsRequest{
Specs: []*fleet.CertificateRequestSpec{
@@ -8136,6 +8183,16 @@ func (s *integrationTestSuite) TestCertificatesSpecs() {
},
}, http.StatusOK, &applyResp)
// Only one activity was created for "No team"
activitiesAfterNoTeam, _, err := s.ds.ListActivities(ctx, fleet.ListActivitiesOptions{})
require.NoError(t, err)
require.Len(t, activitiesAfterNoTeam, len(activitiesBeforeNoTeam)+1, "expected exactly one new activity for no team")
s.lastActivityMatches(
fleet.ActivityTypeEditedAndroidCertificate{}.ActivityName(),
`{"team_id": null, "team_name": null}`,
0,
)
// list specs for "no team" (team_id 0)
var noTeamCertificatesResp listCertificateTemplatesResponse
s.DoJSON("GET", "/api/latest/fleet/certificates", nil, http.StatusOK, &noTeamCertificatesResp)