validating certificate template subjectname fleet vars (#36377)

**Related issue:** Resolves #36289

## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually
This commit is contained in:
Konstantin Sykulev 2025-12-01 19:10:55 -06:00 committed by GitHub
parent e8c3e26d60
commit f0494961d5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 102 additions and 38 deletions

View file

@ -134,6 +134,7 @@ var teamRefs = []string{
"mdm_android_configuration_profiles",
"software_title_icons",
"software_title_display_names",
"certificate_templates",
}
func (ds *Datastore) DeleteTeam(ctx context.Context, tid uint) error {
@ -145,6 +146,15 @@ func (ds *Datastore) DeleteTeam(ctx context.Context, tid uint) error {
return ctxerr.Wrapf(ctx, err, "deleting policies for team %d", tid)
}
// Delete related records from teamRefs tables before deleting the team itself
// to avoid foreign key constraint violations
for _, table := range teamRefs {
_, err = tx.ExecContext(ctx, fmt.Sprintf(`DELETE FROM %s WHERE team_id=?`, table), tid)
if err != nil {
return ctxerr.Wrapf(ctx, err, "deleting %s for team %d", table, tid)
}
}
_, err = tx.ExecContext(ctx, `DELETE FROM teams WHERE id = ?`, tid)
if err != nil {
return ctxerr.Wrapf(ctx, err, "delete team %d", tid)
@ -155,13 +165,6 @@ func (ds *Datastore) DeleteTeam(ctx context.Context, tid uint) error {
return ctxerr.Wrapf(ctx, err, "deleting pack_targets for team %d", tid)
}
for _, table := range teamRefs {
_, err = tx.ExecContext(ctx, fmt.Sprintf(`DELETE FROM %s WHERE team_id=?`, table), tid)
if err != nil {
return ctxerr.Wrapf(ctx, err, "deleting %s for team %d", table, tid)
}
}
return nil
})
}

View file

@ -0,0 +1,62 @@
package service
import (
"context"
"fmt"
"slices"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/variables"
)
// Fleet variables supported in certificate template subject names.
var fleetVarsSupportedInCertificateTemplates = []fleet.FleetVarName{
fleet.FleetVarHostUUID,
fleet.FleetVarHostHardwareSerial,
fleet.FleetVarHostEndUserIDPUsername,
}
func validateCertificateTemplateFleetVariables(subjectName string) error {
fleetVars := variables.Find(subjectName)
if len(fleetVars) == 0 {
return nil
}
for _, fleetVar := range fleetVars {
if !slices.Contains(fleetVarsSupportedInCertificateTemplates, fleet.FleetVarName(fleetVar)) {
return fmt.Errorf("Fleet variable $FLEET_VAR_%s is not supported in certificate templates", fleetVar)
}
}
return nil
}
// replaceCertificateVariables replaces FLEET_VAR_* variables in the subject name with actual host values
func (svc *Service) replaceCertificateVariables(ctx context.Context, subjectName string, host *fleet.Host) (string, error) {
fleetVars := variables.Find(subjectName)
if len(fleetVars) == 0 {
return subjectName, nil
}
result := subjectName
for _, fleetVar := range fleetVars {
switch fleetVar {
case string(fleet.FleetVarHostUUID):
result = fleet.FleetVarHostUUIDRegexp.ReplaceAllString(result, host.UUID)
case string(fleet.FleetVarHostHardwareSerial):
result = fleet.FleetVarHostHardwareSerialRegexp.ReplaceAllString(result, host.HardwareSerial)
case string(fleet.FleetVarHostEndUserIDPUsername):
users, err := fleet.GetEndUsers(ctx, svc.ds, host.ID)
if err != nil {
return "", ctxerr.Wrapf(ctx, err, "getting host end users for variable %s", fleetVar)
}
if len(users) == 0 || users[0].IdpUserName == "" {
return "", ctxerr.Errorf(ctx, "host %s does not have an IDP username for variable %s", host.UUID, fleetVar)
}
result = fleet.FleetVarHostEndUserIDPUsernameRegexp.ReplaceAllString(result, users[0].IdpUserName)
}
}
return result, nil
}

View file

