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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
+17
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user