fleet/ee/server/service/certificate_authorities_test.go
Victor Lyuboslavsky aaac4b1dfe
Changes needed before gokit/log to slog transition. (#39527)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #38889

PLEASE READ BELOW before looking at file changes

Before converting individual files/packages to slog, we generally need
to make these 2 changes to make the conversion easier:
- Replace uses of `kitlog.With` since they are not fully compatible with
our kitlog adapter
- Directly use the kitlog adapter logger type instead of the kitlog
interface, which will let us have direct access to the underlying slog
logger: `*logging.Logger`

Note: that I did not replace absolutely all uses of `kitlog.Logger`, but
I did remove all uses of `kitlog.With` except for these due to
complexity:
- server/logging/filesystem.go and the other log writers (webhook,
firehose, kinesis, lambda, pubsub, nats)
- server/datastore/mysql/nanomdm_storage.go (adapter pattern)
- server/vulnerabilities/nvd/* (cascades to CLI tools)
- server/service/osquery_utils/queries.go (callback type signatures
cascade broadly)
- cmd/maintained-apps/ (standalone, so can be transitioned later all at
once)

Most of the changes in this PR follow these patterns:
- `kitlog.Logger` type → `*logging.Logger`
- `kitlog.With(logger, ...)` → `logger.With(...)`
- `kitlog.NewNopLogger() → logging.NewNopLogger()`, including similar
variations such as `logging.NewLogfmtLogger(w)` and
`logging.NewJSONLogger(w)`
- removed many now-unused kitlog imports

Unique changes that the PR review should focus on:
- server/platform/logging/kitlog_adapter.go: Core adapter changes
- server/platform/logging/logging.go: New convenience functions
- server/service/integration_logger_test.go: Test changes for slog

# 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`.
  - Was added in previous PR

## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Migrated the codebase to a unified internal structured logging system
for more consistent, reliable logs and observability.
* No user-facing functionality changed; runtime behavior and APIs remain
compatible.
* **Tests**
* Updated tests to use the new logging helpers to ensure consistent test
logging and validation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-11 10:08:33 -06:00

1968 lines
73 KiB
Go

package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"os"
"regexp"
"testing"
"github.com/fleetdm/fleet/v4/ee/server/service/digicert"
"github.com/fleetdm/fleet/v4/ee/server/service/est"
"github.com/fleetdm/fleet/v4/server/authz"
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mock"
scep_mock "github.com/fleetdm/fleet/v4/server/mock/scep"
"github.com/fleetdm/fleet/v4/server/platform/logging"
common_mysql "github.com/fleetdm/fleet/v4/server/platform/mysql"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetCertificateAuthorityByID(t *testing.T) {
t.Parallel()
ds := new(mock.Store)
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
svc := newTestService(t, ds)
t.Run("GET a CA - Happy path", func(t *testing.T) {
ds.GetCertificateAuthorityByIDFunc = func(ctx context.Context, id uint, includeSecrets bool) (*fleet.CertificateAuthority, error) {
require.False(t, includeSecrets, "includeSecrets should be false - the API should never return the secrets")
return &fleet.CertificateAuthority{
ID: 1,
Name: ptr.String("Test CA"),
URL: ptr.String("https://example.com"),
Type: string(fleet.CATypeDigiCert),
APIToken: ptr.String(fleet.MaskedPassword),
ProfileID: ptr.String("digicert-profile-id"),
CertificateCommonName: ptr.String("digicert-common-name"),
CertificateUserPrincipalNames: &[]string{"digicert-upn1"},
CertificateSeatID: ptr.String("digicert-seat-id"),
}, nil
}
returnedCA, err := svc.GetCertificateAuthority(ctx, 1)
require.NoError(t, err)
require.NotNil(t, returnedCA)
})
t.Run("GET a CA - CA not found", func(t *testing.T) {
ds.GetCertificateAuthorityByIDFunc = func(ctx context.Context, id uint, includeSecrets bool) (*fleet.CertificateAuthority, error) {
require.False(t, includeSecrets, "includeSecrets should be false - the API should never return the secrets")
return nil, common_mysql.NotFound("CertificateAuthority").WithID(id)
}
returnedCA, err := svc.GetCertificateAuthority(ctx, 1)
require.ErrorContains(t, err, "not found")
require.Nil(t, returnedCA)
})
}
func TestListCertificateAuthorities(t *testing.T) {
ds := new(mock.Store)
svc := newTestService(t, ds)
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
t.Run("GET all CAs - Happy path", func(t *testing.T) {
ds.ListCertificateAuthoritiesFunc = func(ctx context.Context) ([]*fleet.CertificateAuthoritySummary, error) {
return []*fleet.CertificateAuthoritySummary{
{
ID: 1,
Name: "TestDigicertCA",
Type: string(fleet.CATypeDigiCert),
},
{
ID: 2,
Name: "TestSCEPCA",
Type: string(fleet.CATypeCustomSCEPProxy),
},
}, nil
}
returnedCAs, err := svc.ListCertificateAuthorities(ctx)
require.NoError(t, err)
require.Len(t, returnedCAs, 2)
})
t.Run("GET all CAs - No CAs found", func(t *testing.T) {
ds.ListCertificateAuthoritiesFunc = func(ctx context.Context) ([]*fleet.CertificateAuthoritySummary, error) {
return []*fleet.CertificateAuthoritySummary{}, nil
}
returnedCAs, err := svc.ListCertificateAuthorities(ctx)
require.NoError(t, err)
require.Len(t, returnedCAs, 0)
})
}
func setupMockCAServers(t *testing.T) (digicertServer, hydrantServer *httptest.Server) {
// Setup mock digicert server
pathRegex := regexp.MustCompile(`^/mpki/api/v2/profile/([a-zA-Z0-9_-]+)$`)
mockDigiCertServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
matches := pathRegex.FindStringSubmatch(r.URL.Path)
if len(matches) != 2 {
w.WriteHeader(http.StatusBadRequest)
return
}
profileID := matches[1]
resp := map[string]string{
"id": profileID,
"name": "Test CA",
"status": "Active",
}
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(resp)
require.NoError(t, err)
}))
mockHydrantServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet && r.URL.Path == "/cacerts" {
w.Header().Set("Content-Type", "application/pkcs7-mime")
w.WriteHeader(http.StatusOK)
_, err := w.Write([]byte("Imagine if there was actually CA cert data here..."))
require.NoError(t, err)
return
}
w.WriteHeader(http.StatusMethodNotAllowed)
}))
return mockDigiCertServer, mockHydrantServer
}
func TestCreatingCertificateAuthorities(t *testing.T) {
digicertServer, hydrantServer := setupMockCAServers(t)
digicertURL := digicertServer.URL
hydrantURL := hydrantServer.URL
t.Cleanup(func() {
digicertServer.Close()
hydrantServer.Close()
})
verifyNilFieldsForType := func(t *testing.T, ca *fleet.CertificateAuthority) {
if ca.Type != string(fleet.CATypeDigiCert) {
assert.Nil(t, ca.APIToken)
assert.Nil(t, ca.ProfileID)
assert.Nil(t, ca.CertificateCommonName)
assert.Nil(t, ca.CertificateUserPrincipalNames)
assert.Nil(t, ca.CertificateSeatID)
}
if ca.Type != string(fleet.CATypeHydrant) {
assert.Nil(t, ca.ClientID)
assert.Nil(t, ca.ClientSecret)
}
if ca.Type != string(fleet.CATypeCustomSCEPProxy) {
assert.Nil(t, ca.Challenge)
}
if ca.Type != string(fleet.CATypeNDESSCEPProxy) {
assert.Nil(t, ca.AdminURL)
}
if ca.Type != string(fleet.CATypeSmallstep) {
assert.Nil(t, ca.ChallengeURL)
}
// Since username and password is now shared for NDES and Smallstep
if ca.Type != string(fleet.CATypeNDESSCEPProxy) && ca.Type != string(fleet.CATypeSmallstep) {
assert.Nil(t, ca.Username)
assert.Nil(t, ca.Password)
}
}
createdCAs := []*fleet.CertificateAuthority{}
baseSetupForCATests := func() (*Service, context.Context) {
ds := new(mock.Store)
// Reset createdCAs before each test
createdCAs = []*fleet.CertificateAuthority{}
// Setup DS mocks
ds.NewCertificateAuthorityFunc = func(ctx context.Context, ca *fleet.CertificateAuthority) (*fleet.CertificateAuthority, error) {
ca.ID = uint(len(createdCAs) + 1) // nolint:gosec // Disable G115 since this is just a test
createdCAs = append(createdCAs, ca)
// NewCertificateAuthority would normally call NewActivity() after calling this
// however it is not easy to mock that method here and it will just panic, so
// instead we return a specific error from this datastore mock that tests should
// check for
return nil, errors.New("mock error to avoid NewActivity panic")
}
authorizer, err := authz.NewAuthorizer()
require.NoError(t, err)
svc := &Service{
logger: logging.NewLogfmtLogger(os.Stdout),
ds: ds,
authz: authorizer,
digiCertService: digicert.NewService(),
estService: est.NewService(),
scepConfigService: &scep_mock.SCEPConfigService{
ValidateSCEPURLFunc: func(_ context.Context, _ string) error { return nil },
ValidateNDESSCEPAdminURLFunc: func(_ context.Context, _ fleet.NDESSCEPProxyCA) error { return nil },
ValidateSmallstepChallengeURLFunc: func(_ context.Context, _ fleet.SmallstepSCEPProxyCA) error { return nil },
},
}
svc.config.Server.PrivateKey = "supersecret"
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
return svc, ctx
}
t.Run("Errors when no CA type is specified", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
createRequest := fleet.CertificateAuthorityPayload{}
createdCA, err := svc.NewCertificateAuthority(ctx, createRequest)
require.EqualError(t, err, "Couldn't add certificate authority. A certificate authority must be specified")
require.Nil(t, createdCA)
})
t.Run("Errors when multiple CA types are specified", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
createRequest := fleet.CertificateAuthorityPayload{
DigiCert: &fleet.DigiCertCA{},
Hydrant: &fleet.HydrantCA{},
}
createdCA, err := svc.NewCertificateAuthority(ctx, createRequest)
require.EqualError(t, err, "Couldn't add certificate authority. Only one certificate authority can be created at a time")
require.Nil(t, createdCA)
})
t.Run("Create DigiCert CA - Happy path", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
createDigicertRequest := fleet.CertificateAuthorityPayload{
DigiCert: &fleet.DigiCertCA{
Name: "DigicertWIFI",
URL: digicertURL,
APIToken: "digicert_api_token",
ProfileID: "digicert_profile_id",
CertificateCommonName: "digicert_common_name",
CertificateUserPrincipalNames: []string{"digicert_user_principal_name"},
CertificateSeatID: "digicert_seat_id",
},
}
_, err := svc.NewCertificateAuthority(ctx, createDigicertRequest)
require.EqualError(t, err, "mock error to avoid NewActivity panic")
require.Len(t, createdCAs, 1)
createdCA := createdCAs[0]
assert.Equal(t, createDigicertRequest.DigiCert.Name, *createdCA.Name)
assert.Equal(t, createDigicertRequest.DigiCert.URL, *createdCA.URL)
assert.Equal(t, string(fleet.CATypeDigiCert), createdCA.Type)
require.NotNil(t, createdCA.APIToken)
assert.Equal(t, createDigicertRequest.DigiCert.APIToken, *createdCA.APIToken)
require.NotNil(t, createdCA.ProfileID)
assert.Equal(t, createDigicertRequest.DigiCert.ProfileID, *createdCA.ProfileID)
require.NotNil(t, createdCA.CertificateCommonName)
assert.Equal(t, createDigicertRequest.DigiCert.CertificateCommonName, *createdCA.CertificateCommonName)
assert.ElementsMatch(t, createDigicertRequest.DigiCert.CertificateUserPrincipalNames, *createdCA.CertificateUserPrincipalNames)
require.NotNil(t, createdCA.CertificateSeatID)
assert.Equal(t, createDigicertRequest.DigiCert.CertificateSeatID, *createdCA.CertificateSeatID)
verifyNilFieldsForType(t, createdCA)
})
t.Run("Create DigiCert CA - Happy path with variables", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
createDigicertRequest := fleet.CertificateAuthorityPayload{
DigiCert: &fleet.DigiCertCA{
Name: "DigicertWIFI",
URL: digicertURL,
APIToken: "digicert_api_token",
ProfileID: "digicert_profile_id",
CertificateCommonName: "$FLEET_VAR_HOST_HARDWARE_SERIAL",
CertificateUserPrincipalNames: []string{"$FLEET_VAR_HOST_HARDWARE_SERIAL"},
CertificateSeatID: "$FLEET_VAR_HOST_HARDWARE_SERIAL",
},
}
_, err := svc.NewCertificateAuthority(ctx, createDigicertRequest)
require.EqualError(t, err, "mock error to avoid NewActivity panic")
require.Len(t, createdCAs, 1)
createdCA := createdCAs[0]
assert.Equal(t, createDigicertRequest.DigiCert.Name, *createdCA.Name)
assert.Equal(t, createDigicertRequest.DigiCert.URL, *createdCA.URL)
assert.Equal(t, string(fleet.CATypeDigiCert), createdCA.Type)
require.NotNil(t, createdCA.APIToken)
assert.Equal(t, createDigicertRequest.DigiCert.APIToken, *createdCA.APIToken)
require.NotNil(t, createdCA.ProfileID)
assert.Equal(t, createDigicertRequest.DigiCert.ProfileID, *createdCA.ProfileID)
require.NotNil(t, createdCA.CertificateCommonName)
assert.Equal(t, createDigicertRequest.DigiCert.CertificateCommonName, *createdCA.CertificateCommonName)
assert.ElementsMatch(t, createDigicertRequest.DigiCert.CertificateUserPrincipalNames, *createdCA.CertificateUserPrincipalNames)
require.NotNil(t, createdCA.CertificateSeatID)
assert.Equal(t, createDigicertRequest.DigiCert.CertificateSeatID, *createdCA.CertificateSeatID)
verifyNilFieldsForType(t, createdCA)
})
t.Run("Create DigiCert CA - Happy path with no UPNs", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
createDigicertRequest := fleet.CertificateAuthorityPayload{
DigiCert: &fleet.DigiCertCA{
Name: "DigicertWIFI",
URL: digicertURL,
APIToken: "digicert_api_token",
ProfileID: "digicert_profile_id",
CertificateCommonName: "digicert_common_name",
CertificateUserPrincipalNames: nil,
CertificateSeatID: "digicert_seat_id",
},
}
_, err := svc.NewCertificateAuthority(ctx, createDigicertRequest)
require.EqualError(t, err, "mock error to avoid NewActivity panic")
require.Len(t, createdCAs, 1)
createdCA := createdCAs[0]
assert.Equal(t, createDigicertRequest.DigiCert.Name, *createdCA.Name)
assert.Equal(t, createDigicertRequest.DigiCert.URL, *createdCA.URL)
assert.Equal(t, string(fleet.CATypeDigiCert), createdCA.Type)
require.NotNil(t, createdCA.APIToken)
assert.Equal(t, createDigicertRequest.DigiCert.APIToken, *createdCA.APIToken)
require.NotNil(t, createdCA.ProfileID)
assert.Equal(t, createDigicertRequest.DigiCert.ProfileID, *createdCA.ProfileID)
require.NotNil(t, createdCA.CertificateCommonName)
assert.Equal(t, createDigicertRequest.DigiCert.CertificateCommonName, *createdCA.CertificateCommonName)
assert.Nil(t, *createdCA.CertificateUserPrincipalNames)
require.NotNil(t, createdCA.CertificateSeatID)
assert.Equal(t, createDigicertRequest.DigiCert.CertificateSeatID, *createdCA.CertificateSeatID)
verifyNilFieldsForType(t, createdCA)
})
t.Run("Create Hydrant CA - Happy path", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
fmt.Println(hydrantURL)
createHydrantRequest := fleet.CertificateAuthorityPayload{
Hydrant: &fleet.HydrantCA{
Name: "HydrantWIFI",
URL: hydrantURL,
ClientID: "hydrant_client_id",
ClientSecret: "hydrant_client_secret",
},
}
_, err := svc.NewCertificateAuthority(ctx, createHydrantRequest)
require.EqualError(t, err, "mock error to avoid NewActivity panic")
require.Len(t, createdCAs, 1)
createdCA := createdCAs[0]
assert.Equal(t, createHydrantRequest.Hydrant.Name, *createdCA.Name)
assert.Equal(t, createHydrantRequest.Hydrant.URL, *createdCA.URL)
assert.Equal(t, string(fleet.CATypeHydrant), createdCA.Type)
require.NotNil(t, createdCA.ClientID)
assert.Equal(t, createHydrantRequest.Hydrant.ClientID, *createdCA.ClientID)
require.NotNil(t, createdCA.ClientSecret)
assert.Equal(t, createHydrantRequest.Hydrant.ClientSecret, *createdCA.ClientSecret)
verifyNilFieldsForType(t, createdCA)
})
t.Run("Create Custom SCEP CA - Happy path", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
createCustomSCEPRequest := fleet.CertificateAuthorityPayload{
CustomSCEPProxy: &fleet.CustomSCEPProxyCA{
Name: "CustomSCEPWIFI",
URL: "https://customscep.example.com",
Challenge: "challenge",
},
}
_, err := svc.NewCertificateAuthority(ctx, createCustomSCEPRequest)
require.EqualError(t, err, "mock error to avoid NewActivity panic")
require.Len(t, createdCAs, 1)
createdCA := createdCAs[0]
assert.Equal(t, createCustomSCEPRequest.CustomSCEPProxy.Name, *createdCA.Name)
assert.Equal(t, createCustomSCEPRequest.CustomSCEPProxy.URL, *createdCA.URL)
assert.Equal(t, string(fleet.CATypeCustomSCEPProxy), createdCA.Type)
require.NotNil(t, createdCA.Challenge)
assert.Equal(t, createCustomSCEPRequest.CustomSCEPProxy.Challenge, *createdCA.Challenge)
verifyNilFieldsForType(t, createdCA)
})
t.Run("Create NDES SCEP CA - Happy path", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
createNDESSCEPRequest := fleet.CertificateAuthorityPayload{
NDESSCEPProxy: &fleet.NDESSCEPProxyCA{
URL: "https://ndes.example.com",
AdminURL: "https://ndes-admin.example.com",
Username: "ndes_user",
Password: "ndes_password",
},
}
_, err := svc.NewCertificateAuthority(ctx, createNDESSCEPRequest)
require.EqualError(t, err, "mock error to avoid NewActivity panic")
require.Len(t, createdCAs, 1)
createdCA := createdCAs[0]
assert.Equal(t, "NDES", *createdCA.Name)
assert.Equal(t, createNDESSCEPRequest.NDESSCEPProxy.URL, *createdCA.URL)
assert.Equal(t, string(fleet.CATypeNDESSCEPProxy), createdCA.Type)
require.NotNil(t, createdCA.AdminURL)
assert.Equal(t, createNDESSCEPRequest.NDESSCEPProxy.AdminURL, *createdCA.AdminURL)
require.NotNil(t, createdCA.Username)
assert.Equal(t, createNDESSCEPRequest.NDESSCEPProxy.Username, *createdCA.Username)
require.NotNil(t, createdCA.Password)
assert.Equal(t, createNDESSCEPRequest.NDESSCEPProxy.Password, *createdCA.Password)
verifyNilFieldsForType(t, createdCA)
})
t.Run("Create Smallstep SCEP CA - Happy path", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
createSmallstepRequest := fleet.CertificateAuthorityPayload{
Smallstep: &fleet.SmallstepSCEPProxyCA{
Name: "SmallstepWIFI",
URL: "https://smallstep.example.com",
ChallengeURL: "https://smallstep.example.com/challenge",
Username: "smallstep_user",
Password: "smallstep_password",
},
}
_, err := svc.NewCertificateAuthority(ctx, createSmallstepRequest)
require.EqualError(t, err, "mock error to avoid NewActivity panic")
require.Len(t, createdCAs, 1)
createdCA := createdCAs[0]
assert.Equal(t, createSmallstepRequest.Smallstep.Name, *createdCA.Name)
assert.Equal(t, createSmallstepRequest.Smallstep.URL, *createdCA.URL)
assert.Equal(t, string(fleet.CATypeSmallstep), createdCA.Type)
require.NotNil(t, createdCA.ChallengeURL)
assert.Equal(t, createSmallstepRequest.Smallstep.ChallengeURL, *createdCA.ChallengeURL)
require.NotNil(t, createdCA.Username)
assert.Equal(t, createSmallstepRequest.Smallstep.Username, *createdCA.Username)
require.NotNil(t, createdCA.Password)
assert.Equal(t, createSmallstepRequest.Smallstep.Password, *createdCA.Password)
verifyNilFieldsForType(t, createdCA)
})
t.Run("Create DigiCert CA - Bad Name", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
createDigicertRequest := fleet.CertificateAuthorityPayload{
DigiCert: &fleet.DigiCertCA{
Name: "Digicert WIFI",
URL: digicertURL,
APIToken: "digicert_api_token",
ProfileID: "digicert_profile_id",
CertificateCommonName: "digicert_common_name",
CertificateUserPrincipalNames: []string{"digicert_user_principal_name"},
CertificateSeatID: "digicert_seat_id",
},
}
createdCA, err := svc.NewCertificateAuthority(ctx, createDigicertRequest)
require.ErrorContains(t, err, "Invalid characters in the \"name\" field")
require.Len(t, createdCAs, 0)
require.Nil(t, createdCA)
})
t.Run("Create DigiCert CA - empty CN", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
createDigicertRequest := fleet.CertificateAuthorityPayload{
DigiCert: &fleet.DigiCertCA{
Name: "Digicert WIFI",
URL: digicertURL,
APIToken: "digicert_api_token",
ProfileID: "digicert_profile_id",
CertificateCommonName: "",
CertificateUserPrincipalNames: []string{"digicert_user_principal_name"},
CertificateSeatID: "digicert_seat_id",
},
}
createdCA, err := svc.NewCertificateAuthority(ctx, createDigicertRequest)
require.ErrorContains(t, err, "Invalid characters in the \"name\" field.")
require.Len(t, createdCAs, 0)
require.Nil(t, createdCA)
})
t.Run("Create DigiCert CA - unsupported variable in CN", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
createDigicertRequest := fleet.CertificateAuthorityPayload{
DigiCert: &fleet.DigiCertCA{
Name: "DigicertWIFI",
URL: digicertURL,
APIToken: "digicert_api_token",
ProfileID: "digicert_profile_id",
CertificateCommonName: "$FLEET_VAR_BOZO",
CertificateUserPrincipalNames: []string{"digicert_user_principal_name"},
CertificateSeatID: "digicert_seat_id",
},
}
createdCA, err := svc.NewCertificateAuthority(ctx, createDigicertRequest)
require.ErrorContains(t, err, "FLEET_VAR_BOZO is not allowed in CA Common Name")
require.Len(t, createdCAs, 0)
require.Nil(t, createdCA)
})
// Digicert errors
t.Run("Create DigiCert CA - Bad URL format", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
createDigicertRequest := fleet.CertificateAuthorityPayload{
DigiCert: &fleet.DigiCertCA{
Name: "DigicertWIFI",
URL: "bozo",
APIToken: "digicert_api_token",
ProfileID: "digicert_profile_id",
CertificateCommonName: "digicert_common_name",
CertificateUserPrincipalNames: []string{"digicert_user_principal_name"},
CertificateSeatID: "digicert_seat_id",
},
}
createdCA, err := svc.NewCertificateAuthority(ctx, createDigicertRequest)
require.ErrorContains(t, err, "Invalid DigiCert URL")
require.Len(t, createdCAs, 0)
require.Nil(t, createdCA)
})
t.Run("Create DigiCert CA - Bad URL path", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
createDigicertRequest := fleet.CertificateAuthorityPayload{
DigiCert: &fleet.DigiCertCA{
Name: "DigicertWIFI",
URL: digicertURL + "/invalid",
APIToken: "digicert_api_token",
ProfileID: "digicert_profile_id",
CertificateCommonName: "digicert_common_name",
CertificateUserPrincipalNames: []string{"digicert_user_principal_name"},
CertificateSeatID: "digicert_seat_id",
},
}
createdCA, err := svc.NewCertificateAuthority(ctx, createDigicertRequest)
require.ErrorContains(t, err, "Could not verify DigiCert profile ID")
require.Len(t, createdCAs, 0)
require.Nil(t, createdCA)
})
t.Run("Create DigiCert CA - empty seat ID", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
createDigicertRequest := fleet.CertificateAuthorityPayload{
DigiCert: &fleet.DigiCertCA{
Name: "DigicertWIFI",
URL: digicertURL,
APIToken: "digicert_api_token",
ProfileID: "digicert_profile_id",
CertificateCommonName: "digicert_common_name",
CertificateUserPrincipalNames: []string{"digicert_user_principal_name"},
CertificateSeatID: "",
},
}
createdCA, err := svc.NewCertificateAuthority(ctx, createDigicertRequest)
require.ErrorContains(t, err, "CA Seat ID cannot be empty")
require.Len(t, createdCAs, 0)
require.Nil(t, createdCA)
})
t.Run("Create DigiCert CA - unsupported variable in seat id", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
createDigicertRequest := fleet.CertificateAuthorityPayload{
DigiCert: &fleet.DigiCertCA{
Name: "DigicertWIFI",
URL: digicertURL,
APIToken: "digicert_api_token",
ProfileID: "digicert_profile_id",
CertificateCommonName: "digicert_common_name",
CertificateUserPrincipalNames: []string{"digicert_user_principal_name"},
CertificateSeatID: "$FLEET_VAR_BOZO",
},
}
createdCA, err := svc.NewCertificateAuthority(ctx, createDigicertRequest)
require.ErrorContains(t, err, "FLEET_VAR_BOZO is not allowed in DigiCert Seat ID")
require.Len(t, createdCAs, 0)
require.Nil(t, createdCA)
})
t.Run("Create DigiCert CA - empty UPN", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
createDigicertRequest := fleet.CertificateAuthorityPayload{
DigiCert: &fleet.DigiCertCA{
Name: "DigicertWIFI",
URL: digicertURL,
APIToken: "digicert_api_token",
ProfileID: "digicert_profile_id",
CertificateCommonName: "digicert_common_name",
CertificateUserPrincipalNames: []string{""},
CertificateSeatID: "digicert_seat_id",
},
}
createdCA, err := svc.NewCertificateAuthority(ctx, createDigicertRequest)
require.ErrorContains(t, err, "DigiCert certificate_user_principal_name cannot be empty if specified")
require.Len(t, createdCAs, 0)
require.Nil(t, createdCA)
})
t.Run("Create DigiCert CA - unsupported variable in UPNs", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
createDigicertRequest := fleet.CertificateAuthorityPayload{
DigiCert: &fleet.DigiCertCA{
Name: "DigicertWIFI",
URL: digicertURL,
APIToken: "digicert_api_token",
ProfileID: "digicert_profile_id",
CertificateCommonName: "digicert_common_name",
CertificateUserPrincipalNames: []string{"$FLEET_VAR_BOZO"},
CertificateSeatID: "digicert_seat_id",
},
}
createdCA, err := svc.NewCertificateAuthority(ctx, createDigicertRequest)
require.ErrorContains(t, err, "FLEET_VAR_BOZO is not allowed in CA User Principal Name")
require.Len(t, createdCAs, 0)
require.Nil(t, createdCA)
})
t.Run("Create Hydrant CA - Bad name", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
createHydrantRequest := fleet.CertificateAuthorityPayload{
Hydrant: &fleet.HydrantCA{
Name: "Hydrant WIFI",
URL: "https://hydrant.example.com",
ClientID: "hydrant_client_id",
ClientSecret: "hydrant_client_secret",
},
}
createdCA, err := svc.NewCertificateAuthority(ctx, createHydrantRequest)
require.ErrorContains(t, err, "Invalid characters in the \"name\" field.")
require.Len(t, createdCAs, 0)
require.Nil(t, createdCA)
})
t.Run("Create Hydrant CA - Bad URL", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
createHydrantRequest := fleet.CertificateAuthorityPayload{
Hydrant: &fleet.HydrantCA{
Name: "HydrantWIFI",
URL: "bozo",
ClientID: "hydrant_client_id",
ClientSecret: "hydrant_client_secret",
},
}
createdCA, err := svc.NewCertificateAuthority(ctx, createHydrantRequest)
require.ErrorContains(t, err, "Invalid Hydrant URL.")
require.Len(t, createdCAs, 0)
require.Nil(t, createdCA)
})
t.Run("Create Hydrant CA - Bad URL but looks like a hydrant URL", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
createHydrantRequest := fleet.CertificateAuthorityPayload{
Hydrant: &fleet.HydrantCA{
Name: "HydrantWIFI",
URL: "https://hydrant.example.com/invalid",
ClientID: "hydrant_client_id",
ClientSecret: "hydrant_client_secret",
},
}
createdCA, err := svc.NewCertificateAuthority(ctx, createHydrantRequest)
require.ErrorContains(t, err, "Invalid Hydrant URL.")
require.Len(t, createdCAs, 0)
require.Nil(t, createdCA)
})
t.Run("Create Hydrant CA - missing client_id", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
createHydrantRequest := fleet.CertificateAuthorityPayload{
Hydrant: &fleet.HydrantCA{
Name: "HydrantWIFI",
URL: "https://hydrant.example.com/invalid",
ClientID: "",
ClientSecret: "hydrant_client_secret",
},
}
createdCA, err := svc.NewCertificateAuthority(ctx, createHydrantRequest)
require.ErrorContains(t, err, "Invalid Hydrant Client ID.")
require.Len(t, createdCAs, 0)
require.Nil(t, createdCA)
})
t.Run("Create Hydrant CA - missing client_secret", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
createHydrantRequest := fleet.CertificateAuthorityPayload{
Hydrant: &fleet.HydrantCA{
Name: "HydrantWIFI",
URL: "https://hydrant.example.com/invalid",
ClientID: "hydrant_client_id",
ClientSecret: "",
},
}
createdCA, err := svc.NewCertificateAuthority(ctx, createHydrantRequest)
require.ErrorContains(t, err, "Invalid Hydrant Client Secret.")
require.Len(t, createdCAs, 0)
require.Nil(t, createdCA)
})
t.Run("Create Custom SCEP CA - bad name", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
createCustomSCEPRequest := fleet.CertificateAuthorityPayload{
CustomSCEPProxy: &fleet.CustomSCEPProxyCA{
Name: "Custom SCEP WIFI",
URL: "https://customscep.example.com",
Challenge: "challenge",
},
}
createdCA, err := svc.NewCertificateAuthority(ctx, createCustomSCEPRequest)
require.ErrorContains(t, err, "Invalid characters in the \"name\" field.")
require.Len(t, createdCAs, 0)
require.Nil(t, createdCA)
})
t.Run("Create Custom SCEP CA - invalid URL format", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
createCustomSCEPRequest := fleet.CertificateAuthorityPayload{
CustomSCEPProxy: &fleet.CustomSCEPProxyCA{
Name: "CustomSCEPWIFI",
URL: "bozo",
Challenge: "challenge",
},
}
createdCA, err := svc.NewCertificateAuthority(ctx, createCustomSCEPRequest)
require.ErrorContains(t, err, "Invalid SCEP URL.")
require.Len(t, createdCAs, 0)
require.Nil(t, createdCA)
})
t.Run("Create Custom SCEP CA - missing challenge", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
createCustomSCEPRequest := fleet.CertificateAuthorityPayload{
CustomSCEPProxy: &fleet.CustomSCEPProxyCA{
Name: "CustomSCEPWIFI",
URL: "https://customscep.example.com",
Challenge: "",
},
}
createdCA, err := svc.NewCertificateAuthority(ctx, createCustomSCEPRequest)
require.ErrorContains(t, err, "Custom SCEP Proxy challenge cannot be empty")
require.Len(t, createdCAs, 0)
require.Nil(t, createdCA)
})
t.Run("Create Custom SCEP CA - bad SCEP URL", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
svc.scepConfigService = &scep_mock.SCEPConfigService{
ValidateSCEPURLFunc: func(_ context.Context, _ string) error { return errors.New("some error") },
}
createCustomSCEPRequest := fleet.CertificateAuthorityPayload{
CustomSCEPProxy: &fleet.CustomSCEPProxyCA{
Name: "CustomSCEPWIFI",
URL: "https://customscep.example.com",
Challenge: "challenge",
},
}
createdCA, err := svc.NewCertificateAuthority(ctx, createCustomSCEPRequest)
require.ErrorContains(t, err, "Invalid SCEP URL. Please correct and try again.")
require.Len(t, createdCAs, 0)
require.Nil(t, createdCA)
})
t.Run("Create NDES SCEP CA - bad URL format", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
createNDESSCEPRequest := fleet.CertificateAuthorityPayload{
NDESSCEPProxy: &fleet.NDESSCEPProxyCA{
URL: "bozo",
AdminURL: "https://ndes-admin.example.com",
Username: "ndes_user",
Password: "ndes_password",
},
}
createdCA, err := svc.NewCertificateAuthority(ctx, createNDESSCEPRequest)
require.ErrorContains(t, err, "Invalid NDES SCEP URL.")
require.Len(t, createdCAs, 0)
require.Nil(t, createdCA)
})
t.Run("Create NDES SCEP CA - bad SCEP URL", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
svc.scepConfigService = &scep_mock.SCEPConfigService{
ValidateSCEPURLFunc: func(_ context.Context, _ string) error { return errors.New("some error") },
}
createNDESSCEPRequest := fleet.CertificateAuthorityPayload{
NDESSCEPProxy: &fleet.NDESSCEPProxyCA{
URL: "https://ndes.example.com",
AdminURL: "https://ndes-admin.example.com",
Username: "ndes_user",
Password: "ndes_password",
},
}
createdCA, err := svc.NewCertificateAuthority(ctx, createNDESSCEPRequest)
require.ErrorContains(t, err, "Invalid SCEP URL.")
require.Len(t, createdCAs, 0)
require.Nil(t, createdCA)
})
t.Run("Create NDES SCEP CA - bad admin URL generic error", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
svc.scepConfigService = &scep_mock.SCEPConfigService{
ValidateSCEPURLFunc: func(_ context.Context, _ string) error { return nil },
ValidateNDESSCEPAdminURLFunc: func(_ context.Context, _ fleet.NDESSCEPProxyCA) error {
return errors.New("some error")
},
}
createNDESSCEPRequest := fleet.CertificateAuthorityPayload{
NDESSCEPProxy: &fleet.NDESSCEPProxyCA{
URL: "https://ndes.example.com",
AdminURL: "https://ndes-admin.example.com",
Username: "ndes_user",
Password: "ndes_password",
},
}
createdCA, err := svc.NewCertificateAuthority(ctx, createNDESSCEPRequest)
require.ErrorContains(t, err, "Invalid NDES SCEP admin URL or credentials.")
require.Len(t, createdCAs, 0)
require.Nil(t, createdCA)
})
t.Run("Create NDES SCEP CA - bad admin URL NDES Invalid error", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
svc.scepConfigService = &scep_mock.SCEPConfigService{
ValidateSCEPURLFunc: func(_ context.Context, _ string) error { return nil },
ValidateNDESSCEPAdminURLFunc: func(_ context.Context, _ fleet.NDESSCEPProxyCA) error {
return NewNDESInvalidError("some error")
},
}
createNDESSCEPRequest := fleet.CertificateAuthorityPayload{
NDESSCEPProxy: &fleet.NDESSCEPProxyCA{
URL: "https://ndes.example.com",
AdminURL: "https://ndes-admin.example.com",
Username: "ndes_user",
Password: "ndes_password",
},
}
createdCA, err := svc.NewCertificateAuthority(ctx, createNDESSCEPRequest)
require.ErrorContains(t, err, "Invalid NDES SCEP admin URL or credentials.")
require.Len(t, createdCAs, 0)
require.Nil(t, createdCA)
})
t.Run("Create NDES SCEP CA - bad admin URL with cache full error", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
svc.scepConfigService = &scep_mock.SCEPConfigService{
ValidateSCEPURLFunc: func(_ context.Context, _ string) error { return nil },
ValidateNDESSCEPAdminURLFunc: func(_ context.Context, _ fleet.NDESSCEPProxyCA) error {
return NewNDESPasswordCacheFullError("mock error")
},
}
createNDESSCEPRequest := fleet.CertificateAuthorityPayload{
NDESSCEPProxy: &fleet.NDESSCEPProxyCA{
URL: "https://ndes.example.com",
AdminURL: "https://ndes-admin.example.com",
Username: "ndes_user",
Password: "ndes_password",
},
}
createdCA, err := svc.NewCertificateAuthority(ctx, createNDESSCEPRequest)
require.ErrorContains(t, err, "The NDES password cache is full. Please increase the number of cached passwords in NDES and try again.")
require.Len(t, createdCAs, 0)
require.Nil(t, createdCA)
})
t.Run("Create NDES SCEP CA - bad admin URL with insufficient permissions error", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
svc.scepConfigService = &scep_mock.SCEPConfigService{
ValidateSCEPURLFunc: func(_ context.Context, _ string) error { return nil },
ValidateNDESSCEPAdminURLFunc: func(_ context.Context, _ fleet.NDESSCEPProxyCA) error {
return NewNDESInsufficientPermissionsError("mock error")
},
}
createNDESSCEPRequest := fleet.CertificateAuthorityPayload{
NDESSCEPProxy: &fleet.NDESSCEPProxyCA{
URL: "https://ndes.example.com",
AdminURL: "https://ndes-admin.example.com",
Username: "ndes_user",
Password: "ndes_password",
},
}
createdCA, err := svc.NewCertificateAuthority(ctx, createNDESSCEPRequest)
require.ErrorContains(t, err, "Insufficient permissions for NDES SCEP admin URL. Please correct and try again.")
require.Len(t, createdCAs, 0)
require.Nil(t, createdCA)
})
t.Run("Create Smallstep SCEP CA - bad name", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
createSmallstepRequest := fleet.CertificateAuthorityPayload{
Smallstep: &fleet.SmallstepSCEPProxyCA{
Name: "Smallstep SCEP WIFI",
URL: "https://smallstep.example.com",
ChallengeURL: "https://smallstep.example.com/challenge",
Username: "smallstep_user",
Password: "smallstep_password",
},
}
createdCA, err := svc.NewCertificateAuthority(ctx, createSmallstepRequest)
require.ErrorContains(t, err, "Invalid characters in the \"name\" field.")
require.Len(t, createdCAs, 0)
require.Nil(t, createdCA)
})
t.Run("Create Smallstep SCEP CA - invalid URL format", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
createSmallstepRequest := fleet.CertificateAuthorityPayload{
Smallstep: &fleet.SmallstepSCEPProxyCA{
Name: "SmallstepSCEPWIFI",
URL: "bozo",
ChallengeURL: "https://smallstep.example.com/challenge",
Username: "smallstep_user",
Password: "smallstep_password",
},
}
createdCA, err := svc.NewCertificateAuthority(ctx, createSmallstepRequest)
require.ErrorContains(t, err, "Invalid Smallstep SCEP URL.")
require.Len(t, createdCAs, 0)
require.Nil(t, createdCA)
})
t.Run("Create Smallstep SCEP CA - empty username", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
createSmallstepRequest := fleet.CertificateAuthorityPayload{
Smallstep: &fleet.SmallstepSCEPProxyCA{
Name: "SmallstepSCEPWIFI",
URL: "https://smallstep.example.com",
ChallengeURL: "https://smallstep.example.com/challenge",
Username: "",
Password: "smallstep_password",
},
}
createdCA, err := svc.NewCertificateAuthority(ctx, createSmallstepRequest)
require.ErrorContains(t, err, "Smallstep username cannot be empty")
require.Len(t, createdCAs, 0)
require.Nil(t, createdCA)
})
t.Run("Create Smallstep SCEP CA - empty password", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
createSmallstepRequest := fleet.CertificateAuthorityPayload{
Smallstep: &fleet.SmallstepSCEPProxyCA{
Name: "SmallstepSCEPWIFI",
URL: "https://smallstep.example.com",
ChallengeURL: "https://smallstep.example.com/challenge",
Username: "smallstep_user",
Password: "",
},
}
createdCA, err := svc.NewCertificateAuthority(ctx, createSmallstepRequest)
require.ErrorContains(t, err, "Smallstep password cannot be empty")
require.Len(t, createdCAs, 0)
require.Nil(t, createdCA)
})
t.Run("Create Smallstep SCEP CA - masked password", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
createSmallstepRequest := fleet.CertificateAuthorityPayload{
Smallstep: &fleet.SmallstepSCEPProxyCA{
Name: "SmallstepSCEPWIFI",
URL: "https://smallstep.example.com",
ChallengeURL: "https://smallstep.example.com/challenge",
Username: "smallstep_user",
Password: fleet.MaskedPassword,
},
}
createdCA, err := svc.NewCertificateAuthority(ctx, createSmallstepRequest)
require.ErrorContains(t, err, "Smallstep password cannot be empty")
require.Len(t, createdCAs, 0)
require.Nil(t, createdCA)
})
t.Run("Create Smallstep SCEP CA - invalid SCEP URL", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
svc.scepConfigService = &scep_mock.SCEPConfigService{
ValidateSCEPURLFunc: func(_ context.Context, _ string) error { return errors.New("some error") },
}
createSmallstepRequest := fleet.CertificateAuthorityPayload{
Smallstep: &fleet.SmallstepSCEPProxyCA{
Name: "SmallstepSCEPWIFI",
URL: "https://smallstep.example.com",
ChallengeURL: "https://smallstep.example.com/challenge",
Username: "smallstep_user",
Password: "smallstep_password",
},
}
createdCA, err := svc.NewCertificateAuthority(ctx, createSmallstepRequest)
require.ErrorContains(t, err, "Invalid SCEP URL. Please correct and try again.")
require.Len(t, createdCAs, 0)
require.Nil(t, createdCA)
})
t.Run("Create Smallstep SCEP CA - invalid challenge validation", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
svc.scepConfigService = &scep_mock.SCEPConfigService{
ValidateSCEPURLFunc: func(_ context.Context, _ string) error { return nil },
ValidateSmallstepChallengeURLFunc: func(_ context.Context, _ fleet.SmallstepSCEPProxyCA) error {
return errors.New("some error")
},
}
createSmallstepRequest := fleet.CertificateAuthorityPayload{
Smallstep: &fleet.SmallstepSCEPProxyCA{
Name: "SmallstepSCEPWIFI",
URL: "https://smallstep.example.com",
ChallengeURL: "bozo",
Username: "smallstep_user",
Password: "smallstep_password",
},
}
createdCA, err := svc.NewCertificateAuthority(ctx, createSmallstepRequest)
require.ErrorContains(t, err, "Invalid challenge URL or credentials.")
require.Len(t, createdCAs, 0)
require.Nil(t, createdCA)
})
}
func TestUpdatingCertificateAuthorities(t *testing.T) {
t.Parallel()
digicertServer, hydrantServer := setupMockCAServers(t)
digicertURL := digicertServer.URL
hydrantURL := hydrantServer.URL
t.Cleanup(func() {
digicertServer.Close()
hydrantServer.Close()
})
digicertID := uint(1)
hydrantID := uint(2)
scepID := uint(3)
ndesID := uint(4)
smallstepID := uint(5)
createdCAs := []*fleet.CertificateAuthority{}
baseSetupForCATests := func() (*Service, context.Context) {
ds := new(mock.Store)
// Reset createdCAs before each test
createdCAs = []*fleet.CertificateAuthority{}
// Setup CA's mocks
digicertCA := &fleet.CertificateAuthority{
ID: digicertID,
Name: ptr.String("Test Digicert CA"),
URL: ptr.String("https://digicert1.example.com"),
Type: string(fleet.CATypeDigiCert),
APIToken: ptr.String("test-api-token"),
ProfileID: ptr.String("test-profile-id"),
CertificateCommonName: ptr.String("test-common-name $FLEET_VAR_HOST_HARDWARE_SERIAL"),
CertificateUserPrincipalNames: &[]string{"test-upn $FLEET_VAR_HOST_HARDWARE_SERIAL"},
CertificateSeatID: ptr.String("test-seat-id"),
}
hydrantCA := &fleet.CertificateAuthority{
ID: hydrantID,
Name: ptr.String("Hydrant CA"),
URL: ptr.String("https://hydrant1.example.com"),
Type: string(fleet.CATypeHydrant),
ClientID: ptr.String("hydrant-client-id"),
ClientSecret: ptr.String("hydrant-client-secret"),
}
// Custom SCEP CAs
customSCEPCA := &fleet.CertificateAuthority{
ID: scepID,
Name: ptr.String("Custom SCEP CA"),
URL: ptr.String("https://custom-scep.example.com"),
Type: string(fleet.CATypeCustomSCEPProxy),
Challenge: ptr.String("custom-scep-challenge"),
}
// NDES CA
ndesCA := &fleet.CertificateAuthority{
ID: ndesID,
Name: ptr.String("NDES"),
URL: ptr.String("https://ndes.example.com"),
AdminURL: ptr.String("https://ndes-admin.example.com"),
Type: string(fleet.CATypeNDESSCEPProxy),
Username: ptr.String("ndes-username"),
Password: ptr.String("ndes-password"),
}
smallstepCA := &fleet.CertificateAuthority{
ID: smallstepID,
Name: ptr.String("Smallstep CA"),
URL: ptr.String("https://smallstep.example.com"),
Type: string(fleet.CATypeSmallstep),
ChallengeURL: ptr.String("https://smallstep.example.com/challenge"),
Username: ptr.String("smallstep-username"),
Password: ptr.String("smallstep-password"),
}
createdCAs = append(createdCAs, digicertCA, hydrantCA, customSCEPCA, ndesCA, smallstepCA)
ds.GetCertificateAuthorityByIDFunc = func(ctx context.Context, id uint, includeSecrets bool) (*fleet.CertificateAuthority, error) {
for _, ca := range createdCAs {
if ca.ID == id {
return ca, nil
}
}
return nil, common_mysql.NotFound("get ca for update")
}
ds.UpdateCertificateAuthorityByIDFunc = func(ctx context.Context, certificateAuthorityID uint, updatedCA *fleet.CertificateAuthority) error {
for index, ca := range createdCAs {
if ca.ID != certificateAuthorityID {
continue
}
createdCAs[index] = updatedCA
// UpdateCertificateAuthority would normally call NewActivity() after calling this
// however it is not easy to mock that method here and it will just panic, so
// instead we return a specific error from this datastore mock that tests should
// check for
return errors.New("mock error to avoid NewActivity panic")
}
return common_mysql.NotFound("update") // Did not find CA in list of created CAs
}
authorizer, err := authz.NewAuthorizer()
require.NoError(t, err)
svc := &Service{
logger: logging.NewLogfmtLogger(os.Stdout),
ds: ds,
authz: authorizer,
digiCertService: digicert.NewService(),
estService: est.NewService(),
scepConfigService: &scep_mock.SCEPConfigService{
ValidateSCEPURLFunc: func(_ context.Context, _ string) error { return nil },
ValidateNDESSCEPAdminURLFunc: func(_ context.Context, _ fleet.NDESSCEPProxyCA) error { return nil },
ValidateSmallstepChallengeURLFunc: func(_ context.Context, _ fleet.SmallstepSCEPProxyCA) error { return nil },
},
}
svc.config.Server.PrivateKey = "supersecret"
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
return svc, ctx
}
t.Run("Errors on empty payload", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
err := svc.UpdateCertificateAuthority(ctx, digicertID, fleet.CertificateAuthorityUpdatePayload{})
require.EqualError(t, err, "Couldn't edit certificate authority. A certificate authority must be specified")
})
t.Run("Errors on multiple payloads", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
DigiCertCAUpdatePayload: &fleet.DigiCertCAUpdatePayload{
APIToken: ptr.String("updated-api-token"),
},
HydrantCAUpdatePayload: &fleet.HydrantCAUpdatePayload{
ClientSecret: ptr.String("updated-secret"),
},
}
err := svc.UpdateCertificateAuthority(ctx, digicertID, payload)
require.EqualError(t, err, "Couldn't edit certificate authority. Only one certificate authority can be edited at a time")
})
t.Run("Errors if no certificate authority is found", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
DigiCertCAUpdatePayload: &fleet.DigiCertCAUpdatePayload{},
}
err := svc.UpdateCertificateAuthority(ctx, 999, payload)
require.EqualError(t, err, "Couldn't edit certificate authority. Certificate authority with ID 999 does not exist.")
})
t.Run("Errors on empty inner update payload", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payloadMap := map[uint]fleet.CertificateAuthorityUpdatePayload{
digicertID: {
DigiCertCAUpdatePayload: &fleet.DigiCertCAUpdatePayload{},
},
hydrantID: {
HydrantCAUpdatePayload: &fleet.HydrantCAUpdatePayload{},
},
scepID: {
CustomSCEPProxyCAUpdatePayload: &fleet.CustomSCEPProxyCAUpdatePayload{},
},
ndesID: {
NDESSCEPProxyCAUpdatePayload: &fleet.NDESSCEPProxyCAUpdatePayload{},
},
smallstepID: {
SmallstepSCEPProxyCAUpdatePayload: &fleet.SmallstepSCEPProxyCAUpdatePayload{},
},
}
for id, payload := range payloadMap {
err := svc.UpdateCertificateAuthority(ctx, id, payload)
require.Contains(t, err.Error(), "update payload is empty")
}
})
t.Run("Errors on wrong existing CA type", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
HydrantCAUpdatePayload: &fleet.HydrantCAUpdatePayload{
ClientSecret: ptr.String("updated-secret"),
},
}
err := svc.UpdateCertificateAuthority(ctx, digicertID, payload)
require.EqualError(t, err, "Couldn't edit certificate authority. The certificate authority types must be the same.")
})
t.Run("Digicert", func(t *testing.T) {
t.Run("Full update succeeds", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
upns := []string{"updated-upns"}
payload := fleet.CertificateAuthorityUpdatePayload{
DigiCertCAUpdatePayload: &fleet.DigiCertCAUpdatePayload{
Name: ptr.String("Updated_Digicert"),
URL: &digicertURL,
APIToken: ptr.String("updated-api-token"),
ProfileID: ptr.String("profile-id"),
CertificateCommonName: ptr.String("updated-cn"),
CertificateSeatID: ptr.String("updated-seat-id"),
CertificateUserPrincipalNames: &upns,
},
}
err := svc.UpdateCertificateAuthority(ctx, digicertID, payload)
require.EqualError(t, err, "mock error to avoid NewActivity panic")
})
t.Run("Allows variable for certificate related fields", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
DigiCertCAUpdatePayload: &fleet.DigiCertCAUpdatePayload{
CertificateCommonName: ptr.String("$FLEET_VAR_HOST_HARDWARE_SERIAL"),
CertificateUserPrincipalNames: &[]string{"$FLEET_VAR_HOST_HARDWARE_SERIAL"},
CertificateSeatID: ptr.String("$FLEET_VAR_HOST_HARDWARE_SERIAL"),
},
}
err := svc.UpdateCertificateAuthority(ctx, digicertID, payload)
require.Error(t, err, "mock error to avoid NewActivity panic")
})
t.Run("Fails updating URL if no api token", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
DigiCertCAUpdatePayload: &fleet.DigiCertCAUpdatePayload{
URL: &digicertURL,
},
}
err := svc.UpdateCertificateAuthority(ctx, digicertID, payload)
require.EqualError(t, err, "Couldn't edit certificate authority. \"api_token\" must be set when modifying \"url\", or \"profile_id\" of an existing certificate authority: Test Digicert CA")
})
t.Run("Fails updating profile ID if no api token", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
DigiCertCAUpdatePayload: &fleet.DigiCertCAUpdatePayload{
ProfileID: ptr.String("fake-profile-id"),
},
}
err := svc.UpdateCertificateAuthority(ctx, digicertID, payload)
require.EqualError(t, err, "Couldn't edit certificate authority. \"api_token\" must be set when modifying \"url\", or \"profile_id\" of an existing certificate authority: Test Digicert CA")
})
t.Run("Invalid URL Format", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
DigiCertCAUpdatePayload: &fleet.DigiCertCAUpdatePayload{
URL: ptr.String("bozo"),
APIToken: ptr.String("secret-token"),
},
}
err := svc.UpdateCertificateAuthority(ctx, digicertID, payload)
require.EqualError(t, err, "validation failed: url Couldn't edit certificate authority. Invalid DigiCert URL. Please correct and try again.")
})
t.Run("Bad URL Path", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
DigiCertCAUpdatePayload: &fleet.DigiCertCAUpdatePayload{
URL: ptr.String(digicertURL + "/invalid"),
APIToken: ptr.String("secret-token"),
},
}
err := svc.UpdateCertificateAuthority(ctx, digicertID, payload)
require.ErrorContains(t, err, "Couldn't edit certificate authority. Could not verify DigiCert URL: unexpected DigiCert status code: 400. Please correct and try again.")
})
t.Run("Empty CN", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
DigiCertCAUpdatePayload: &fleet.DigiCertCAUpdatePayload{
CertificateCommonName: ptr.String(""),
},
}
err := svc.UpdateCertificateAuthority(ctx, digicertID, payload)
require.EqualError(t, err, "validation failed: certificate_common_name Couldn't edit certificate authority. CA Common Name (CN) cannot be empty")
})
t.Run("Unsupported variable in CN", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
DigiCertCAUpdatePayload: &fleet.DigiCertCAUpdatePayload{
CertificateCommonName: ptr.String("$FLEET_VAR_BOZO"),
},
}
err := svc.UpdateCertificateAuthority(ctx, digicertID, payload)
require.EqualError(t, err, "validation failed: certificate_common_name Couldn't edit certificate authority. FLEET_VAR_BOZO is not allowed in CA Common Name (CN)")
})
t.Run("Empty Seat ID", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
DigiCertCAUpdatePayload: &fleet.DigiCertCAUpdatePayload{
CertificateSeatID: ptr.String(""),
},
}
err := svc.UpdateCertificateAuthority(ctx, digicertID, payload)
require.EqualError(t, err, "validation failed: certificate_seat_id Couldn't edit certificate authority. CA Seat ID cannot be empty")
})
t.Run("Unsupported variable in Seat ID", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
DigiCertCAUpdatePayload: &fleet.DigiCertCAUpdatePayload{
CertificateSeatID: ptr.String("$FLEET_VAR_BOZO"),
},
}
err := svc.UpdateCertificateAuthority(ctx, digicertID, payload)
require.EqualError(t, err, "validation failed: certificate_seat_id Couldn't edit certificate authority. FLEET_VAR_BOZO is not allowed in DigiCert Seat ID")
})
t.Run("Empty UPN", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
DigiCertCAUpdatePayload: &fleet.DigiCertCAUpdatePayload{
CertificateUserPrincipalNames: &[]string{""},
},
}
err := svc.UpdateCertificateAuthority(ctx, digicertID, payload)
require.EqualError(t, err, "validation failed: certificate_user_principal_names Couldn't edit certificate authority. DigiCert certificate_user_principal_name cannot be empty if specified")
})
t.Run("Unsupported variable in UPN", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
DigiCertCAUpdatePayload: &fleet.DigiCertCAUpdatePayload{
CertificateUserPrincipalNames: &[]string{"$FLEET_VAR_BOZO"},
},
}
err := svc.UpdateCertificateAuthority(ctx, digicertID, payload)
require.EqualError(t, err, "validation failed: certificate_user_principal_names Couldn't edit certificate authority. FLEET_VAR_BOZO is not allowed in CA User Principal Name")
})
})
t.Run("Hydrant", func(t *testing.T) {
t.Run("Full update succeeds", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
HydrantCAUpdatePayload: &fleet.HydrantCAUpdatePayload{
Name: ptr.String("Updated_Hydrant"),
URL: &hydrantURL,
ClientID: ptr.String("updated-id"),
ClientSecret: ptr.String("updated-secret"),
},
}
err := svc.UpdateCertificateAuthority(ctx, hydrantID, payload)
require.EqualError(t, err, "mock error to avoid NewActivity panic")
})
t.Run("Bad name", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
HydrantCAUpdatePayload: &fleet.HydrantCAUpdatePayload{
Name: ptr.String("Updated Hydrant"),
URL: &hydrantURL,
ClientID: ptr.String("updated-id"),
ClientSecret: ptr.String("updated-secret"),
},
}
err := svc.UpdateCertificateAuthority(ctx, hydrantID, payload)
require.EqualError(t, err, "validation failed: name Couldn't edit certificate authority. Invalid characters in the \"name\" field. Only letters, numbers and underscores allowed.")
})
t.Run("Invalid URL format", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
HydrantCAUpdatePayload: &fleet.HydrantCAUpdatePayload{
Name: ptr.String("UpdatedHydrant"),
URL: ptr.String("bozo"),
ClientID: ptr.String("updated-id"),
ClientSecret: ptr.String("updated-secret"),
},
}
err := svc.UpdateCertificateAuthority(ctx, hydrantID, payload)
require.EqualError(t, err, "validation failed: url Couldn't edit certificate authority. Invalid Hydrant URL. Please correct and try again.")
})
t.Run("Bad URL", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
HydrantCAUpdatePayload: &fleet.HydrantCAUpdatePayload{
Name: ptr.String("UpdatedHydrant"),
URL: ptr.String("https://hydrant.example.com/invalid"),
ClientID: ptr.String("updated-id"),
ClientSecret: ptr.String("updated-secret"),
},
}
err := svc.UpdateCertificateAuthority(ctx, hydrantID, payload)
require.EqualError(t, err, "Couldn't edit certificate authority. Invalid Hydrant URL. Please correct and try again.")
})
t.Run("Empty ClientID", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
HydrantCAUpdatePayload: &fleet.HydrantCAUpdatePayload{
ClientID: ptr.String(""),
},
}
err := svc.UpdateCertificateAuthority(ctx, hydrantID, payload)
require.EqualError(t, err, "Couldn't edit certificate authority. Invalid Hydrant Client ID. Please correct and try again.")
})
t.Run("Empty ClientSecret", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
HydrantCAUpdatePayload: &fleet.HydrantCAUpdatePayload{
ClientSecret: ptr.String(""),
},
}
err := svc.UpdateCertificateAuthority(ctx, hydrantID, payload)
require.EqualError(t, err, "Couldn't edit certificate authority. Invalid Hydrant Client Secret. Please correct and try again.")
})
})
t.Run("Custom SCEP", func(t *testing.T) {
t.Run("Full update succeeds", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
CustomSCEPProxyCAUpdatePayload: &fleet.CustomSCEPProxyCAUpdatePayload{
Name: ptr.String("Updated_Scep"),
URL: ptr.String("https://customscep.example.com"),
Challenge: ptr.String("updated-challenge"),
},
}
err := svc.UpdateCertificateAuthority(ctx, scepID, payload)
require.EqualError(t, err, "mock error to avoid NewActivity panic")
})
t.Run("Bad name", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
CustomSCEPProxyCAUpdatePayload: &fleet.CustomSCEPProxyCAUpdatePayload{
Name: ptr.String("Updated SCEP"),
},
}
err := svc.UpdateCertificateAuthority(ctx, scepID, payload)
require.EqualError(t, err, "validation failed: name Couldn't edit certificate authority. Invalid characters in the \"name\" field. Only letters, numbers and underscores allowed.")
})
t.Run("Invalid URL format", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
CustomSCEPProxyCAUpdatePayload: &fleet.CustomSCEPProxyCAUpdatePayload{
URL: ptr.String("bozo"),
Challenge: ptr.String("challenge"),
},
}
err := svc.UpdateCertificateAuthority(ctx, scepID, payload)
require.EqualError(t, err, "validation failed: url Couldn't edit certificate authority. Invalid SCEP URL. Please correct and try again.")
})
t.Run("Requires challenge when updating URL", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
CustomSCEPProxyCAUpdatePayload: &fleet.CustomSCEPProxyCAUpdatePayload{
URL: ptr.String("https://customscep.localhost.com"),
},
}
err := svc.UpdateCertificateAuthority(ctx, scepID, payload)
require.EqualError(t, err, "Couldn't edit certificate authority. \"challenge\" must be set when modifying \"url\" of an existing certificate authority: Custom SCEP CA")
})
t.Run("Bad SCEP URL", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
svc.scepConfigService = &scep_mock.SCEPConfigService{
ValidateSCEPURLFunc: func(_ context.Context, _ string) error { return errors.New("some error") },
}
payload := fleet.CertificateAuthorityUpdatePayload{
CustomSCEPProxyCAUpdatePayload: &fleet.CustomSCEPProxyCAUpdatePayload{
URL: ptr.String("https://customscep.localhost.com"),
Challenge: ptr.String("updated-challenge"),
},
}
err := svc.UpdateCertificateAuthority(ctx, scepID, payload)
require.EqualError(t, err, "Couldn't edit certificate authority. Invalid SCEP URL. Please correct and try again.")
})
})
t.Run("NDES SCEP", func(t *testing.T) {
t.Run("Full update succeeds", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
NDESSCEPProxyCAUpdatePayload: &fleet.NDESSCEPProxyCAUpdatePayload{
URL: ptr.String("https://ndes.example.com"),
AdminURL: ptr.String("https://ndes-admin.example.com"),
Username: ptr.String("ndes_user"),
Password: ptr.String("ndes_password"),
},
}
err := svc.UpdateCertificateAuthority(ctx, ndesID, payload)
require.EqualError(t, err, "mock error to avoid NewActivity panic")
})
t.Run("Invalid URL format", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
NDESSCEPProxyCAUpdatePayload: &fleet.NDESSCEPProxyCAUpdatePayload{
URL: ptr.String("bozo"),
Password: ptr.String("updated-pasword"),
},
}
err := svc.UpdateCertificateAuthority(ctx, ndesID, payload)
require.EqualError(t, err, "validation failed: url Couldn't edit certificate authority. Invalid NDES SCEP URL. Please correct and try again.")
})
t.Run("Bad SCEP URL", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
svc.scepConfigService = &scep_mock.SCEPConfigService{
ValidateSCEPURLFunc: func(_ context.Context, _ string) error { return errors.New("some error") },
}
payload := fleet.CertificateAuthorityUpdatePayload{
NDESSCEPProxyCAUpdatePayload: &fleet.NDESSCEPProxyCAUpdatePayload{
URL: ptr.String("https://ndes.example.com"),
Password: ptr.String("updated-pasword"),
},
}
err := svc.UpdateCertificateAuthority(ctx, ndesID, payload)
require.EqualError(t, err, "Couldn't edit certificate authority. Invalid SCEP URL. Please correct and try again.")
})
t.Run("Missing password", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
NDESSCEPProxyCAUpdatePayload: &fleet.NDESSCEPProxyCAUpdatePayload{
URL: ptr.String("https://ndes.example.com"),
},
}
err := svc.UpdateCertificateAuthority(ctx, ndesID, payload)
require.EqualError(t, err, "Couldn't edit certificate authority. \"password\" must be set when modifying an existing certificate authority: NDES")
})
t.Run("Bad admin URL generic error", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
svc.scepConfigService = &scep_mock.SCEPConfigService{
ValidateNDESSCEPAdminURLFunc: func(_ context.Context, _ fleet.NDESSCEPProxyCA) error {
return errors.New("some error")
},
}
payload := fleet.CertificateAuthorityUpdatePayload{
NDESSCEPProxyCAUpdatePayload: &fleet.NDESSCEPProxyCAUpdatePayload{
AdminURL: ptr.String("https://ndes.example.com"),
Password: ptr.String("updated-password"),
},
}
err := svc.UpdateCertificateAuthority(ctx, ndesID, payload)
require.EqualError(t, err, "Couldn't edit certificate authority. Invalid NDES SCEP admin URL or credentials. Please correct and try again.")
})
t.Run("Bad admin URL NDES Invalid error", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
svc.scepConfigService = &scep_mock.SCEPConfigService{
ValidateNDESSCEPAdminURLFunc: func(_ context.Context, _ fleet.NDESSCEPProxyCA) error {
return NewNDESInvalidError("some error")
},
}
payload := fleet.CertificateAuthorityUpdatePayload{
NDESSCEPProxyCAUpdatePayload: &fleet.NDESSCEPProxyCAUpdatePayload{
AdminURL: ptr.String("https://ndes.example.com"),
Password: ptr.String("updated-password"),
},
}
err := svc.UpdateCertificateAuthority(ctx, ndesID, payload)
require.EqualError(t, err, "Couldn't edit certificate authority. Invalid NDES SCEP admin URL or credentials. Please correct and try again.")
})
t.Run("Bad admin URL NDES Password cache full error", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
svc.scepConfigService = &scep_mock.SCEPConfigService{
ValidateNDESSCEPAdminURLFunc: func(_ context.Context, _ fleet.NDESSCEPProxyCA) error {
return NewNDESPasswordCacheFullError("some error")
},
}
payload := fleet.CertificateAuthorityUpdatePayload{
NDESSCEPProxyCAUpdatePayload: &fleet.NDESSCEPProxyCAUpdatePayload{
AdminURL: ptr.String("https://ndes.example.com"),
Password: ptr.String("updated-password"),
},
}
err := svc.UpdateCertificateAuthority(ctx, ndesID, payload)
require.EqualError(t, err, "Couldn't edit certificate authority. The NDES password cache is full. Please increase the number of cached passwords in NDES and try again.")
})
t.Run("Bad admin URL NDES Insufficient permissions", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
svc.scepConfigService = &scep_mock.SCEPConfigService{
ValidateNDESSCEPAdminURLFunc: func(_ context.Context, _ fleet.NDESSCEPProxyCA) error {
return NewNDESInsufficientPermissionsError("some error")
},
}
payload := fleet.CertificateAuthorityUpdatePayload{
NDESSCEPProxyCAUpdatePayload: &fleet.NDESSCEPProxyCAUpdatePayload{
AdminURL: ptr.String("https://ndes.example.com"),
Password: ptr.String("updated-password"),
},
}
err := svc.UpdateCertificateAuthority(ctx, ndesID, payload)
require.EqualError(t, err, "Couldn't edit certificate authority. Insufficient permissions for NDES SCEP admin URL. Please correct and try again.")
})
})
t.Run("Smallstep SCEP", func(t *testing.T) {
t.Run("Full update succeeds", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
SmallstepSCEPProxyCAUpdatePayload: &fleet.SmallstepSCEPProxyCAUpdatePayload{
Name: ptr.String("Updated_Smallstep"),
URL: ptr.String("https://smallstep.example.com"),
ChallengeURL: ptr.String("https://smallstep.example.com/challenge"),
Username: ptr.String("smallstep_user"),
Password: ptr.String("smallstep_password"),
},
}
err := svc.UpdateCertificateAuthority(ctx, smallstepID, payload)
require.EqualError(t, err, "mock error to avoid NewActivity panic")
})
t.Run("Bad name", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
SmallstepSCEPProxyCAUpdatePayload: &fleet.SmallstepSCEPProxyCAUpdatePayload{
Name: ptr.String("Updated Smallstep"),
},
}
err := svc.UpdateCertificateAuthority(ctx, smallstepID, payload)
require.EqualError(t, err, "validation failed: name Couldn't edit certificate authority. Invalid characters in the \"name\" field. Only letters, numbers and underscores allowed.")
})
t.Run("Invalid URL format", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
SmallstepSCEPProxyCAUpdatePayload: &fleet.SmallstepSCEPProxyCAUpdatePayload{
URL: ptr.String("bozo"),
ChallengeURL: ptr.String("https://smallstep.example.com/challenge"),
Username: ptr.String("updated-username"),
Password: ptr.String("updated-password"),
},
}
err := svc.UpdateCertificateAuthority(ctx, smallstepID, payload)
require.EqualError(t, err, "validation failed: url Couldn't edit certificate authority. Invalid SCEP URL. Please correct and try again.")
})
t.Run("Invalid Challenge URL format", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
SmallstepSCEPProxyCAUpdatePayload: &fleet.SmallstepSCEPProxyCAUpdatePayload{
URL: ptr.String("https://smallstep.example.com"),
ChallengeURL: ptr.String("bozo"),
Username: ptr.String("updated-username"),
Password: ptr.String("updated-password"),
},
}
err := svc.UpdateCertificateAuthority(ctx, smallstepID, payload)
require.EqualError(t, err, "validation failed: url Couldn't edit certificate authority. Invalid Challenge URL. Please correct and try again.")
})
t.Run("Bad Smallstep SCEP URL", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
svc.scepConfigService = &scep_mock.SCEPConfigService{
ValidateSCEPURLFunc: func(_ context.Context, _ string) error { return nil },
ValidateSmallstepChallengeURLFunc: func(_ context.Context, _ fleet.SmallstepSCEPProxyCA) error {
return errors.New("some error")
},
}
payload := fleet.CertificateAuthorityUpdatePayload{
SmallstepSCEPProxyCAUpdatePayload: &fleet.SmallstepSCEPProxyCAUpdatePayload{
URL: ptr.String("https://smallstep.example.com"),
ChallengeURL: ptr.String("https://smallstep.example.com/challenge"),
Username: ptr.String("updated-username"),
Password: ptr.String("updated-password"),
},
}
err := svc.UpdateCertificateAuthority(ctx, smallstepID, payload)
require.EqualError(t, err, "Couldn't edit certificate authority. Invalid challenge URL or credentials. Please correct and try again.")
})
t.Run("Requires password when updating SCEP URL", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
SmallstepSCEPProxyCAUpdatePayload: &fleet.SmallstepSCEPProxyCAUpdatePayload{
URL: ptr.String("https://smallstep.example.com"),
},
}
err := svc.UpdateCertificateAuthority(ctx, smallstepID, payload)
require.EqualError(t, err, "Couldn't edit certificate authority. \"password\" must be set when modifying \"url\", \"challenge_url\", or \"username\" of an existing certificate authority: Smallstep CA")
})
t.Run("Requires password when updating challenge URL", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
SmallstepSCEPProxyCAUpdatePayload: &fleet.SmallstepSCEPProxyCAUpdatePayload{
ChallengeURL: ptr.String("https://smallstep.example.com/challenge"),
},
}
err := svc.UpdateCertificateAuthority(ctx, smallstepID, payload)
require.EqualError(t, err, "Couldn't edit certificate authority. \"password\" must be set when modifying \"url\", \"challenge_url\", or \"username\" of an existing certificate authority: Smallstep CA")
})
t.Run("Requires password when updating username", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
SmallstepSCEPProxyCAUpdatePayload: &fleet.SmallstepSCEPProxyCAUpdatePayload{
Username: ptr.String("updated-username"),
},
}
err := svc.UpdateCertificateAuthority(ctx, smallstepID, payload)
require.EqualError(t, err, "Couldn't edit certificate authority. \"password\" must be set when modifying \"url\", \"challenge_url\", or \"username\" of an existing certificate authority: Smallstep CA")
})
t.Run("Validates password and username on update", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
SmallstepSCEPProxyCAUpdatePayload: &fleet.SmallstepSCEPProxyCAUpdatePayload{
Username: ptr.String("updated-username"),
Password: ptr.String("updated-password"),
},
}
mockScep := &scep_mock.SCEPConfigService{
ValidateSmallstepChallengeURLFunc: func(_ context.Context, _ fleet.SmallstepSCEPProxyCA) error {
return errors.New("bad password or username")
},
ValidateSmallstepChallengeURLFuncInvoked: false, // Reset so we can check it was invoked
}
svc.scepConfigService = mockScep
err := svc.UpdateCertificateAuthority(ctx, smallstepID, payload)
require.True(t, mockScep.ValidateSmallstepChallengeURLFuncInvoked)
require.EqualError(t, err, "Couldn't edit certificate authority. Invalid challenge URL or credentials. Please correct and try again.")
})
t.Run("Errors on empty username", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
SmallstepSCEPProxyCAUpdatePayload: &fleet.SmallstepSCEPProxyCAUpdatePayload{
Username: ptr.String(""),
Password: ptr.String("updated-password"),
},
}
err := svc.UpdateCertificateAuthority(ctx, smallstepID, payload)
require.EqualError(t, err, "Couldn't edit certificate authority. Smallstep SCEP Proxy username cannot be empty")
})
t.Run("Errors on empty password", func(t *testing.T) {
svc, ctx := baseSetupForCATests()
payload := fleet.CertificateAuthorityUpdatePayload{
SmallstepSCEPProxyCAUpdatePayload: &fleet.SmallstepSCEPProxyCAUpdatePayload{
Username: ptr.String("updated-username"),
Password: ptr.String(""),
},
}
err := svc.UpdateCertificateAuthority(ctx, smallstepID, payload)
require.EqualError(t, err, "Couldn't edit certificate authority. Smallstep SCEP Proxy password cannot be empty")
})
})
}
func TestDeleteCertificateAuthority(t *testing.T) {
t.Parallel()
ds := new(mock.Store)
ctx := context.Background()
svc := newTestService(t, ds)
admin := &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}
ctx = viewer.NewContext(ctx, viewer.Viewer{User: admin})
t.Run("successfully deletes certificate", func(t *testing.T) {
ds.DeleteCertificateAuthorityFunc = func(ctx context.Context, certificateAuthorityID uint) (*fleet.CertificateAuthoritySummary, error) {
return nil, errors.New("forced error to short-circuit activity creation")
}
err := svc.DeleteCertificateAuthority(ctx, 1)
require.Error(t, err)
require.Equal(t, "forced error to short-circuit activity creation", err.Error())
})
t.Run("returns not found error if certificate authority does not exist", func(t *testing.T) {
ds.DeleteCertificateAuthorityFunc = func(ctx context.Context, certificateAuthorityID uint) (*fleet.CertificateAuthoritySummary, error) {
return nil, common_mysql.NotFound("certificate authority")
}
err := svc.DeleteCertificateAuthority(ctx, 999)
require.Error(t, err)
require.Contains(t, err.Error(), "certificate authority was not found")
})
}