@ -3,12 +3,12 @@ package service
import (
"context"
"errors"
"fmt"
"strconv"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
hostctx "github.com/fleetdm/fleet/v4/server/contexts/host"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/variables"
)
type createCertificateTemplateRequest struct {
@ -46,6 +46,10 @@ func (svc *Service) CreateCertificateTemplate(ctx context.Context, name string,
return nil, err
}
if err := validateCertificateTemplateFleetVariables(subjectName); err != nil {
return nil, &fleet.BadRequestError{Message: err.Error()}
}
certTemplate := &fleet.CertificateTemplate{
Name: name,
TeamID: teamID,
@ -285,6 +289,11 @@ func (svc *Service) ApplyCertificateTemplateSpecs(ctx context.Context, specs []*
teamID = uint(parsed)
}
// Validate Fleet variables in subject name
if err := validateCertificateTemplateFleetVariables(spec.SubjectName); err != nil {
return &fleet.BadRequestError{Message: fmt.Sprintf("%s (certificate %s)", err.Error(), spec.Name)}
}
cert := &fleet.CertificateTemplate{
Name: spec.Name,
CertificateAuthorityID: spec.CertificateAuthorityId,
@ -325,35 +334,6 @@ func (svc *Service) DeleteCertificateTemplateSpecs(ctx context.Context, certific
return svc.ds.BatchDeleteCertificateTemplates(ctx, certificateTemplateIDs)
}
// replaceCertificateVariables replaces FLEET_VAR_* variables in the subject name with actual host values
func (svc *Service) replaceCertificateVariables(ctx context.Context, subjectName string, host *fleet.Host) (string, error) {
fleetVars := variables.Find(subjectName)
if len(fleetVars) == 0 {
return subjectName, nil
}
result := subjectName
for _, fleetVar := range fleetVars {
switch fleetVar {
case "HOST_UUID":
result = fleet.FleetVarHostUUIDRegexp.ReplaceAllString(result, host.UUID)
case "HOST_HARDWARE_SERIAL":
result = fleet.FleetVarHostHardwareSerialRegexp.ReplaceAllString(result, host.HardwareSerial)
case "HOST_END_USER_IDP_USERNAME":
users, err := fleet.GetEndUsers(ctx, svc.ds, host.ID)
if err != nil {
return "", ctxerr.Wrapf(ctx, err, "getting host end users for variable %s", fleetVar)
}
if len(users) == 0 || users[0].IdpUserName == "" {
return "", ctxerr.Errorf(ctx, "host %s does not have an IDP username for variable %s", host.UUID, fleetVar)
}
result = fleet.FleetVarHostEndUserIDPUsernameRegexp.ReplaceAllString(result, users[0].IdpUserName)
}
}
return result, nil
}
type updateCertificateStatusRequest struct {
CertificateTemplateID uint `url:"id"`
NodeKey string `json:"node_key"`

View file

@ -2920,6 +2920,13 @@ func (c *Client) doGitOpsAndroidCertificates(config *spec.GitOps, logFn func(for
)
}
// Validate Fleet variables in subject name
if err := validateCertificateTemplateFleetVariables(certificates[i].SubjectName); err != nil {
return newGitOpsValidationError(
fmt.Sprintf(`Invalid Fleet variable in certificate %q: %s`, certificates[i].Name, err.Error()),
)
}
caID, ok := caIDsByName[certificates[i].CertificateAuthorityName]
if !ok {
return fmt.Errorf("certificate authority %q not found for certificate %q",

View file

@ -7978,6 +7978,18 @@ func (s *integrationTestSuite) TestCertificatesSpecs() {
// apply certificate templates
var applyResp applyCertificateTemplateSpecsResponse
s.DoJSON("POST", "/api/latest/fleet/spec/certificates", applyCertificateTemplateSpecsRequest{
Specs: []*fleet.CertificateRequestSpec{
{
Name: "Invalid Template",
Team: fmt.Sprint(team.ID),
CertificateAuthorityId: ca.ID,
SubjectName: "CN=$FLEET_VAR_NOT_VALID/OU=$FLEET_VAR_HOST_UUID",
},
},
}, http.StatusBadRequest, &applyResp)
// valid templates
s.DoJSON("POST", "/api/latest/fleet/spec/certificates", applyCertificateTemplateSpecsRequest{
Specs: []*fleet.CertificateRequestSpec{
{
@ -7990,7 +8002,7 @@ func (s *integrationTestSuite) TestCertificatesSpecs() {
Name: "Template 2",
Team: fmt.Sprint(team.ID),
CertificateAuthorityId: ca.ID,
SubjectName: "CN=$FLEET_VAR_HOST_END_USER_IDP_USERNAME/OU=$FLEET_VAR_HOST_UUID/ST=$FLEET_VAR_HOST_HARDWARE_SERIAL",
SubjectName: "CN=$FLEET_VAR_HOST_END_USER_IDP_USERNAME/OU=$FLEET_VAR_HOST_UUID",
},
},
}, http.StatusOK, &applyResp)