From ee1eaf21cf78f57d795185f461ba8ea0a7daa571 Mon Sep 17 00:00:00 2001 From: Scott Gress Date: Tue, 2 Dec 2025 10:25:13 -0600 Subject: [PATCH] Support generate-gitops for android certificates (#36457) **Related issue:** Resolves #36436 # Details Implements outputting `android_settings.certificates` when running `fleetctl generate-gitops`. # Checklist for submitter ## Testing - [X] Added/updated automated tests - [X] QA'd all new/changed functionality manually * Added a certificate authority via the UI, then added some certificates via GitOps, and verified that the certificates were outputted correctly when using `fleetctl generate-gitops`. --- cmd/fleetctl/fleetctl/generate_gitops.go | 43 ++++++++++++++++++- cmd/fleetctl/fleetctl/generate_gitops_test.go | 29 +++++++++++++ .../generateGitops/expectedTeamControls.yaml | 7 ++- .../testdata/generateGitops/teamConfig.json | 9 ++++ .../teams/team-a-thumbsup.yml | 5 +++ server/service/client_android_certificates.go | 18 ++++++++ 6 files changed, 108 insertions(+), 3 deletions(-) diff --git a/cmd/fleetctl/fleetctl/generate_gitops.go b/cmd/fleetctl/fleetctl/generate_gitops.go index 83ba40c9fa..abbc31fc05 100644 --- a/cmd/fleetctl/fleetctl/generate_gitops.go +++ b/cmd/fleetctl/fleetctl/generate_gitops.go @@ -83,6 +83,8 @@ type generateGitopsClient interface { GetSetupExperienceScript(teamID uint) (*fleet.Script, error) GetAppleMDMEnrollmentProfile(teamID uint) (*fleet.MDMAppleSetupAssistant, error) GetCertificateAuthoritiesSpec(includeSecrets bool) (*fleet.GroupedCertificateAuthorities, error) + GetCertificateTemplates(teamID string) ([]*fleet.CertificateTemplateResponseSummary, error) + GetCertificateTemplate(certificateID uint, hostUUID *string) (*fleet.CertificateTemplateResponseFull, error) } // Given a struct type and a field name, return the JSON field name. @@ -1064,9 +1066,46 @@ func (cmd *GenerateGitopsCommand) generateControls(teamId *uint, teamName string } } - if cmd.AppConfig.License.IsPremium() { - mdmT := reflect.TypeOf(fleet.TeamMDM{}) + // Get any Android certificate templates. + var certSummaries []*fleet.CertificateTemplateResponseSummary + var err error + if teamId == nil { + certSummaries, err = cmd.Client.GetCertificateTemplates("") + } else { + certSummaries, err = cmd.Client.GetCertificateTemplates(strconv.FormatUint(uint64(*teamId), 10)) + } + if err != nil { + fmt.Fprintf(cmd.CLI.App.ErrWriter, "Error getting certificate templates: %s\n", err) + return nil, err + } + mdmT := reflect.TypeOf(fleet.TeamMDM{}) + + if len(certSummaries) > 0 { + androidSettingsType := reflect.TypeOf(fleet.AndroidSettings{}) + certType := reflect.TypeOf(fleet.CertificateTemplateResponseFull{}) + fullCerts := make([]map[string]interface{}, 0, len(certSummaries)) + for _, certSummary := range certSummaries { + certFull, err := cmd.Client.GetCertificateTemplate(certSummary.ID, nil) + if err != nil { + fmt.Fprintf(cmd.CLI.App.ErrWriter, "Error getting certificate template details for ID %d: %s\n", certSummary.ID, err) + return nil, err + } + fullCerts = append(fullCerts, map[string]interface{}{ + jsonFieldName(certType, "Name"): certFull.Name, + jsonFieldName(certType, "CertificateAuthorityName"): certFull.CertificateAuthorityName, + jsonFieldName(certType, "SubjectName"): certFull.SubjectName, + }) + } + androidSettings, ok := result[jsonFieldName(mdmT, "AndroidSettings")].(map[string]interface{}) + if !ok { + androidSettings = map[string]interface{}{} + } + androidSettings[jsonFieldName(androidSettingsType, "Certificates")] = fullCerts + result[jsonFieldName(mdmT, "AndroidSettings")] = androidSettings + } + + if cmd.AppConfig.License.IsPremium() { if teamMdm != nil { result[jsonFieldName(mdmT, "EnableDiskEncryption")] = teamMdm.EnableDiskEncryption result[jsonFieldName(mdmT, "RequireBitLockerPIN")] = teamMdm.RequireBitLockerPIN diff --git a/cmd/fleetctl/fleetctl/generate_gitops_test.go b/cmd/fleetctl/fleetctl/generate_gitops_test.go index ca977e4cee..707334fab4 100644 --- a/cmd/fleetctl/fleetctl/generate_gitops_test.go +++ b/cmd/fleetctl/fleetctl/generate_gitops_test.go @@ -581,6 +581,35 @@ func (MockClient) GetCertificateAuthoritiesSpec(includeSecrets bool) (*fleet.Gro return &res, nil } +func (MockClient) GetCertificateTemplates(teamID string) ([]*fleet.CertificateTemplateResponseSummary, error) { + var res []*fleet.CertificateTemplateResponseSummary + if teamID == "1" { + res = []*fleet.CertificateTemplateResponseSummary{ + { + ID: 1, + CertificateAuthorityName: "DIGIDOO", + Name: "my_certypoo", + }, + } + } + return res, nil +} + +func (MockClient) GetCertificateTemplate(certificateID uint, hostUUID *string) (*fleet.CertificateTemplateResponseFull, error) { + var res *fleet.CertificateTemplateResponseFull + if certificateID == 1 { + res = &fleet.CertificateTemplateResponseFull{ + CertificateTemplateResponseSummary: fleet.CertificateTemplateResponseSummary{ + ID: 1, + CertificateAuthorityName: "DIGIDOO", + Name: "my_certypoo", + }, + SubjectName: "CN=OU=$FLEET_VAR_HOST_UUID/ST=$FLEET_VAR_HOST_HARDWARE_SERIAL", + } + } + return res, nil +} + func maskSecret(value string, shouldShowSecret bool) string { if shouldShowSecret { return value diff --git a/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedTeamControls.yaml b/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedTeamControls.yaml index c6423e555a..2004196db0 100644 --- a/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedTeamControls.yaml +++ b/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedTeamControls.yaml @@ -2,4 +2,9 @@ macos_settings: custom_settings: - path: ../lib/some_team/profiles/team-macos-mobileconfig-profile.mobileconfig scripts: -- path: ../lib/some_team/scripts/Script B.ps1 \ No newline at end of file +- path: ../lib/some_team/scripts/Script B.ps1 +android_settings: + certificates: + - certificate_authority_name: DIGIDOO + name: my_certypoo + subject_name: CN=OU=$FLEET_VAR_HOST_UUID/ST=$FLEET_VAR_HOST_HARDWARE_SERIAL \ No newline at end of file diff --git a/cmd/fleetctl/fleetctl/testdata/generateGitops/teamConfig.json b/cmd/fleetctl/fleetctl/testdata/generateGitops/teamConfig.json index 4fc34d9884..89736aa27b 100644 --- a/cmd/fleetctl/fleetctl/testdata/generateGitops/teamConfig.json +++ b/cmd/fleetctl/fleetctl/testdata/generateGitops/teamConfig.json @@ -134,6 +134,15 @@ }, "windows_settings": { "custom_settings": null + }, + "android_settings": { + "certificates": [ + { + "certificate_authority_name": "DIGIDOO", + "name": "my_certypoo", + "subject_name": "CN=OU=$FLEET_VAR_HOST_UUID/ST=$FLEET_VAR_HOST_HARDWARE_SERIAL" + } + ] } }, "scripts": [ diff --git a/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/teams/team-a-thumbsup.yml b/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/teams/team-a-thumbsup.yml index a513aca940..86d7c53aab 100644 --- a/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/teams/team-a-thumbsup.yml +++ b/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/teams/team-a-thumbsup.yml @@ -18,6 +18,11 @@ agent_options: osqueryd: edge controls: android_enabled_and_configured: true + android_settings: + certificates: + - certificate_authority_name: DIGIDOO + name: my_certypoo + subject_name: CN=OU=$FLEET_VAR_HOST_UUID/ST=$FLEET_VAR_HOST_HARDWARE_SERIAL enable_disk_encryption: true ios_updates: deadline: "2021-12-31" diff --git a/server/service/client_android_certificates.go b/server/service/client_android_certificates.go index 624ce8ca70..fbcc45c821 100644 --- a/server/service/client_android_certificates.go +++ b/server/service/client_android_certificates.go @@ -1,6 +1,8 @@ package service import ( + "strconv" + "github.com/fleetdm/fleet/v4/server/fleet" ) @@ -16,6 +18,22 @@ func (c *Client) GetCertificateTemplates(teamID string) ([]*fleet.CertificateTem return responseBody.Certificates, nil } +// GetCertificateTemplate retrieves the full details of a single certificate, optionally +// replacing vars in the subject with values from a specified host. +func (c *Client) GetCertificateTemplate(certificateID uint, hostUUID *string) (*fleet.CertificateTemplateResponseFull, error) { + verb, path := "GET", "/api/latest/fleet/certificates/"+strconv.FormatUint(uint64(certificateID), 10) + var responseBody getCertificateTemplateResponse + var query string + if hostUUID != nil { + query = "host_uuid=" + *hostUUID + } + err := c.authenticatedRequestWithQuery(nil, verb, path, &responseBody, query) + if err != nil { + return nil, err + } + return responseBody.Certificate, nil +} + // ApplyCertificateSpecs sends a list of certificate specs to the fleet instance to be added/updated. func (c *Client) ApplyCertificateSpecs(specs []*fleet.CertificateRequestSpec) error { req := applyCertificateTemplateSpecsRequest{Specs: specs}