**Related issue:** Resolves #36681, #48042 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. ## Testing - [x] Added/updated automated tests - [ ] QA'd all new/changed functionality manually ## Database migrations - [x] Checked schema for all modified table for columns that will auto-update timestamps during migration. - [x] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - [x] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Certificate templates and managed Android app configurations now keep track of referenced variables. * Variable changes can now trigger automatic re-sending of affected profiles and app availability updates. * **Bug Fixes** * Resend behavior now refreshes certificate templates when related variable values change. * Android managed app configurations are re-queued when their variables are updated. * **Database** * Added support for variable tracking on certificate templates and Android app configurations. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
937 lines
34 KiB
Go
937 lines
34 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"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"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestCreateCertificateTemplate(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
// Certificate templates are Premium-gated (CAs are Premium, and templates require a CA).
|
|
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}})
|
|
|
|
ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
|
|
|
|
type TestCAID uint
|
|
|
|
const (
|
|
InvalidCATypeID TestCAID = iota + 1
|
|
ValidCATypeID
|
|
)
|
|
|
|
const TeamID = 1
|
|
|
|
ds.GetCertificateAuthorityByIDFunc = func(ctx context.Context, id uint, includeSecrets bool) (*fleet.CertificateAuthority, error) {
|
|
if id == uint(InvalidCATypeID) {
|
|
ca := fleet.CertificateAuthority{
|
|
ID: id,
|
|
Type: string(fleet.CATypeDigiCert),
|
|
}
|
|
return &ca, nil
|
|
}
|
|
if id == uint(ValidCATypeID) {
|
|
ca := fleet.CertificateAuthority{
|
|
ID: id,
|
|
Type: string(fleet.CATypeCustomSCEPProxy),
|
|
}
|
|
return &ca, nil
|
|
}
|
|
return nil, errors.New("not found")
|
|
}
|
|
|
|
ds.CreateCertificateTemplateFunc = func(ctx context.Context, certificateTemplate *fleet.CertificateTemplate) (*fleet.CertificateTemplateResponse, error) {
|
|
return &fleet.CertificateTemplateResponse{
|
|
CertificateTemplateResponseSummary: fleet.CertificateTemplateResponseSummary{
|
|
ID: 1,
|
|
Name: certificateTemplate.Name,
|
|
},
|
|
}, nil
|
|
}
|
|
ds.CreatePendingCertificateTemplatesForExistingHostsFunc = func(ctx context.Context, certificateTemplateID uint, teamID uint) (int64, error) {
|
|
return 0, nil
|
|
}
|
|
ds.SetCertificateTemplateVariablesFunc = func(ctx context.Context, certTemplateID uint, fleetVars []fleet.FleetVarName) error {
|
|
return nil
|
|
}
|
|
ds.TeamLiteFunc = func(ctx context.Context, tid uint) (*fleet.TeamLite, error) {
|
|
return &fleet.TeamLite{ID: tid, Name: "Yellow jackets"}, nil
|
|
}
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{}, nil
|
|
}
|
|
t.Run("Invalid CA type", func(t *testing.T) {
|
|
_, err := svc.CreateCertificateTemplate(ctx, "my template", TeamID, uint(InvalidCATypeID), "CN=$FLEET_VAR_HOST_UUID", "")
|
|
require.Error(t, err)
|
|
// Check that the error is about invalid CA type
|
|
require.Contains(t, err.Error(), "Currently, only the custom_scep_proxy certificate authority is supported")
|
|
})
|
|
|
|
t.Run("Valid CA type", func(t *testing.T) {
|
|
_, err := svc.CreateCertificateTemplate(ctx, "my template", TeamID, uint(ValidCATypeID), "CN=$FLEET_VAR_HOST_UUID", "")
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("Missing CA", func(t *testing.T) {
|
|
_, err := svc.CreateCertificateTemplate(ctx, "my template", TeamID, 999, "CN=$FLEET_VAR_HOST_UUID", "")
|
|
require.Error(t, err)
|
|
// Check that the error is about invalid CA type
|
|
require.Contains(t, err.Error(), "not found")
|
|
})
|
|
|
|
t.Run("Empty or whitespace-only name", func(t *testing.T) {
|
|
whitespaceNames := []string{"", " ", " ", "\t", "\n", " \t\n "}
|
|
for _, name := range whitespaceNames {
|
|
_, err := svc.CreateCertificateTemplate(ctx, name, TeamID, uint(ValidCATypeID), "CN=$FLEET_VAR_HOST_UUID", "")
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "Certificate template name is required")
|
|
}
|
|
})
|
|
|
|
t.Run("Name too long", func(t *testing.T) {
|
|
longName := strings.Repeat("a", 256)
|
|
_, err := svc.CreateCertificateTemplate(ctx, longName, TeamID, uint(ValidCATypeID), "CN=$FLEET_VAR_HOST_UUID", "")
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "Certificate template name is too long")
|
|
})
|
|
|
|
t.Run("Name with invalid characters", func(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
}{
|
|
{name: "template@name"},
|
|
{name: "template#name"},
|
|
{name: "template$name"},
|
|
{name: "template%name"},
|
|
{name: "template.name"},
|
|
{name: "template/name"},
|
|
{name: "template\\name"},
|
|
{name: "template!name"},
|
|
{name: "template?name"},
|
|
{name: "template*name"},
|
|
{name: "template+name"},
|
|
{name: "template=name"},
|
|
{name: "template<name>"},
|
|
{name: "template(name)"},
|
|
{name: "template[name]"},
|
|
{name: "template{name}"},
|
|
{name: "template|name"},
|
|
{name: "template;name"},
|
|
{name: "template:name"},
|
|
{name: "template'name"},
|
|
{name: "template\"name"},
|
|
{name: "template`name"},
|
|
{name: "template~name"},
|
|
{name: "template^name"},
|
|
{name: "template name"},
|
|
}
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := svc.CreateCertificateTemplate(ctx, tc.name, TeamID, uint(ValidCATypeID), "CN=$FLEET_VAR_HOST_UUID", "")
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "Invalid certificate template name")
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("Name with valid characters", func(t *testing.T) {
|
|
validNames := []string{
|
|
"my template",
|
|
" my template ",
|
|
"my-template",
|
|
"my_template",
|
|
"MyTemplate123",
|
|
"Template 1",
|
|
"UPPERCASE",
|
|
"lowercase",
|
|
"Mix-Ed_Case 123",
|
|
"a",
|
|
"1",
|
|
"a1",
|
|
"1a",
|
|
}
|
|
for _, name := range validNames {
|
|
t.Run(name, func(t *testing.T) {
|
|
_, err := svc.CreateCertificateTemplate(ctx, name, TeamID, uint(ValidCATypeID), "CN=$FLEET_VAR_HOST_UUID", "")
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("Empty or whitespace-only subject name", func(t *testing.T) {
|
|
whitespaceSubjectNames := []string{"", " ", " \t\n "}
|
|
for _, subjectName := range whitespaceSubjectNames {
|
|
_, err := svc.CreateCertificateTemplate(ctx, "my template", TeamID, uint(ValidCATypeID), subjectName, "")
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "Certificate template subject name is required")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCreateCertificateTemplateSubjectAlternativeName(t *testing.T) {
|
|
const ValidCATypeID = uint(2)
|
|
const TeamID = 1
|
|
|
|
makePremiumService := func(t *testing.T) (fleet.Service, context.Context, *mock.Store) {
|
|
ds := new(mock.Store)
|
|
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}})
|
|
ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
|
|
|
|
ds.GetCertificateAuthorityByIDFunc = func(ctx context.Context, id uint, includeSecrets bool) (*fleet.CertificateAuthority, error) {
|
|
return &fleet.CertificateAuthority{ID: id, Type: string(fleet.CATypeCustomSCEPProxy)}, nil
|
|
}
|
|
ds.CreateCertificateTemplateFunc = func(ctx context.Context, certificateTemplate *fleet.CertificateTemplate) (*fleet.CertificateTemplateResponse, error) {
|
|
return &fleet.CertificateTemplateResponse{
|
|
CertificateTemplateResponseSummary: fleet.CertificateTemplateResponseSummary{
|
|
ID: 1,
|
|
Name: certificateTemplate.Name,
|
|
SubjectName: certificateTemplate.SubjectName,
|
|
SubjectAlternativeName: certificateTemplate.SubjectAlternativeName,
|
|
},
|
|
TeamID: certificateTemplate.TeamID,
|
|
}, nil
|
|
}
|
|
ds.CreatePendingCertificateTemplatesForExistingHostsFunc = func(ctx context.Context, certificateTemplateID uint, teamID uint) (int64, error) {
|
|
return 0, nil
|
|
}
|
|
ds.SetCertificateTemplateVariablesFunc = func(ctx context.Context, certTemplateID uint, fleetVars []fleet.FleetVarName) error {
|
|
return nil
|
|
}
|
|
ds.TeamLiteFunc = func(ctx context.Context, tid uint) (*fleet.TeamLite, error) {
|
|
return &fleet.TeamLite{ID: tid, Name: "Yellow jackets"}, nil
|
|
}
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{}, nil
|
|
}
|
|
return svc, ctx, ds
|
|
}
|
|
|
|
t.Run("Premium tenant with valid SAN succeeds and round-trips the value", func(t *testing.T) {
|
|
svc, ctx, ds := makePremiumService(t)
|
|
|
|
san := "DNS=wifi.example.com, UPN=$FLEET_VAR_HOST_END_USER_IDP_USERNAME, EMAIL=$FLEET_VAR_HOST_END_USER_IDP_USERNAME"
|
|
resp, err := svc.CreateCertificateTemplate(ctx, "wifi", TeamID, ValidCATypeID, "CN=$FLEET_VAR_HOST_UUID", san)
|
|
require.NoError(t, err)
|
|
require.Equal(t, san, resp.SubjectAlternativeName)
|
|
require.True(t, ds.CreateCertificateTemplateFuncInvoked)
|
|
})
|
|
|
|
t.Run("Non-Premium tenant cannot create any certificate template (gate is in CreateCertificateTemplate, before validation)", func(t *testing.T) {
|
|
// Certificate templates require a CA, and CAs are Premium-only, so the whole feature is
|
|
// gated by a Premium check at the top of Service.CreateCertificateTemplate. SAN-bearing
|
|
// payloads are not the only ones rejected.
|
|
ds := new(mock.Store)
|
|
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierFree}})
|
|
ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
|
|
|
|
// With SAN.
|
|
_, err := svc.CreateCertificateTemplate(ctx, "wifi-with-san", TeamID, ValidCATypeID, "CN=$FLEET_VAR_HOST_UUID", "DNS=example.com")
|
|
require.ErrorIs(t, err, fleet.ErrMissingLicense)
|
|
|
|
// Without SAN also rejected.
|
|
_, err = svc.CreateCertificateTemplate(ctx, "wifi-no-san", TeamID, ValidCATypeID, "CN=$FLEET_VAR_HOST_UUID", "")
|
|
require.ErrorIs(t, err, fleet.ErrMissingLicense)
|
|
})
|
|
|
|
t.Run("Format failures return InvalidArgumentError scoped to the SAN field", func(t *testing.T) {
|
|
svc, ctx, _ := makePremiumService(t)
|
|
|
|
cases := []struct {
|
|
name string
|
|
san string
|
|
fragment string
|
|
}{
|
|
{"missing equals", "DNS=ok, OOPS", "missing '='"},
|
|
{"unknown key", "FOO=bar", "unsupported key"},
|
|
{"rfc822 not synonym", "RFC822=user@x", "unsupported key"},
|
|
{"too long", strings.Repeat("DNS=a,", 1024), "too long"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := svc.CreateCertificateTemplate(ctx, "wifi", TeamID, ValidCATypeID, "CN=$FLEET_VAR_HOST_UUID", tc.san)
|
|
require.Error(t, err)
|
|
var iae *fleet.InvalidArgumentError
|
|
require.ErrorAs(t, err, &iae)
|
|
require.True(t, iae.HasErrors())
|
|
details := iae.Invalid()
|
|
require.Len(t, details, 1)
|
|
require.Equal(t, "subject_alternative_name", details[0]["name"])
|
|
require.Contains(t, details[0]["reason"], tc.fragment)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("Unsupported variable in SAN is rejected", func(t *testing.T) {
|
|
svc, ctx, _ := makePremiumService(t)
|
|
|
|
_, err := svc.CreateCertificateTemplate(ctx, "wifi", TeamID, ValidCATypeID, "CN=$FLEET_VAR_HOST_UUID", "EMAIL=$FLEET_VAR_NDES_SCEP_CHALLENGE")
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "FLEET_VAR_NDES_SCEP_CHALLENGE")
|
|
})
|
|
|
|
t.Run("All supported HOST variables accepted in SAN", func(t *testing.T) {
|
|
svc, ctx, _ := makePremiumService(t)
|
|
|
|
san := "DNS=$FLEET_VAR_HOST_UUID, EMAIL=$FLEET_VAR_HOST_END_USER_IDP_USERNAME, " +
|
|
"UPN=$FLEET_VAR_HOST_END_USER_IDP_USERNAME_LOCAL_PART, " +
|
|
"URI=$FLEET_VAR_HOST_END_USER_IDP_GROUPS, " +
|
|
"DNS=$FLEET_VAR_HOST_END_USER_IDP_DEPARTMENT, " +
|
|
"EMAIL=$FLEET_VAR_HOST_END_USER_IDP_FULL_NAME, " +
|
|
"DNS=$FLEET_VAR_HOST_PLATFORM, " +
|
|
"DNS=$FLEET_VAR_HOST_HARDWARE_SERIAL"
|
|
resp, err := svc.CreateCertificateTemplate(ctx, "all-vars", TeamID, ValidCATypeID, "CN=$FLEET_VAR_HOST_UUID", san)
|
|
require.NoError(t, err)
|
|
require.Equal(t, san, resp.SubjectAlternativeName)
|
|
})
|
|
}
|
|
|
|
func TestValidateCertificateTemplateSubjectAlternativeName(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
san string
|
|
expectError bool
|
|
errContains string
|
|
}{
|
|
{"empty allowed", "", false, ""},
|
|
{"whitespace allowed", " \t\n ", false, ""},
|
|
{"single DNS", "DNS=example.com", false, ""},
|
|
{"single EMAIL", "EMAIL=user@example.com", false, ""},
|
|
{"single UPN", "UPN=user@corp.example.com", false, ""},
|
|
{"single IP", "IP=10.0.0.1", false, ""},
|
|
{"single URI", "URI=spiffe://example.com/x", false, ""},
|
|
{"all five mixed", "DNS=a, EMAIL=b@x, UPN=c@d, IP=10.0.0.1, URI=spiffe://x", false, ""},
|
|
{"case insensitive keys", "dns=a, email=b@x, upn=c@d, ip=10.0.0.1, uri=spiffe://x", false, ""},
|
|
{"repeated keys", "DNS=a, DNS=b, EMAIL=c@x, EMAIL=d@y", false, ""},
|
|
{"trailing comma is fine", "DNS=a,", false, ""},
|
|
{"missing equals", "DNS=a, OOPS", true, "missing '='"},
|
|
{"unknown key FOO", "FOO=bar", true, "unsupported key"},
|
|
{"RFC822 is not a synonym", "RFC822=user@x", true, "unsupported key"},
|
|
{"length cap", strings.Repeat("DNS=a,", 1024), true, "too long"},
|
|
{"empty key with equals only", "=value", true, "empty key"},
|
|
{"empty value DNS=", "DNS=", true, "empty value"},
|
|
{"empty value EMAIL= mixed", "DNS=ok.example.com, EMAIL=", true, "empty value"},
|
|
{"separator only", ",", true, "no entries"},
|
|
{"separator only with whitespace", " , ", true, "no entries"},
|
|
{"only commas", ",,,", true, "no entries"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := validateCertificateTemplateSubjectAlternativeName(tc.san, "")
|
|
if tc.expectError {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), tc.errContains)
|
|
// Validator must return a typed *fleet.InvalidArgumentError scoped to the SAN field (HTTP 422).
|
|
var iae *fleet.InvalidArgumentError
|
|
require.ErrorAs(t, err, &iae)
|
|
require.True(t, iae.HasErrors())
|
|
details := iae.Invalid()
|
|
require.Len(t, details, 1)
|
|
require.Equal(t, "subject_alternative_name", details[0]["name"])
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestApplyCertificateTemplateSpecs(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
// Certificate templates are Premium-gated.
|
|
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}})
|
|
|
|
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
|
|
}
|
|
|
|
// Set up certificate authority mocks
|
|
certAuthorities := []*fleet.CertificateAuthority{
|
|
{
|
|
ID: 1,
|
|
Name: ptr.String("Test CA 1"),
|
|
Type: string(fleet.CATypeCustomSCEPProxy),
|
|
URL: ptr.String("https://ca1.example.com"),
|
|
Challenge: ptr.String("challenge1"),
|
|
},
|
|
{
|
|
ID: 2,
|
|
Name: ptr.String("Test CA 2"),
|
|
Type: string(fleet.CATypeCustomSCEPProxy),
|
|
URL: ptr.String("https://ca2.example.com"),
|
|
Challenge: ptr.String("challenge2"),
|
|
},
|
|
{
|
|
ID: 3,
|
|
Name: ptr.String("Test CA 3"),
|
|
Type: string(fleet.CATypeDigiCert),
|
|
URL: ptr.String("https://ca3.example.com"),
|
|
Challenge: ptr.String("challenge3"),
|
|
CertificateCommonName: ptr.String("foo"),
|
|
CertificateSeatID: ptr.String("foo"),
|
|
CertificateUserPrincipalNames: &[]string{"foo"},
|
|
APIToken: ptr.String("foo"),
|
|
ProfileID: ptr.String("foo"),
|
|
},
|
|
}
|
|
|
|
ds.ListCertificateAuthoritiesFunc = func(ctx context.Context) ([]*fleet.CertificateAuthoritySummary, error) {
|
|
summaries := make([]*fleet.CertificateAuthoritySummary, 0, len(certAuthorities))
|
|
for _, ca := range certAuthorities {
|
|
summaries = append(summaries, &fleet.CertificateAuthoritySummary{
|
|
ID: ca.ID,
|
|
Name: *ca.Name,
|
|
Type: ca.Type,
|
|
})
|
|
}
|
|
return summaries, nil
|
|
}
|
|
|
|
// Track certificate templates that are created
|
|
var createdCertificates []fleet.CertificateTemplate
|
|
var nextTemplateID uint = 100
|
|
|
|
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 createdMap, nil
|
|
}
|
|
|
|
ds.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, opts fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
|
|
var result []*fleet.CertificateTemplateResponseSummary
|
|
for _, cert := range createdCertificates {
|
|
if cert.TeamID == teamID {
|
|
result = append(result, &fleet.CertificateTemplateResponseSummary{
|
|
ID: nextTemplateID,
|
|
Name: cert.Name,
|
|
})
|
|
nextTemplateID++
|
|
}
|
|
}
|
|
return result, nil, nil
|
|
}
|
|
|
|
ds.CreatePendingCertificateTemplatesForExistingHostsFunc = func(ctx context.Context, certificateTemplateID uint, teamID uint) (int64, error) {
|
|
return 0, nil
|
|
}
|
|
ds.SetCertificateTemplateVariablesFunc = func(ctx context.Context, certTemplateID uint, fleetVars []fleet.FleetVarName) error {
|
|
return nil
|
|
}
|
|
|
|
ds.GetCertificateTemplateByTeamIDAndNameFunc = func(ctx context.Context, teamID uint, name string) (*fleet.CertificateTemplateResponse, error) {
|
|
return &fleet.CertificateTemplateResponse{
|
|
CertificateTemplateResponseSummary: fleet.CertificateTemplateResponseSummary{
|
|
ID: nextTemplateID,
|
|
Name: name,
|
|
},
|
|
TeamID: teamID,
|
|
}, nil
|
|
}
|
|
|
|
t.Run("Valid CA types", func(t *testing.T) {
|
|
err := svc.ApplyCertificateTemplateSpecs(ctx, []*fleet.CertificateRequestSpec{
|
|
{
|
|
Name: "Template 1",
|
|
CertificateAuthorityId: 1,
|
|
SubjectName: "foo",
|
|
},
|
|
{
|
|
Name: "Template 2",
|
|
CertificateAuthorityId: 2,
|
|
SubjectName: "bar",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, createdCertificates, 2)
|
|
require.Equal(t, "Template 1", createdCertificates[0].Name)
|
|
require.Equal(t, uint(1), createdCertificates[0].CertificateAuthorityID)
|
|
require.Equal(t, "Template 2", createdCertificates[1].Name)
|
|
require.Equal(t, uint(2), createdCertificates[1].CertificateAuthorityID)
|
|
})
|
|
|
|
t.Run("Invalid CA type", func(t *testing.T) {
|
|
err := svc.ApplyCertificateTemplateSpecs(ctx, []*fleet.CertificateRequestSpec{
|
|
{
|
|
Name: "Template 3",
|
|
CertificateAuthorityId: 3,
|
|
SubjectName: "baz",
|
|
},
|
|
})
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "Currently, only the custom_scep_proxy certificate authority is supported")
|
|
})
|
|
|
|
t.Run("Missing CA", func(t *testing.T) {
|
|
err := svc.ApplyCertificateTemplateSpecs(ctx, []*fleet.CertificateRequestSpec{
|
|
{
|
|
Name: "Template 4",
|
|
CertificateAuthorityId: 4,
|
|
SubjectName: "baz",
|
|
},
|
|
})
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "not found")
|
|
})
|
|
|
|
t.Run("Empty name", func(t *testing.T) {
|
|
err := svc.ApplyCertificateTemplateSpecs(ctx, []*fleet.CertificateRequestSpec{
|
|
{
|
|
Name: "",
|
|
CertificateAuthorityId: 1,
|
|
SubjectName: "foo",
|
|
},
|
|
})
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "Certificate template name is required")
|
|
})
|
|
|
|
t.Run("Whitespace-only name", func(t *testing.T) {
|
|
err := svc.ApplyCertificateTemplateSpecs(ctx, []*fleet.CertificateRequestSpec{
|
|
{
|
|
Name: " ",
|
|
CertificateAuthorityId: 1,
|
|
SubjectName: "foo",
|
|
},
|
|
})
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "Certificate template name is required")
|
|
})
|
|
|
|
t.Run("Name with invalid characters", func(t *testing.T) {
|
|
err := svc.ApplyCertificateTemplateSpecs(ctx, []*fleet.CertificateRequestSpec{
|
|
{
|
|
Name: "template@name",
|
|
CertificateAuthorityId: 1,
|
|
SubjectName: "foo",
|
|
},
|
|
})
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "Invalid certificate template name")
|
|
})
|
|
|
|
t.Run("Name too long", func(t *testing.T) {
|
|
longName := strings.Repeat("a", 256)
|
|
err := svc.ApplyCertificateTemplateSpecs(ctx, []*fleet.CertificateRequestSpec{
|
|
{
|
|
Name: longName,
|
|
CertificateAuthorityId: 1,
|
|
SubjectName: "foo",
|
|
},
|
|
})
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "Certificate template name is too long")
|
|
})
|
|
|
|
t.Run("Whitespace-only subject name", func(t *testing.T) {
|
|
err := svc.ApplyCertificateTemplateSpecs(ctx, []*fleet.CertificateRequestSpec{
|
|
{
|
|
Name: "Template 2",
|
|
CertificateAuthorityId: 1,
|
|
SubjectName: " ",
|
|
},
|
|
})
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "Certificate template subject name is required")
|
|
require.Contains(t, err.Error(), "Template 2")
|
|
})
|
|
|
|
// SAN coverage: the only Apply-specific assertion worth a unit test is that the cert-name
|
|
// suffix from validateCertificateTemplateSubjectAlternativeName reaches the typed error so
|
|
// admins applying a multi-cert spec can identify which entry is bad. Other SAN paths
|
|
// (validator semantics, single-cert Create) are covered in TestValidateCertificateTemplate*
|
|
// and TestCreateCertificateTemplate*.
|
|
t.Run("Invalid SAN rejected with cert-name suffix in subject_alternative_name field", func(t *testing.T) {
|
|
err := svc.ApplyCertificateTemplateSpecs(ctx, []*fleet.CertificateRequestSpec{
|
|
{
|
|
Name: "Template SAN bad",
|
|
CertificateAuthorityId: 1,
|
|
SubjectName: "CN=$FLEET_VAR_HOST_UUID",
|
|
SubjectAlternativeName: "FOO=bar",
|
|
},
|
|
})
|
|
require.Error(t, err)
|
|
var iae *fleet.InvalidArgumentError
|
|
require.ErrorAs(t, err, &iae)
|
|
details := iae.Invalid()
|
|
require.Len(t, details, 1)
|
|
require.Equal(t, "subject_alternative_name", details[0]["name"])
|
|
require.Contains(t, details[0]["reason"], "Template SAN bad")
|
|
require.Contains(t, details[0]["reason"], `unsupported key "FOO"`)
|
|
})
|
|
}
|
|
|
|
func TestReplaceCertificateVariables(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
|
|
givenName := "Jane"
|
|
familyName := "Doe"
|
|
dept := "Engineering"
|
|
|
|
ds.ScimUserByHostIDFunc = func(ctx context.Context, hostID uint) (*fleet.ScimUser, error) {
|
|
return &fleet.ScimUser{
|
|
UserName: "jane@example.com",
|
|
GivenName: &givenName,
|
|
FamilyName: &familyName,
|
|
Department: &dept,
|
|
Groups: []fleet.ScimUserGroup{
|
|
{DisplayName: "admins"},
|
|
{DisplayName: "devs"},
|
|
},
|
|
}, nil
|
|
}
|
|
ds.ListHostDeviceMappingFunc = func(ctx context.Context, hostID uint) ([]*fleet.HostDeviceMapping, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
svc := &Service{ds: ds}
|
|
host := &fleet.Host{
|
|
ID: 1,
|
|
UUID: "host-uuid-123",
|
|
HardwareSerial: "SERIAL-456",
|
|
Platform: "android",
|
|
}
|
|
|
|
t.Run("HOST_UUID", func(t *testing.T) {
|
|
result, err := svc.replaceCertificateVariables(t.Context(), "CN=$FLEET_VAR_HOST_UUID", host, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "CN=host-uuid-123", result)
|
|
})
|
|
|
|
t.Run("HOST_HARDWARE_SERIAL", func(t *testing.T) {
|
|
result, err := svc.replaceCertificateVariables(t.Context(), "CN=$FLEET_VAR_HOST_HARDWARE_SERIAL", host, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "CN=SERIAL-456", result)
|
|
})
|
|
|
|
t.Run("HOST_PLATFORM", func(t *testing.T) {
|
|
result, err := svc.replaceCertificateVariables(t.Context(), "O=$FLEET_VAR_HOST_PLATFORM", host, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "O=android", result)
|
|
})
|
|
|
|
t.Run("HOST_END_USER_IDP_USERNAME", func(t *testing.T) {
|
|
result, err := svc.replaceCertificateVariables(t.Context(), "CN=$FLEET_VAR_HOST_END_USER_IDP_USERNAME", host, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "CN=jane@example.com", result)
|
|
})
|
|
|
|
t.Run("HOST_END_USER_IDP_USERNAME_LOCAL_PART", func(t *testing.T) {
|
|
result, err := svc.replaceCertificateVariables(t.Context(), "CN=$FLEET_VAR_HOST_END_USER_IDP_USERNAME_LOCAL_PART", host, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "CN=jane", result)
|
|
})
|
|
|
|
t.Run("HOST_END_USER_IDP_GROUPS", func(t *testing.T) {
|
|
result, err := svc.replaceCertificateVariables(t.Context(), "OU=$FLEET_VAR_HOST_END_USER_IDP_GROUPS", host, nil)
|
|
require.NoError(t, err)
|
|
// Comma between groups is escaped so it's not mistaken for a DN separator.
|
|
require.Equal(t, `OU=admins\,devs`, result)
|
|
})
|
|
|
|
t.Run("HOST_END_USER_IDP_DEPARTMENT", func(t *testing.T) {
|
|
result, err := svc.replaceCertificateVariables(t.Context(), "OU=$FLEET_VAR_HOST_END_USER_IDP_DEPARTMENT", host, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "OU=Engineering", result)
|
|
})
|
|
|
|
t.Run("HOST_END_USER_IDP_FULL_NAME", func(t *testing.T) {
|
|
result, err := svc.replaceCertificateVariables(t.Context(), "CN=$FLEET_VAR_HOST_END_USER_IDP_FULL_NAME", host, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "CN=Jane Doe", result)
|
|
})
|
|
|
|
t.Run("multiple variables in one string", func(t *testing.T) {
|
|
input := "CN=$FLEET_VAR_HOST_END_USER_IDP_USERNAME,O=$FLEET_VAR_HOST_PLATFORM,OU=$FLEET_VAR_HOST_END_USER_IDP_DEPARTMENT"
|
|
result, err := svc.replaceCertificateVariables(t.Context(), input, host, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "CN=jane@example.com,O=android,OU=Engineering", result)
|
|
})
|
|
|
|
t.Run("endUsersMemo is populated on first call and reused", func(t *testing.T) {
|
|
var memo []fleet.HostEndUser
|
|
_, err := svc.replaceCertificateVariables(t.Context(), "CN=$FLEET_VAR_HOST_END_USER_IDP_USERNAME", host, &memo)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, memo)
|
|
require.Len(t, memo, 1)
|
|
|
|
// Second call reuses the memo without hitting the datastore again.
|
|
ds.ScimUserByHostIDFuncInvoked = false
|
|
_, err = svc.replaceCertificateVariables(t.Context(), "CN=$FLEET_VAR_HOST_END_USER_IDP_FULL_NAME", host, &memo)
|
|
require.NoError(t, err)
|
|
require.False(t, ds.ScimUserByHostIDFuncInvoked)
|
|
})
|
|
|
|
t.Run("missing IDP user returns error", func(t *testing.T) {
|
|
ds.ScimUserByHostIDFunc = func(ctx context.Context, hostID uint) (*fleet.ScimUser, error) {
|
|
return nil, ¬FoundError{}
|
|
}
|
|
_, err := svc.replaceCertificateVariables(t.Context(), "CN=$FLEET_VAR_HOST_END_USER_IDP_USERNAME", host, nil)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "does not have an IDP user")
|
|
})
|
|
|
|
t.Run("missing groups returns error", func(t *testing.T) {
|
|
ds.ScimUserByHostIDFunc = func(ctx context.Context, hostID uint) (*fleet.ScimUser, error) {
|
|
return &fleet.ScimUser{UserName: "jane@example.com"}, nil
|
|
}
|
|
_, err := svc.replaceCertificateVariables(t.Context(), "OU=$FLEET_VAR_HOST_END_USER_IDP_GROUPS", host, nil)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "does not have IDP groups")
|
|
})
|
|
|
|
t.Run("missing department returns error", func(t *testing.T) {
|
|
ds.ScimUserByHostIDFunc = func(ctx context.Context, hostID uint) (*fleet.ScimUser, error) {
|
|
return &fleet.ScimUser{UserName: "jane@example.com"}, nil
|
|
}
|
|
_, err := svc.replaceCertificateVariables(t.Context(), "OU=$FLEET_VAR_HOST_END_USER_IDP_DEPARTMENT", host, nil)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "does not have an IDP department")
|
|
})
|
|
|
|
t.Run("missing full name returns error", func(t *testing.T) {
|
|
ds.ScimUserByHostIDFunc = func(ctx context.Context, hostID uint) (*fleet.ScimUser, error) {
|
|
return &fleet.ScimUser{UserName: "jane@example.com"}, nil
|
|
}
|
|
_, err := svc.replaceCertificateVariables(t.Context(), "CN=$FLEET_VAR_HOST_END_USER_IDP_FULL_NAME", host, nil)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "does not have an IDP full name")
|
|
})
|
|
|
|
t.Run("no variables returns input unchanged", func(t *testing.T) {
|
|
result, err := svc.replaceCertificateVariables(t.Context(), "CN=static-value", host, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "CN=static-value", result)
|
|
})
|
|
|
|
t.Run("unsupported variable returns error", func(t *testing.T) {
|
|
_, err := svc.replaceCertificateVariables(t.Context(), "CN=$FLEET_VAR_NDES_SCEP_CHALLENGE", host, nil)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "unsupported Fleet variable")
|
|
})
|
|
|
|
t.Run("special characters are RFC 4514 escaped", func(t *testing.T) {
|
|
dept := "Sales, Marketing + Ops"
|
|
ds.ScimUserByHostIDFunc = func(ctx context.Context, hostID uint) (*fleet.ScimUser, error) {
|
|
return &fleet.ScimUser{
|
|
UserName: "jane@example.com",
|
|
GivenName: &givenName,
|
|
FamilyName: &familyName,
|
|
Department: &dept,
|
|
Groups: []fleet.ScimUserGroup{
|
|
{DisplayName: "group<A>"},
|
|
{DisplayName: `group"B"`},
|
|
},
|
|
}, nil
|
|
}
|
|
result, err := svc.replaceCertificateVariables(t.Context(), "OU=$FLEET_VAR_HOST_END_USER_IDP_DEPARTMENT", host, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, `OU=Sales\, Marketing \+ Ops`, result)
|
|
|
|
result, err = svc.replaceCertificateVariables(t.Context(), "OU=$FLEET_VAR_HOST_END_USER_IDP_GROUPS", host, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, `OU=group\<A\>\,group\"B\"`, result)
|
|
})
|
|
}
|
|
|
|
func TestExtractCertTemplateFleetVars(t *testing.T) {
|
|
t.Run("extracts from subject_name and SAN", func(t *testing.T) {
|
|
vars := extractCertTemplateFleetVars(
|
|
"CN=$FLEET_VAR_HOST_UUID",
|
|
"EMAIL=$FLEET_VAR_HOST_END_USER_IDP_USERNAME, DNS=$FLEET_VAR_HOST_PLATFORM",
|
|
)
|
|
require.ElementsMatch(t, []fleet.FleetVarName{
|
|
fleet.FleetVarHostUUID,
|
|
fleet.FleetVarHostEndUserIDPUsername,
|
|
fleet.FleetVarHostPlatform,
|
|
}, vars)
|
|
})
|
|
|
|
t.Run("returns nil for no variables", func(t *testing.T) {
|
|
vars := extractCertTemplateFleetVars("CN=static", "DNS=example.com")
|
|
require.Nil(t, vars)
|
|
})
|
|
|
|
t.Run("deduplicates across subject and SAN", func(t *testing.T) {
|
|
vars := extractCertTemplateFleetVars(
|
|
"CN=$FLEET_VAR_HOST_UUID",
|
|
"DNS=$FLEET_VAR_HOST_UUID",
|
|
)
|
|
require.Equal(t, []fleet.FleetVarName{fleet.FleetVarHostUUID}, vars)
|
|
})
|
|
}
|
|
|
|
func TestCreateCertificateTemplateVariableTracking(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}})
|
|
ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: new(fleet.RoleAdmin)}})
|
|
|
|
ds.GetCertificateAuthorityByIDFunc = func(ctx context.Context, id uint, includeSecrets bool) (*fleet.CertificateAuthority, error) {
|
|
return &fleet.CertificateAuthority{ID: id, Type: string(fleet.CATypeCustomSCEPProxy)}, nil
|
|
}
|
|
ds.CreateCertificateTemplateFunc = func(ctx context.Context, ct *fleet.CertificateTemplate) (*fleet.CertificateTemplateResponse, error) {
|
|
return &fleet.CertificateTemplateResponse{
|
|
CertificateTemplateResponseSummary: fleet.CertificateTemplateResponseSummary{ID: 42, Name: ct.Name},
|
|
TeamID: ct.TeamID,
|
|
}, nil
|
|
}
|
|
ds.CreatePendingCertificateTemplatesForExistingHostsFunc = func(ctx context.Context, certID uint, teamID uint) (int64, error) {
|
|
return 0, nil
|
|
}
|
|
ds.TeamLiteFunc = func(ctx context.Context, tid uint) (*fleet.TeamLite, error) {
|
|
return &fleet.TeamLite{ID: tid, Name: "team"}, nil
|
|
}
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{}, nil
|
|
}
|
|
|
|
var capturedVars []fleet.FleetVarName
|
|
ds.SetCertificateTemplateVariablesFunc = func(ctx context.Context, certTemplateID uint, fleetVars []fleet.FleetVarName) error {
|
|
require.Equal(t, uint(42), certTemplateID)
|
|
capturedVars = fleetVars
|
|
return nil
|
|
}
|
|
|
|
_, err := svc.CreateCertificateTemplate(ctx, "wifi-cert", 1, 1,
|
|
"CN=$FLEET_VAR_HOST_END_USER_IDP_USERNAME",
|
|
"DNS=$FLEET_VAR_HOST_UUID, EMAIL=$FLEET_VAR_HOST_END_USER_IDP_DEPARTMENT",
|
|
)
|
|
require.NoError(t, err)
|
|
require.True(t, ds.SetCertificateTemplateVariablesFuncInvoked)
|
|
require.ElementsMatch(t, []fleet.FleetVarName{
|
|
fleet.FleetVarHostEndUserIDPUsername,
|
|
fleet.FleetVarHostUUID,
|
|
fleet.FleetVarHostEndUserIDPDepartment,
|
|
}, capturedVars)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
ds.GetCertificateTemplateByIdForHostFunc = func(ctx context.Context, id uint, hostUUID string) (*fleet.CertificateTemplateResponseForHost, error) {
|
|
return &fleet.CertificateTemplateResponseForHost{
|
|
CertificateTemplateResponse: fleet.CertificateTemplateResponse{
|
|
CertificateTemplateResponseSummary: fleet.CertificateTemplateResponseSummary{
|
|
ID: id,
|
|
Name: templateName,
|
|
},
|
|
},
|
|
Status: fleet.CertificateTemplateDelivered,
|
|
}, 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)
|
|
})
|
|
|
|
t.Run("returns 400 when template is pending for host", func(t *testing.T) {
|
|
ds.GetCertificateTemplateByIdForHostFunc = func(ctx context.Context, id uint, hostUUID string) (*fleet.CertificateTemplateResponseForHost, error) {
|
|
return &fleet.CertificateTemplateResponseForHost{
|
|
CertificateTemplateResponse: fleet.CertificateTemplateResponse{
|
|
CertificateTemplateResponseSummary: fleet.CertificateTemplateResponseSummary{
|
|
ID: id,
|
|
Name: templateName,
|
|
},
|
|
},
|
|
Status: fleet.CertificateTemplatePending,
|
|
}, nil
|
|
}
|
|
ds.ResendHostCertificateTemplateFuncInvoked = false
|
|
|
|
err := svc.ResendHostCertificateTemplate(ctx, hostID, templateID)
|
|
require.Error(t, err)
|
|
|
|
var umErr interface{ StatusCode() int }
|
|
require.ErrorAs(t, err, &umErr)
|
|
require.Equal(t, 400, umErr.StatusCode())
|
|
require.False(t, ds.ResendHostCertificateTemplateFuncInvoked)
|
|
require.False(t, opts.ActivityMock.NewActivityFuncInvoked)
|
|
})
|
|
}
|