mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
parent
ac69cb7bcc
commit
37722a925f
25 changed files with 815 additions and 107 deletions
|
|
@ -25,9 +25,9 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/ee/server/scim"
|
||||
eeservice "github.com/fleetdm/fleet/v4/ee/server/service"
|
||||
"github.com/fleetdm/fleet/v4/ee/server/service/digicert"
|
||||
"github.com/fleetdm/fleet/v4/ee/server/service/est"
|
||||
"github.com/fleetdm/fleet/v4/ee/server/service/hostidentity"
|
||||
"github.com/fleetdm/fleet/v4/ee/server/service/hostidentity/httpsig"
|
||||
"github.com/fleetdm/fleet/v4/ee/server/service/hydrant"
|
||||
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
|
||||
"github.com/fleetdm/fleet/v4/pkg/scripts"
|
||||
"github.com/fleetdm/fleet/v4/server"
|
||||
|
|
@ -802,7 +802,7 @@ the way that the Fleet server works.
|
|||
var softwareTitleIconStore fleet.SoftwareTitleIconStore
|
||||
var distributedLock fleet.Lock
|
||||
if license.IsPremium() {
|
||||
hydrantService := hydrant.NewService(hydrant.WithLogger(logger))
|
||||
hydrantService := est.NewService(est.WithLogger(logger))
|
||||
profileMatcher := apple_mdm.NewProfileMatcher(redisPool)
|
||||
if config.S3.SoftwareInstallersBucket != "" {
|
||||
if config.S3.BucketsAndPrefixesMatch() {
|
||||
|
|
|
|||
|
|
@ -859,6 +859,15 @@ func (cmd *GenerateGitopsCommand) generateCertificateAuthorities(filePath string
|
|||
})
|
||||
}
|
||||
}
|
||||
if estCA, ok := result["custom_est_proxy"]; ok && estCA != nil {
|
||||
for _, intg := range estCA.([]any) {
|
||||
intg.(map[string]interface{})["password"] = cmd.AddComment(filePath, "TODO: Add your EST password here")
|
||||
cmd.Messages.SecretWarnings = append(cmd.Messages.SecretWarnings, SecretWarning{
|
||||
Filename: "default.yml",
|
||||
Key: "certificate_authorities.custom_est_proxy.password",
|
||||
})
|
||||
}
|
||||
}
|
||||
if smallstep, ok := result["smallstep"]; ok && smallstep != nil {
|
||||
for _, intg := range smallstep.([]interface{}) {
|
||||
intg.(map[string]interface{})["password"] = cmd.AddComment(filePath, "TODO: Add your Smallstep password here")
|
||||
|
|
|
|||
|
|
@ -559,6 +559,14 @@ func (MockClient) GetCertificateAuthoritiesSpec(includeSecrets bool) (*fleet.Gro
|
|||
ClientSecret: maskSecret("some-hydrant-client-secret", includeSecrets),
|
||||
},
|
||||
},
|
||||
EST: []fleet.ESTProxyCA{
|
||||
{
|
||||
Name: "some-est-name",
|
||||
URL: "https://some-est-url.example.com",
|
||||
Username: "some-est-username",
|
||||
Password: maskSecret("some-est-password", includeSecrets),
|
||||
},
|
||||
},
|
||||
Smallstep: []fleet.SmallstepSCEPProxyCA{
|
||||
{
|
||||
Name: "some-smallstep-name",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
certificate_authorities:
|
||||
custom_est_proxy:
|
||||
- name: some-est-name
|
||||
password: some-est-password
|
||||
url: https://some-est-url.example.com
|
||||
username: some-est-username
|
||||
custom_scep_proxy:
|
||||
- challenge: some-custom-scep-proxy-challenge
|
||||
name: some-custom-scep-proxy-name
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
certificate_authorities:
|
||||
custom_est_proxy:
|
||||
- name: some-est-name
|
||||
password: ___GITOPS_COMMENT_7___
|
||||
url: https://some-est-url.example.com
|
||||
username: some-est-username
|
||||
custom_scep_proxy:
|
||||
- challenge: ___GITOPS_COMMENT_5___
|
||||
name: some-custom-scep-proxy-name
|
||||
|
|
@ -26,9 +31,9 @@ certificate_authorities:
|
|||
smallstep:
|
||||
- challenge_url: https://some-smallstep-challenge-url.com
|
||||
name: some-smallstep-name
|
||||
password: ___GITOPS_COMMENT_7___
|
||||
password: ___GITOPS_COMMENT_8___
|
||||
url: https://some-smallstep-url.com
|
||||
username: ___GITOPS_COMMENT_8___
|
||||
username: ___GITOPS_COMMENT_9___
|
||||
features:
|
||||
enable_host_users: true
|
||||
enable_software_inventory: true
|
||||
|
|
@ -75,8 +80,8 @@ mdm:
|
|||
entity_id: some-mdm-entity-id.com
|
||||
idp_name: some-other-idp-name
|
||||
issuer_uri: https://some-mdm-issuer-uri.com
|
||||
metadata: ___GITOPS_COMMENT_9___
|
||||
metadata_url: ___GITOPS_COMMENT_10___
|
||||
metadata: ___GITOPS_COMMENT_10___
|
||||
metadata_url: ___GITOPS_COMMENT_11___
|
||||
end_user_license_agreement: ./lib/eula/test.pdf
|
||||
volume_purchasing_program:
|
||||
- location: Fleet Device Management Inc.
|
||||
|
|
@ -91,7 +96,7 @@ org_info:
|
|||
org_logo_url_light_background: http://some-org-logo-url-light-background.com
|
||||
org_name: Fleet
|
||||
secrets:
|
||||
- secret: ___GITOPS_COMMENT_11___
|
||||
- secret: ___GITOPS_COMMENT_12___
|
||||
server_settings:
|
||||
ai_features_disabled: false
|
||||
debug_host_ids:
|
||||
|
|
@ -111,8 +116,8 @@ sso_settings:
|
|||
entity_id: dogfood.fleetdm.com
|
||||
idp_image_url: http://some-sso-idp-image-url.com
|
||||
idp_name: some-idp-name
|
||||
metadata: ___GITOPS_COMMENT_12___
|
||||
metadata_url: ___GITOPS_COMMENT_13___
|
||||
metadata: ___GITOPS_COMMENT_13___
|
||||
metadata_url: ___GITOPS_COMMENT_14___
|
||||
sso_server_url: https://sso.fleetdm.com
|
||||
webhook_settings:
|
||||
activities_webhook:
|
||||
|
|
|
|||
|
|
@ -50,6 +50,11 @@ labels:
|
|||
name: Label C
|
||||
org_settings:
|
||||
certificate_authorities:
|
||||
custom_est_proxy:
|
||||
- name: some-est-name
|
||||
password: # TODO: Add your EST password here
|
||||
url: https://some-est-url.example.com
|
||||
username: some-est-username
|
||||
custom_scep_proxy:
|
||||
- challenge: # TODO: Add your custom SCEP proxy challenge here
|
||||
name: some-custom-scep-proxy-name
|
||||
|
|
@ -178,7 +183,7 @@ policies:
|
|||
critical: false
|
||||
description: This is a global policy
|
||||
install_software:
|
||||
hash_sha256: ___GITOPS_COMMENT_14___
|
||||
hash_sha256: ___GITOPS_COMMENT_15___
|
||||
labels_include_any:
|
||||
- Label A
|
||||
- Label B
|
||||
|
|
|
|||
|
|
@ -32,6 +32,11 @@ labels:
|
|||
name: Label C
|
||||
org_settings:
|
||||
certificate_authorities:
|
||||
custom_est_proxy:
|
||||
- name: some-est-name
|
||||
password: # TODO: Add your EST password here
|
||||
url: https://some-est-url.example.com
|
||||
username: some-est-username
|
||||
custom_scep_proxy:
|
||||
- challenge: # TODO: Add your custom SCEP proxy challenge here
|
||||
name: some-custom-scep-proxy-name
|
||||
|
|
|
|||
|
|
@ -1855,6 +1855,51 @@ This activity contains the following fields:
|
|||
}
|
||||
```
|
||||
|
||||
## added_custom_est_proxy
|
||||
|
||||
Generated when a custom EST certificate authority configuration is added in Fleet.
|
||||
|
||||
This activity contains the following fields:
|
||||
- "name": Name of the certificate authority.
|
||||
|
||||
#### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "EST_WIFI"
|
||||
}
|
||||
```
|
||||
|
||||
## deleted_custom_est_proxy
|
||||
|
||||
Generated when a custom EST certificate authority configuration is deleted in Fleet.
|
||||
|
||||
This activity contains the following fields:
|
||||
- "name": Name of the certificate authority.
|
||||
|
||||
#### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "EST_WIFI"
|
||||
}
|
||||
```
|
||||
|
||||
## edited_custom_est_proxy
|
||||
|
||||
Generated when a custom EST certificate authority configuration is edited in Fleet.
|
||||
|
||||
This activity contains the following fields:
|
||||
- "name": Name of the certificate authority.
|
||||
|
||||
#### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "EST_WIFI"
|
||||
}
|
||||
```
|
||||
|
||||
## added_smallstep
|
||||
|
||||
Generated when Smallstep certificate authority configuration is added in Fleet.
|
||||
|
|
|
|||
|
|
@ -91,6 +91,22 @@ func (svc *Service) NewCertificateAuthority(ctx context.Context, p fleet.Certifi
|
|||
activity = fleet.ActivityAddedHydrant{}
|
||||
}
|
||||
|
||||
if p.CustomESTProxy != nil {
|
||||
p.CustomESTProxy.Preprocess()
|
||||
|
||||
if err := svc.validateEST(ctx, p.CustomESTProxy, errPrefix); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
caToCreate.Type = string(fleet.CATypeCustomESTProxy)
|
||||
caToCreate.Name = &p.CustomESTProxy.Name
|
||||
caToCreate.URL = &p.CustomESTProxy.URL
|
||||
caToCreate.Username = &p.CustomESTProxy.Username
|
||||
caToCreate.Password = &p.CustomESTProxy.Password
|
||||
caDisplayType = "custom EST"
|
||||
activity = fleet.ActivityAddedCustomESTProxy{}
|
||||
}
|
||||
|
||||
if p.NDESSCEPProxy != nil {
|
||||
p.NDESSCEPProxy.Preprocess()
|
||||
|
||||
|
|
@ -175,6 +191,9 @@ func (svc *Service) validatePayload(p *fleet.CertificateAuthorityPayload, errPre
|
|||
if p.Smallstep != nil {
|
||||
casToCreate++
|
||||
}
|
||||
if p.CustomESTProxy != nil {
|
||||
casToCreate++
|
||||
}
|
||||
if casToCreate == 0 {
|
||||
return &fleet.BadRequestError{Message: fmt.Sprintf("%sA certificate authority must be specified", errPrefix)}
|
||||
}
|
||||
|
|
@ -311,12 +330,37 @@ func (svc *Service) validateHydrant(ctx context.Context, hydrantCA *fleet.Hydran
|
|||
if hydrantCA.ClientSecret == "" || hydrantCA.ClientSecret == fleet.MaskedPassword {
|
||||
return fleet.NewInvalidArgumentError("client_secret", fmt.Sprintf("%sInvalid Hydrant Client Secret. Please correct and try again.", errPrefix))
|
||||
}
|
||||
if err := svc.hydrantService.ValidateHydrantURL(ctx, *hydrantCA); err != nil {
|
||||
if err := svc.estService.ValidateESTURL(ctx, fleet.ESTProxyCA{
|
||||
ID: hydrantCA.ID,
|
||||
Name: hydrantCA.Name,
|
||||
URL: hydrantCA.URL,
|
||||
Username: hydrantCA.ClientID,
|
||||
Password: hydrantCA.ClientSecret,
|
||||
}); err != nil {
|
||||
return fleet.NewInvalidArgumentError("url", fmt.Sprintf("%sInvalid Hydrant URL. Please correct and try again.", errPrefix))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *Service) validateEST(ctx context.Context, estProxyCA *fleet.ESTProxyCA, errPrefix string) error {
|
||||
if err := validateCAName(estProxyCA.Name, errPrefix); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateURL(estProxyCA.URL, "EST", errPrefix); err != nil {
|
||||
return err
|
||||
}
|
||||
if estProxyCA.Username == "" {
|
||||
return fleet.NewInvalidArgumentError("username", fmt.Sprintf("%sInvalid EST Username. Please correct and try again.", errPrefix))
|
||||
}
|
||||
if estProxyCA.Password == "" || estProxyCA.Password == fleet.MaskedPassword {
|
||||
return fleet.NewInvalidArgumentError("password", fmt.Sprintf("%sInvalid EST Password. Please correct and try again.", errPrefix))
|
||||
}
|
||||
if err := svc.estService.ValidateESTURL(ctx, *estProxyCA); err != nil {
|
||||
return fleet.NewInvalidArgumentError("url", fmt.Sprintf("%sInvalid EST URL. Please correct and try again.", errPrefix))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateURL(caURL, displayType, errPrefix string) error {
|
||||
if u, err := url.ParseRequestURI(caURL); err != nil {
|
||||
return fleet.NewInvalidArgumentError("url", fmt.Sprintf("%sInvalid %s URL. Please correct and try again.", errPrefix, displayType))
|
||||
|
|
@ -425,6 +469,10 @@ func (svc *Service) DeleteCertificateAuthority(ctx context.Context, certificateA
|
|||
activity = fleet.ActivityDeletedSmallstep{
|
||||
Name: ca.Name,
|
||||
}
|
||||
case string(fleet.CATypeCustomESTProxy):
|
||||
activity = fleet.ActivityDeletedCustomESTProxy{
|
||||
Name: ca.Name,
|
||||
}
|
||||
}
|
||||
|
||||
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), activity); err != nil {
|
||||
|
|
@ -528,6 +576,16 @@ func (svc *Service) getCertificateAuthoritiesBatchOperations(ctx context.Context
|
|||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, ca := range incoming.EST {
|
||||
if strings.TrimSpace(ca.Name) == "" {
|
||||
return nil, fleet.NewInvalidArgumentError("name", "certificate_authorities.custom_est_proxy: CA name cannot be empty.")
|
||||
}
|
||||
ca.Preprocess()
|
||||
|
||||
if err := checkAllNames(ca.Name, "custom_est_proxy", "Custom EST Proxy"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// preprocess smallstep
|
||||
for _, ca := range incoming.Smallstep {
|
||||
if ca.Name == "" {
|
||||
|
|
@ -556,6 +614,9 @@ func (svc *Service) getCertificateAuthoritiesBatchOperations(ctx context.Context
|
|||
if err := svc.processHydrantCAs(ctx, batchOps, incoming.Hydrant, existing.Hydrant); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := svc.processESTCAs(ctx, batchOps, incoming.EST, existing.EST); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := svc.processSmallstepCAs(ctx, batchOps, incoming.Smallstep, existing.Smallstep); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -800,6 +861,59 @@ func (svc *Service) processHydrantCAs(ctx context.Context, batchOps *fleet.Certi
|
|||
return nil
|
||||
}
|
||||
|
||||
func (svc *Service) processESTCAs(ctx context.Context, batchOps *fleet.CertificateAuthoritiesBatchOperations, incomingCAs []fleet.ESTProxyCA, existingCAs []fleet.ESTProxyCA) error {
|
||||
incomingByName := make(map[string]*fleet.ESTProxyCA)
|
||||
for _, incoming := range incomingCAs {
|
||||
// Note: caller is responsible for ensuring incoming list has no duplicates
|
||||
incomingByName[incoming.Name] = &incoming
|
||||
}
|
||||
|
||||
existingByName := make(map[string]*fleet.ESTProxyCA)
|
||||
for _, existing := range existingCAs {
|
||||
// if existing CA isn't in the incoming list, we should delete it
|
||||
if _, ok := incomingByName[existing.Name]; !ok {
|
||||
batchOps.Delete = append(batchOps.Delete, &fleet.CertificateAuthority{
|
||||
Type: string(fleet.CATypeCustomESTProxy),
|
||||
Name: &existing.Name,
|
||||
URL: &existing.URL,
|
||||
Username: &existing.Username,
|
||||
Password: &existing.Password,
|
||||
})
|
||||
}
|
||||
// Note: datastore is responsible for ensuring no existing list has no duplicates
|
||||
existingByName[existing.Name] = &existing
|
||||
}
|
||||
|
||||
for name, incoming := range incomingByName {
|
||||
if err := svc.validateEST(ctx, incoming, "certificate_authorities.custom_est_proxy: "); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create the payload to be added or updated
|
||||
if _, ok := existingByName[name]; ok {
|
||||
// update existing
|
||||
batchOps.Update = append(batchOps.Update, &fleet.CertificateAuthority{
|
||||
Type: string(fleet.CATypeCustomESTProxy),
|
||||
Name: &incoming.Name,
|
||||
URL: &incoming.URL,
|
||||
Username: &incoming.Username,
|
||||
Password: &incoming.Password,
|
||||
})
|
||||
} else {
|
||||
// add new
|
||||
batchOps.Add = append(batchOps.Add, &fleet.CertificateAuthority{
|
||||
Type: string(fleet.CATypeCustomESTProxy),
|
||||
Name: &incoming.Name,
|
||||
URL: &incoming.URL,
|
||||
Username: &incoming.Username,
|
||||
Password: &incoming.Password,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *Service) processSmallstepCAs(ctx context.Context, batchOps *fleet.CertificateAuthoritiesBatchOperations, incomingCAs []fleet.SmallstepSCEPProxyCA, existingCAs []fleet.SmallstepSCEPProxyCA) error {
|
||||
incomingByName := make(map[string]*fleet.SmallstepSCEPProxyCA)
|
||||
for _, incoming := range incomingCAs {
|
||||
|
|
@ -881,6 +995,10 @@ func (svc *Service) recordActivitiesBatchApplyCAs(ctx context.Context, ops *flee
|
|||
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityAddedHydrant{Name: *ca.Name}); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "create activity for added hydrant")
|
||||
}
|
||||
case string(fleet.CATypeCustomESTProxy):
|
||||
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityAddedCustomESTProxy{Name: *ca.Name}); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "create activity for added custom est proxy")
|
||||
}
|
||||
case string(fleet.CATypeSmallstep):
|
||||
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityAddedSmallstep{Name: *ca.Name}); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "create activity for added smallstep SCEP proxy")
|
||||
|
|
@ -905,6 +1023,10 @@ func (svc *Service) recordActivitiesBatchApplyCAs(ctx context.Context, ops *flee
|
|||
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityEditedHydrant{Name: *ca.Name}); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "create activity for edited hydrant")
|
||||
}
|
||||
case string(fleet.CATypeCustomESTProxy):
|
||||
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityEditedCustomESTProxy{Name: *ca.Name}); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "create activityu for edited custom EST proxy")
|
||||
}
|
||||
case string(fleet.CATypeSmallstep):
|
||||
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityEditedSmallstep{Name: *ca.Name}); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "create activity for edited smallstep SCEP proxy")
|
||||
|
|
@ -929,6 +1051,10 @@ func (svc *Service) recordActivitiesBatchApplyCAs(ctx context.Context, ops *flee
|
|||
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityDeletedHydrant{Name: *ca.Name}); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "create activity for deleted hydrant")
|
||||
}
|
||||
case string(fleet.CATypeCustomESTProxy):
|
||||
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityDeletedCustomESTProxy{Name: *ca.Name}); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "create activity for deleted custom EST proxy")
|
||||
}
|
||||
case string(fleet.CATypeSmallstep):
|
||||
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityDeletedSmallstep{Name: *ca.Name}); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "create activity for deleted smallstep SCEP proxy")
|
||||
|
|
@ -962,7 +1088,8 @@ func (svc *Service) UpdateCertificateAuthority(ctx context.Context, id uint, p f
|
|||
var activity fleet.ActivityDetails
|
||||
var caActivityName string
|
||||
|
||||
if p.DigiCertCAUpdatePayload != nil {
|
||||
switch {
|
||||
case p.DigiCertCAUpdatePayload != nil:
|
||||
if p.DigiCertCAUpdatePayload.IsEmpty() {
|
||||
return &fleet.BadRequestError{Message: fmt.Sprintf("%sDigiCert CA update payload is empty", errPrefix)}
|
||||
}
|
||||
|
|
@ -989,8 +1116,7 @@ func (svc *Service) UpdateCertificateAuthority(ctx context.Context, id uint, p f
|
|||
caActivityName = *oldCA.Name
|
||||
}
|
||||
activity = fleet.ActivityEditedDigiCert{Name: caActivityName}
|
||||
}
|
||||
if p.HydrantCAUpdatePayload != nil {
|
||||
case p.HydrantCAUpdatePayload != nil:
|
||||
if p.HydrantCAUpdatePayload.IsEmpty() {
|
||||
return &fleet.BadRequestError{Message: fmt.Sprintf("%sHydrant CA update payload is empty", errPrefix)}
|
||||
}
|
||||
|
|
@ -1013,8 +1139,30 @@ func (svc *Service) UpdateCertificateAuthority(ctx context.Context, id uint, p f
|
|||
caActivityName = *oldCA.Name
|
||||
}
|
||||
activity = fleet.ActivityEditedHydrant{Name: caActivityName}
|
||||
}
|
||||
if p.NDESSCEPProxyCAUpdatePayload != nil {
|
||||
case p.CustomESTCAUpdatePayload != nil:
|
||||
if p.CustomESTCAUpdatePayload.IsEmpty() {
|
||||
return &fleet.BadRequestError{Message: fmt.Sprintf("%sCustom EST CA update payload is empty", errPrefix)}
|
||||
}
|
||||
|
||||
if err := p.CustomESTCAUpdatePayload.ValidateRelatedFields(errPrefix, *oldCA.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
p.CustomESTCAUpdatePayload.Preprocess()
|
||||
if err := svc.validateCustomESTUpdate(ctx, p.CustomESTCAUpdatePayload, oldCA, errPrefix); err != nil {
|
||||
return err
|
||||
}
|
||||
caToUpdate.Type = string(fleet.CATypeCustomESTProxy)
|
||||
caToUpdate.Name = p.CustomESTCAUpdatePayload.Name
|
||||
caToUpdate.URL = p.CustomESTCAUpdatePayload.URL
|
||||
caToUpdate.Username = p.CustomESTCAUpdatePayload.Username
|
||||
caToUpdate.Password = p.CustomESTCAUpdatePayload.Password
|
||||
if caToUpdate.Name != nil {
|
||||
caActivityName = *caToUpdate.Name
|
||||
} else {
|
||||
caActivityName = *oldCA.Name
|
||||
}
|
||||
activity = fleet.ActivityEditedCustomESTProxy{Name: caActivityName}
|
||||
case p.NDESSCEPProxyCAUpdatePayload != nil:
|
||||
if p.NDESSCEPProxyCAUpdatePayload.IsEmpty() {
|
||||
return &fleet.BadRequestError{Message: fmt.Sprintf("%sNDES SCEP Proxy CA update payload is empty", errPrefix)}
|
||||
}
|
||||
|
|
@ -1037,8 +1185,7 @@ func (svc *Service) UpdateCertificateAuthority(ctx context.Context, id uint, p f
|
|||
caActivityName = *oldCA.Name
|
||||
}
|
||||
activity = fleet.ActivityEditedNDESSCEPProxy{}
|
||||
}
|
||||
if p.CustomSCEPProxyCAUpdatePayload != nil {
|
||||
case p.CustomSCEPProxyCAUpdatePayload != nil:
|
||||
if p.CustomSCEPProxyCAUpdatePayload.IsEmpty() {
|
||||
return &fleet.BadRequestError{Message: fmt.Sprintf("%sCustom SCEP Proxy CA update payload is empty", errPrefix)}
|
||||
}
|
||||
|
|
@ -1061,8 +1208,7 @@ func (svc *Service) UpdateCertificateAuthority(ctx context.Context, id uint, p f
|
|||
}
|
||||
activity = fleet.ActivityEditedCustomSCEPProxy{Name: caActivityName}
|
||||
|
||||
}
|
||||
if p.SmallstepSCEPProxyCAUpdatePayload != nil {
|
||||
case p.SmallstepSCEPProxyCAUpdatePayload != nil:
|
||||
if p.SmallstepSCEPProxyCAUpdatePayload.IsEmpty() {
|
||||
return &fleet.BadRequestError{Message: fmt.Sprintf("%sSmallstep SCEP Proxy CA update payload is empty", errPrefix)}
|
||||
}
|
||||
|
|
@ -1196,10 +1342,10 @@ func (svc *Service) validateHydrantUpdate(ctx context.Context, hydrant *fleet.Hy
|
|||
return err
|
||||
}
|
||||
|
||||
hydrantCAToVerify := fleet.HydrantCA{ // The hydrant service for verification only requires the URL.
|
||||
hydrantCAToVerify := fleet.ESTProxyCA{ // The hydrant service for verification only requires the URL.
|
||||
URL: *hydrant.URL,
|
||||
}
|
||||
if err := svc.hydrantService.ValidateHydrantURL(ctx, hydrantCAToVerify); err != nil {
|
||||
if err := svc.estService.ValidateESTURL(ctx, hydrantCAToVerify); err != nil {
|
||||
return &fleet.BadRequestError{Message: fmt.Sprintf("%sInvalid Hydrant URL. Please correct and try again.", errPrefix)}
|
||||
}
|
||||
}
|
||||
|
|
@ -1217,6 +1363,38 @@ func (svc *Service) validateHydrantUpdate(ctx context.Context, hydrant *fleet.Hy
|
|||
return nil
|
||||
}
|
||||
|
||||
func (svc *Service) validateCustomESTUpdate(ctx context.Context, estUpdate *fleet.CustomESTCAUpdatePayload, oldCA *fleet.CertificateAuthority, errPrefix string) error {
|
||||
if estUpdate.Name != nil {
|
||||
if err := validateCAName(*estUpdate.Name, errPrefix); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if estUpdate.URL != nil {
|
||||
if err := validateURL(*estUpdate.URL, "EST", errPrefix); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hydrantCAToVerify := fleet.ESTProxyCA{ // The EST service for verification only requires the URL.
|
||||
URL: *estUpdate.URL,
|
||||
}
|
||||
if err := svc.estService.ValidateESTURL(ctx, hydrantCAToVerify); err != nil {
|
||||
return &fleet.BadRequestError{Message: fmt.Sprintf("%sInvalid EST URL. Please correct and try again.", errPrefix)}
|
||||
}
|
||||
}
|
||||
if estUpdate.Username != nil && *estUpdate.Username == "" {
|
||||
return &fleet.BadRequestError{
|
||||
Message: fmt.Sprintf("%sInvalid EST Username. Please correct and try again.", errPrefix),
|
||||
}
|
||||
}
|
||||
if estUpdate.Password != nil && *estUpdate.Password == "" {
|
||||
return &fleet.BadRequestError{
|
||||
Message: fmt.Sprintf("%sInvalid EST Password. Please correct and try again.", errPrefix),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *Service) validateNDESSCEPProxyUpdate(ctx context.Context, ndesSCEP *fleet.NDESSCEPProxyCAUpdatePayload, oldCA *fleet.CertificateAuthority, errPrefix string) error {
|
||||
// some methods in this fuction require the NDESSCEPProxyCA type so we convert the ndes update payload here
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/ee/server/service/digicert"
|
||||
"github.com/fleetdm/fleet/v4/ee/server/service/hydrant"
|
||||
"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/datastore/mysql/common_mysql"
|
||||
|
|
@ -204,7 +204,7 @@ func TestCreatingCertificateAuthorities(t *testing.T) {
|
|||
ds: ds,
|
||||
authz: authorizer,
|
||||
digiCertService: digicert.NewService(),
|
||||
hydrantService: hydrant.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 },
|
||||
|
|
@ -1199,7 +1199,7 @@ func TestUpdatingCertificateAuthorities(t *testing.T) {
|
|||
ds: ds,
|
||||
authz: authorizer,
|
||||
digiCertService: digicert.NewService(),
|
||||
hydrantService: hydrant.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 },
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package hydrant
|
||||
package est
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
@ -25,17 +25,17 @@ type Service struct {
|
|||
client *http.Client
|
||||
}
|
||||
|
||||
// Compile-time check for HydrantService interface
|
||||
var _ fleet.HydrantService = (*Service)(nil)
|
||||
// Compile-time check for ESTService interface
|
||||
var _ fleet.ESTService = (*Service)(nil)
|
||||
|
||||
func NewService(opts ...Opt) fleet.HydrantService {
|
||||
func NewService(opts ...Opt) fleet.ESTService {
|
||||
s := &Service{}
|
||||
s.populateOpts(opts)
|
||||
s.client = fleethttp.NewClient(fleethttp.WithTimeout(s.timeout))
|
||||
return s
|
||||
}
|
||||
|
||||
// Opt is the type for Hydrant integration options.
|
||||
// Opt is the type for EST integration options.
|
||||
type Opt func(*Service)
|
||||
|
||||
// WithTimeout sets the timeout to use for the HTTP client.
|
||||
|
|
@ -64,60 +64,60 @@ func (s *Service) populateOpts(opts []Opt) {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *Service) ValidateHydrantURL(ctx context.Context, hydrantCA fleet.HydrantCA) error {
|
||||
reqURL := hydrantCA.URL + "/cacerts"
|
||||
func (s *Service) ValidateESTURL(ctx context.Context, estCA fleet.ESTProxyCA) error {
|
||||
reqURL := estCA.URL + "/cacerts"
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "creating Hydrant CA request")
|
||||
return ctxerr.Wrap(ctx, err, "creating EST CA request")
|
||||
}
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "sending Hydrant CA request")
|
||||
return ctxerr.Wrap(ctx, err, "sending EST CA request")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return ctxerr.Errorf(ctx, "unexpected Hydrant CA status code: %d", resp.StatusCode)
|
||||
return ctxerr.Errorf(ctx, "unexpected EST CA status code: %d", resp.StatusCode)
|
||||
}
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
if !strings.HasPrefix(contentType, "application/pkcs7-mime") {
|
||||
return ctxerr.Errorf(ctx, "unexpected Hydrant CA content type: %s", contentType)
|
||||
return ctxerr.Errorf(ctx, "unexpected EST CA content type: %s", contentType)
|
||||
}
|
||||
// For now we are just verifying that there is a body of the reportedly correct format. We could
|
||||
// possibly do more. A better implementation would be similar to Digicert's which validates the
|
||||
// credentials in addition to the URL but I don't see a way to do that with Hydrant's API.
|
||||
caCerts, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "reading Hydrant CA response body")
|
||||
return ctxerr.Wrap(ctx, err, "reading EST CA response body")
|
||||
}
|
||||
if len(caCerts) == 0 {
|
||||
return ctxerr.Errorf(ctx, "no CA certificates found in Hydrant CA /cacerts response. URL may be incorrect")
|
||||
return ctxerr.Errorf(ctx, "no CA certificates found in EST CA /cacerts response. URL may be incorrect")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) GetCertificate(ctx context.Context, hydrantCA fleet.HydrantCA, csr string) (*fleet.HydrantCertificate, error) {
|
||||
reqURL, err := url.Parse(hydrantCA.URL + "/simpleenroll")
|
||||
func (s *Service) GetCertificate(ctx context.Context, estCA fleet.ESTProxyCA, csr string) (*fleet.ESTCertificate, error) {
|
||||
reqURL, err := url.Parse(estCA.URL + "/simpleenroll")
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "parsing Hydrant CA URL")
|
||||
return nil, ctxerr.Wrap(ctx, err, "parsing EST CA URL")
|
||||
}
|
||||
apiCredential := hydrantCA.ClientID + ":" + hydrantCA.ClientSecret
|
||||
apiCredential := estCA.Username + ":" + estCA.Password
|
||||
encodedCredential := base64.StdEncoding.EncodeToString([]byte(apiCredential))
|
||||
|
||||
hydrantRequest, err := http.NewRequestWithContext(ctx, "POST", reqURL.String(), strings.NewReader(csr))
|
||||
estRequest, err := http.NewRequestWithContext(ctx, "POST", reqURL.String(), strings.NewReader(csr))
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "creating Hydrant CA request")
|
||||
return nil, ctxerr.Wrap(ctx, err, "creating EST CA request")
|
||||
}
|
||||
hydrantRequest.Header.Set("Content-Type", "application/pkcs10")
|
||||
hydrantRequest.Header.Set("Accept", "application/pkcs7-mime")
|
||||
hydrantRequest.Header.Set("Authorization", "Basic "+encodedCredential)
|
||||
resp, err := s.client.Do(hydrantRequest)
|
||||
estRequest.Header.Set("Content-Type", "application/pkcs10")
|
||||
estRequest.Header.Set("Accept", "application/pkcs7-mime")
|
||||
estRequest.Header.Set("Authorization", "Basic "+encodedCredential)
|
||||
resp, err := s.client.Do(estRequest)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "sending Hydrant CA request")
|
||||
return nil, ctxerr.Wrap(ctx, err, "sending EST CA request")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
bytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "reading Hydrant CA response body")
|
||||
return nil, ctxerr.Wrap(ctx, err, "reading EST CA response body")
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bytesToLog := bytes
|
||||
|
|
@ -125,11 +125,11 @@ func (s *Service) GetCertificate(ctx context.Context, hydrantCA fleet.HydrantCA,
|
|||
if len(bytes) > 1000 {
|
||||
bytesToLog = bytes[:1000]
|
||||
}
|
||||
s.logger.Log("msg", "unexpected Hydrant CA status code", "status_code", resp.StatusCode, "response_body", string(bytesToLog))
|
||||
return nil, ctxerr.Errorf(ctx, "unexpected Hydrant CA status code: %d", resp.StatusCode)
|
||||
s.logger.Log("msg", "unexpected EST CA status code", "status_code", resp.StatusCode, "response_body", string(bytesToLog))
|
||||
return nil, ctxerr.Errorf(ctx, "unexpected EST CA status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return &fleet.HydrantCertificate{
|
||||
return &fleet.ESTCertificate{
|
||||
Certificate: bytes,
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -28,11 +28,24 @@ func (svc *Service) RequestCertificate(ctx context.Context, p fleet.RequestCerti
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ca.Type != string(fleet.CATypeHydrant) {
|
||||
return nil, &fleet.BadRequestError{Message: "This API currently only supports Hydrant Certificate Authorities."}
|
||||
if ca.Type != string(fleet.CATypeHydrant) && ca.Type != string(fleet.CATypeCustomESTProxy) {
|
||||
return nil, &fleet.BadRequestError{Message: "This API currently only supports Hydrant and EST Certificate Authorities."}
|
||||
}
|
||||
if ca.ClientSecret == nil {
|
||||
return nil, &fleet.BadRequestError{Message: "Certificate authority does not have a client secret configured."}
|
||||
if ca.Type == string(fleet.CATypeHydrant) {
|
||||
if ca.ClientID == nil {
|
||||
return nil, &fleet.BadRequestError{Message: "Certificate authority does not have a username configured."}
|
||||
}
|
||||
if ca.ClientSecret == nil {
|
||||
return nil, &fleet.BadRequestError{Message: "Certificate authority does not have a client secret configured."}
|
||||
}
|
||||
}
|
||||
if ca.Type == string(fleet.CATypeCustomESTProxy) {
|
||||
if ca.Username == nil {
|
||||
return nil, &fleet.BadRequestError{Message: "Certificate authority does not have a username configured."}
|
||||
}
|
||||
if ca.Password == nil {
|
||||
return nil, &fleet.BadRequestError{Message: "Certificate authority does not have a password configured."}
|
||||
}
|
||||
}
|
||||
certificateRequest, err := svc.parseCSR(ctx, p.CSR)
|
||||
if err != nil {
|
||||
|
|
@ -86,21 +99,33 @@ func (svc *Service) RequestCertificate(ctx context.Context, p fleet.RequestCerti
|
|||
csrForRequest = strings.ReplaceAll(csrForRequest, "-----END CERTIFICATE REQUEST-----", "")
|
||||
csrForRequest = strings.ReplaceAll(csrForRequest, "\\n", "")
|
||||
|
||||
certificate, err := svc.hydrantService.GetCertificate(ctx, fleet.HydrantCA{
|
||||
Name: *ca.Name,
|
||||
URL: *ca.URL,
|
||||
ClientID: *ca.ClientID,
|
||||
ClientSecret: *ca.ClientSecret,
|
||||
}, csrForRequest)
|
||||
var estCA fleet.ESTProxyCA
|
||||
if ca.Type == string(fleet.CATypeHydrant) {
|
||||
estCA = fleet.ESTProxyCA{
|
||||
Name: *ca.Name,
|
||||
URL: *ca.URL,
|
||||
Username: *ca.ClientID,
|
||||
Password: *ca.ClientSecret,
|
||||
}
|
||||
} else {
|
||||
estCA = fleet.ESTProxyCA{
|
||||
Name: *ca.Name,
|
||||
URL: *ca.URL,
|
||||
Username: *ca.Username,
|
||||
Password: *ca.Password,
|
||||
}
|
||||
}
|
||||
|
||||
certificate, err := svc.estService.GetCertificate(ctx, estCA, csrForRequest)
|
||||
if err != nil {
|
||||
level.Error(svc.logger).Log("msg", "Hydrant certificate request failed", "ca_id", ca.ID, "error", err)
|
||||
level.Error(svc.logger).Log("msg", "EST certificate request failed", "ca_id", ca.ID, "error", err)
|
||||
// Bad request may seem like a strange error here but there are many cases where a malformed
|
||||
// CSR can cause this error and Hydrant's API often returns a 5XX error even in these cases
|
||||
// so it is not always possible to distinguish between an error caused by a bad request or
|
||||
// an internal CA error.
|
||||
return nil, &fleet.BadRequestError{Message: fmt.Sprintf("Hydrant certificate request failed: %s", err.Error())}
|
||||
return nil, &fleet.BadRequestError{Message: fmt.Sprintf("EST certificate request failed: %s", err.Error())}
|
||||
}
|
||||
level.Info(svc.logger).Log("msg", "Successfully retrieved a certificate from Hydrant", "ca_id", ca.ID, "idp_username", idpUsername)
|
||||
level.Info(svc.logger).Log("msg", "Successfully retrieved a certificate from EST", "ca_id", ca.ID, "idp_username", idpUsername)
|
||||
// Wrap the certificate in a PEM block for easier consumption by the client
|
||||
return ptr.String("-----BEGIN CERTIFICATE-----\n" + string(certificate.Certificate) + "\n-----END CERTIFICATE-----\n"), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/ee/server/service/hydrant"
|
||||
"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/datastore/mysql/common_mysql"
|
||||
|
|
@ -118,6 +118,14 @@ func TestRequestCertificate(t *testing.T) {
|
|||
APIToken: ptr.String("test-api-token"),
|
||||
ProfileID: ptr.String("test-profile-id"),
|
||||
}
|
||||
customESTCA := &fleet.CertificateAuthority{
|
||||
ID: 3,
|
||||
Name: ptr.String("TestCustomESTCA"),
|
||||
Type: string(fleet.CATypeCustomESTProxy),
|
||||
URL: &mockHydrantServer.URL,
|
||||
Username: ptr.String("test-username"),
|
||||
Password: ptr.String("test-password"),
|
||||
}
|
||||
|
||||
baseSetupForTests := func() (*Service, context.Context) {
|
||||
ds := new(mock.Store)
|
||||
|
|
@ -125,7 +133,7 @@ func TestRequestCertificate(t *testing.T) {
|
|||
// Setup DS mocks
|
||||
ds.GetCertificateAuthorityByIDFunc = func(ctx context.Context, id uint, includeSecrets bool) (*fleet.CertificateAuthority, error) {
|
||||
require.True(t, includeSecrets, "RequestCertificate should always fetch secrets")
|
||||
for _, ca := range []*fleet.CertificateAuthority{hydrantCA, digicertCA} {
|
||||
for _, ca := range []*fleet.CertificateAuthority{hydrantCA, digicertCA, customESTCA} {
|
||||
if ca.ID == id {
|
||||
return ca, nil
|
||||
}
|
||||
|
|
@ -142,9 +150,9 @@ func TestRequestCertificate(t *testing.T) {
|
|||
logger: logger,
|
||||
ds: ds,
|
||||
authz: authorizer,
|
||||
hydrantService: hydrant.NewService(
|
||||
hydrant.WithTimeout(2*time.Second),
|
||||
hydrant.WithLogger(logger),
|
||||
estService: est.NewService(
|
||||
est.WithTimeout(2*time.Second),
|
||||
est.WithLogger(logger),
|
||||
),
|
||||
}
|
||||
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
|
||||
|
|
@ -210,7 +218,7 @@ func TestRequestCertificate(t *testing.T) {
|
|||
IDPToken: ptr.String("test-idp-token"),
|
||||
IDPClientID: ptr.String("test-client-id"), // Missing client ID
|
||||
})
|
||||
require.ErrorContains(t, err, "Hydrant certificate request failed")
|
||||
require.ErrorContains(t, err, "EST certificate request failed")
|
||||
require.Nil(t, cert)
|
||||
})
|
||||
|
||||
|
|
@ -263,7 +271,18 @@ func TestRequestCertificate(t *testing.T) {
|
|||
require.Nil(t, cert)
|
||||
})
|
||||
|
||||
t.Run("Request certificate - non-Hydrant CA", func(t *testing.T) {
|
||||
t.Run("Request a certificate - Custom EST CA", func(t *testing.T) {
|
||||
svc, ctx := baseSetupForTests()
|
||||
cert, err := svc.RequestCertificate(ctx, fleet.RequestCertificatePayload{
|
||||
ID: customESTCA.ID,
|
||||
CSR: goodCSR,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cert)
|
||||
require.Equal(t, "-----BEGIN CERTIFICATE-----\n"+hydrantSimpleEnrollResponse+"\n-----END CERTIFICATE-----\n", *cert)
|
||||
})
|
||||
|
||||
t.Run("Request certificate - non-Hydrant and non-EST CA", func(t *testing.T) {
|
||||
svc, ctx := baseSetupForTests()
|
||||
_, err := svc.RequestCertificate(ctx, fleet.RequestCertificatePayload{
|
||||
ID: digicertCA.ID,
|
||||
|
|
@ -272,7 +291,7 @@ func TestRequestCertificate(t *testing.T) {
|
|||
IDPToken: ptr.String("test-idp-token"),
|
||||
IDPClientID: ptr.String("test-idp-client-id"),
|
||||
})
|
||||
require.ErrorContains(t, err, "This API currently only supports Hydrant Certificate Authorities.")
|
||||
require.ErrorContains(t, err, "This API currently only supports Hydrant and EST Certificate Authorities.")
|
||||
})
|
||||
|
||||
t.Run("Request certificate - nonexistent CA", func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ type Service struct {
|
|||
keyValueStore fleet.KeyValueStore
|
||||
scepConfigService fleet.SCEPConfigService
|
||||
digiCertService fleet.DigiCertService
|
||||
hydrantService fleet.HydrantService
|
||||
estService fleet.ESTService
|
||||
}
|
||||
|
||||
func NewService(
|
||||
|
|
@ -55,7 +55,7 @@ func NewService(
|
|||
keyValueStore fleet.KeyValueStore,
|
||||
scepConfigService fleet.SCEPConfigService,
|
||||
digiCertService fleet.DigiCertService,
|
||||
hydrantService fleet.HydrantService,
|
||||
estService fleet.ESTService,
|
||||
) (*Service, error) {
|
||||
authorizer, err := authz.NewAuthorizer()
|
||||
if err != nil {
|
||||
|
|
@ -81,7 +81,7 @@ func NewService(
|
|||
keyValueStore: keyValueStore,
|
||||
scepConfigService: scepConfigService,
|
||||
digiCertService: digiCertService,
|
||||
hydrantService: hydrantService,
|
||||
estService: estService,
|
||||
}
|
||||
|
||||
// Override methods that can't be easily overriden via
|
||||
|
|
|
|||
|
|
@ -510,6 +510,28 @@ func (ds *Datastore) generateUpdateQueryWithArgs(ctx context.Context, ca *fleet.
|
|||
*args = append(*args, encryptedClientSecret)
|
||||
}
|
||||
|
||||
case string(fleet.CATypeCustomESTProxy):
|
||||
if ca.URL != nil {
|
||||
updates = append(updates, "url = ?")
|
||||
*args = append(*args, *ca.URL)
|
||||
}
|
||||
if ca.Name != nil {
|
||||
updates = append(updates, "name = ?")
|
||||
*args = append(*args, *ca.Name)
|
||||
}
|
||||
if ca.Username != nil {
|
||||
updates = append(updates, "username = ?")
|
||||
*args = append(*args, *ca.Username)
|
||||
}
|
||||
if ca.Password != nil {
|
||||
updates = append(updates, "password_encrypted = ?")
|
||||
encryptedPassword, err := encrypt([]byte(*ca.Password), ds.serverPrivateKey)
|
||||
if err != nil {
|
||||
return "", ctxerr.Wrap(ctx, err, "encrypting password for new certificate authority")
|
||||
}
|
||||
*args = append(*args, encryptedPassword)
|
||||
}
|
||||
|
||||
case string(fleet.CATypeNDESSCEPProxy):
|
||||
if ca.URL != nil {
|
||||
updates = append(updates, "url = ?")
|
||||
|
|
|
|||
|
|
@ -184,6 +184,21 @@ func testCreateCertificateAuthority(t *testing.T, ds *Datastore) {
|
|||
ClientSecret: ptr.String("hydrant-client-secret2"),
|
||||
}
|
||||
|
||||
estCA1 := &fleet.CertificateAuthority{
|
||||
Name: ptr.String("Custom EST CA"),
|
||||
URL: ptr.String("http://customcerts.example.com"),
|
||||
Type: string(fleet.CATypeCustomESTProxy),
|
||||
Username: ptr.String("custom-est-username"),
|
||||
Password: ptr.String("custom-est-password"),
|
||||
}
|
||||
estCA2 := &fleet.CertificateAuthority{
|
||||
Name: ptr.String("Custom EST CA 2"),
|
||||
URL: ptr.String("http://customcerts2.example.com"),
|
||||
Type: string(fleet.CATypeCustomESTProxy),
|
||||
Username: ptr.String("custom-est-username2"),
|
||||
Password: ptr.String("custom-est-password2"),
|
||||
}
|
||||
|
||||
// Custom SCEP CAs
|
||||
customSCEPCA1 := &fleet.CertificateAuthority{
|
||||
Name: ptr.String("Custom SCEP CA"),
|
||||
|
|
@ -231,6 +246,8 @@ func testCreateCertificateAuthority(t *testing.T, ds *Datastore) {
|
|||
digicertCA2,
|
||||
hydrantCA1,
|
||||
hydrantCA2,
|
||||
estCA1,
|
||||
estCA2,
|
||||
customSCEPCA1,
|
||||
customSCEPCA2,
|
||||
ndesCA,
|
||||
|
|
@ -385,6 +402,14 @@ func testUpdateCertificateAuthorityByID(t *testing.T, ds *Datastore) {
|
|||
ClientSecret: ptr.String("hydrant-client-secret"),
|
||||
}
|
||||
|
||||
customESTCA := &fleet.CertificateAuthority{
|
||||
Name: ptr.String("Custom EST CA"),
|
||||
URL: ptr.String("https://estca.example.com"),
|
||||
Type: string(fleet.CATypeCustomESTProxy),
|
||||
Username: ptr.String("custom-est-username"),
|
||||
Password: ptr.String("custom-est-password"),
|
||||
}
|
||||
|
||||
// Custom SCEP CAs
|
||||
customSCEPCA1 := &fleet.CertificateAuthority{
|
||||
Name: ptr.String("Custom SCEP CA"),
|
||||
|
|
@ -416,6 +441,7 @@ func testUpdateCertificateAuthorityByID(t *testing.T, ds *Datastore) {
|
|||
casToCreate := []*fleet.CertificateAuthority{
|
||||
digicertCA1,
|
||||
hydrantCA1,
|
||||
customESTCA,
|
||||
customSCEPCA1,
|
||||
ndesCA1,
|
||||
smallstepCA1,
|
||||
|
|
@ -483,6 +509,26 @@ func testUpdateCertificateAuthorityByID(t *testing.T, ds *Datastore) {
|
|||
require.Equal(t, "updated-client-secret", *updatedCA.ClientSecret)
|
||||
})
|
||||
|
||||
t.Run("successfully updates custom est proxy CA", func(t *testing.T) {
|
||||
customESTCA := caMap[fleet.CATypeCustomESTProxy]
|
||||
|
||||
customESTCA.Name = ptr.String("updated EST")
|
||||
customESTCA.URL = ptr.String("https://coolguy.localhost")
|
||||
customESTCA.Username = ptr.String("updated-username")
|
||||
customESTCA.Password = ptr.String("updated-password")
|
||||
|
||||
err := ds.UpdateCertificateAuthorityByID(ctx, customESTCA.ID, customESTCA)
|
||||
require.NoError(t, err)
|
||||
|
||||
updatedCA, err := ds.GetCertificateAuthorityByID(ctx, customESTCA.ID, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "updated EST", *updatedCA.Name)
|
||||
require.Equal(t, "https://coolguy.localhost", *updatedCA.URL)
|
||||
require.Equal(t, "updated-username", *updatedCA.Username)
|
||||
require.Equal(t, "updated-password", *updatedCA.Password)
|
||||
})
|
||||
|
||||
t.Run("successfully updates ndes scep proxy CA", func(t *testing.T) {
|
||||
ndesCA := caMap[fleet.CATypeNDESSCEPProxy]
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
package tables
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func init() {
|
||||
MigrationClient.AddMigration(Up_20251104112849, Down_20251104112849)
|
||||
}
|
||||
|
||||
func Up_20251104112849(tx *sql.Tx) error {
|
||||
if _, err := tx.Exec(`
|
||||
ALTER TABLE certificate_authorities
|
||||
MODIFY COLUMN type
|
||||
ENUM('digicert','ndes_scep_proxy','custom_scep_proxy','hydrant','smallstep','custom_est_proxy') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL
|
||||
`); err != nil {
|
||||
return fmt.Errorf("adding custom_est_proxy to certificate_authorities.types enum: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Down_20251104112849(tx *sql.Tx) error {
|
||||
return nil
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -206,6 +206,9 @@ var ActivityDetailsList = []ActivityDetails{
|
|||
ActivityAddedHydrant{},
|
||||
ActivityDeletedHydrant{},
|
||||
ActivityEditedHydrant{},
|
||||
ActivityAddedCustomESTProxy{},
|
||||
ActivityDeletedCustomESTProxy{},
|
||||
ActivityEditedCustomESTProxy{},
|
||||
ActivityAddedSmallstep{},
|
||||
ActivityDeletedSmallstep{},
|
||||
ActivityEditedSmallstep{},
|
||||
|
|
@ -2598,6 +2601,51 @@ func (a ActivityEditedHydrant) Documentation() (activity string, details string,
|
|||
}`
|
||||
}
|
||||
|
||||
type ActivityAddedCustomESTProxy struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (a ActivityAddedCustomESTProxy) ActivityName() string {
|
||||
return "added_custom_est_proxy"
|
||||
}
|
||||
|
||||
func (a ActivityAddedCustomESTProxy) Documentation() (activity string, details string, detailsExample string) {
|
||||
return "Generated when a custom EST certificate authority configuration is added in Fleet.", `This activity contains the following fields:
|
||||
- "name": Name of the certificate authority.`, `{
|
||||
"name": "EST_WIFI"
|
||||
}`
|
||||
}
|
||||
|
||||
type ActivityDeletedCustomESTProxy struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (a ActivityDeletedCustomESTProxy) ActivityName() string {
|
||||
return "deleted_custom_est_proxy"
|
||||
}
|
||||
|
||||
func (a ActivityDeletedCustomESTProxy) Documentation() (activity string, details string, detailsExample string) {
|
||||
return "Generated when a custom EST certificate authority configuration is deleted in Fleet.", `This activity contains the following fields:
|
||||
- "name": Name of the certificate authority.`, `{
|
||||
"name": "EST_WIFI"
|
||||
}`
|
||||
}
|
||||
|
||||
type ActivityEditedCustomESTProxy struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (a ActivityEditedCustomESTProxy) ActivityName() string {
|
||||
return "edited_custom_est_proxy"
|
||||
}
|
||||
|
||||
func (a ActivityEditedCustomESTProxy) Documentation() (activity string, details string, detailsExample string) {
|
||||
return "Generated when a custom EST certificate authority configuration is edited in Fleet.", `This activity contains the following fields:
|
||||
- "name": Name of the certificate authority.`, `{
|
||||
"name": "EST_WIFI"
|
||||
}`
|
||||
}
|
||||
|
||||
type ActivityAddedSmallstep struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ const (
|
|||
CATypeDigiCert CAType = "digicert"
|
||||
CATypeCustomSCEPProxy CAType = "custom_scep_proxy"
|
||||
CATypeHydrant CAType = "hydrant"
|
||||
CATypeCustomESTProxy CAType = "custom_est_proxy" // Enrollment over Secure Transport
|
||||
CATypeSmallstep CAType = "smallstep"
|
||||
)
|
||||
|
||||
|
|
@ -78,9 +79,9 @@ type CertificateAuthority struct {
|
|||
// Smallstep
|
||||
ChallengeURL *string `json:"challenge_url,omitempty" db:"challenge_url"`
|
||||
|
||||
// Username is stored by both Smallstep and NDES CA types
|
||||
// Username is stored by Smallstep, NDES, and EST CA types
|
||||
Username *string `json:"username,omitempty" db:"username"`
|
||||
// Password is stored by both Smallstep and NDES CA types
|
||||
// Password is stored by Smallstep, NDES, and EST CA types
|
||||
Password *string `json:"password,omitempty" db:"-"`
|
||||
|
||||
// Custom SCEP Proxy
|
||||
|
|
@ -103,6 +104,7 @@ type CertificateAuthorityPayload struct {
|
|||
NDESSCEPProxy *NDESSCEPProxyCA `json:"ndes_scep_proxy,omitempty"`
|
||||
CustomSCEPProxy *CustomSCEPProxyCA `json:"custom_scep_proxy,omitempty"`
|
||||
Hydrant *HydrantCA `json:"hydrant,omitempty"`
|
||||
CustomESTProxy *ESTProxyCA `json:"custom_est_proxy,omitempty"`
|
||||
Smallstep *SmallstepSCEPProxyCA `json:"smallstep,omitempty"`
|
||||
}
|
||||
|
||||
|
|
@ -135,6 +137,34 @@ func (d *DigiCertCA) Preprocess() {
|
|||
d.ProfileID = Preprocess(d.ProfileID)
|
||||
}
|
||||
|
||||
// Enrollment over Secure Transport Certificate Authority
|
||||
type ESTProxyCA struct {
|
||||
ID uint `json:"-"`
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func (h *ESTProxyCA) Equals(other *ESTProxyCA) bool {
|
||||
return h.Name == other.Name &&
|
||||
h.URL == other.URL &&
|
||||
h.Username == other.Username &&
|
||||
(h.Password == "" || h.Password == MaskedPassword || h.Password == other.Password)
|
||||
}
|
||||
|
||||
func (h *ESTProxyCA) NeedToVerify(other *ESTProxyCA) bool {
|
||||
return h.Name != other.Name ||
|
||||
h.URL != other.URL ||
|
||||
h.Username != other.Username ||
|
||||
!(h.Password == "" || h.Password == MaskedPassword || h.Password == other.Password)
|
||||
}
|
||||
|
||||
func (h *ESTProxyCA) Preprocess() {
|
||||
h.Name = Preprocess(h.Name)
|
||||
h.URL = Preprocess(h.URL)
|
||||
}
|
||||
|
||||
type HydrantCA struct {
|
||||
ID uint `json:"-"`
|
||||
Name string `json:"name"`
|
||||
|
|
@ -208,6 +238,7 @@ type CertificateAuthorityUpdatePayload struct {
|
|||
*NDESSCEPProxyCAUpdatePayload `json:"ndes_scep_proxy,omitempty"`
|
||||
*CustomSCEPProxyCAUpdatePayload `json:"custom_scep_proxy,omitempty"`
|
||||
*HydrantCAUpdatePayload `json:"hydrant,omitempty"`
|
||||
*CustomESTCAUpdatePayload `json:"custom_est_proxy,omitempty"`
|
||||
*SmallstepSCEPProxyCAUpdatePayload `json:"smallstep,omitempty"`
|
||||
}
|
||||
|
||||
|
|
@ -220,6 +251,9 @@ func (p *CertificateAuthorityUpdatePayload) ValidatePayload(privateKey string, e
|
|||
if p.HydrantCAUpdatePayload != nil {
|
||||
caInPayload++
|
||||
}
|
||||
if p.CustomESTCAUpdatePayload != nil {
|
||||
caInPayload++
|
||||
}
|
||||
if p.NDESSCEPProxyCAUpdatePayload != nil {
|
||||
caInPayload++
|
||||
}
|
||||
|
|
@ -375,6 +409,36 @@ func (hp *HydrantCAUpdatePayload) Preprocess() {
|
|||
}
|
||||
}
|
||||
|
||||
type CustomESTCAUpdatePayload struct {
|
||||
Name *string `json:"name"`
|
||||
URL *string `json:"url"`
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
}
|
||||
|
||||
// IsEmpty checks if the struct only has all empty values
|
||||
func (ep CustomESTCAUpdatePayload) IsEmpty() bool {
|
||||
return ep.Name == nil && ep.URL == nil && ep.Username == nil && ep.Password == nil
|
||||
}
|
||||
|
||||
// ValidateRelatedFields verifies that fields that are related to each other are set correctly.
|
||||
// For example if the Name is provided then the Username and Password must also be provided
|
||||
func (ep *CustomESTCAUpdatePayload) ValidateRelatedFields(errPrefix string, certName string) error {
|
||||
if ep.URL != nil && (ep.Username == nil || ep.Password == nil) {
|
||||
return &BadRequestError{Message: fmt.Sprintf(`%s"password" must be set when modifying "url" of an existing certificate authority: %s.`, errPrefix, certName)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ep *CustomESTCAUpdatePayload) Preprocess() {
|
||||
if ep.Name != nil {
|
||||
*ep.Name = Preprocess(*ep.Name)
|
||||
}
|
||||
if ep.URL != nil {
|
||||
*ep.URL = Preprocess(*ep.URL)
|
||||
}
|
||||
}
|
||||
|
||||
type SmallstepSCEPProxyCAUpdatePayload struct {
|
||||
Name *string `json:"name"`
|
||||
URL *string `json:"url"`
|
||||
|
|
@ -426,6 +490,7 @@ func (c *RequestCertificatePayload) AuthzType() string {
|
|||
}
|
||||
|
||||
type GroupedCertificateAuthorities struct {
|
||||
EST []ESTProxyCA `json:"custom_est_proxy"` // Enrollment over Secure Transport
|
||||
Hydrant []HydrantCA `json:"hydrant"`
|
||||
DigiCert []DigiCertCA `json:"digicert"`
|
||||
NDESSCEP *NDESSCEPProxyCA `json:"ndes_scep_proxy"`
|
||||
|
|
@ -437,6 +502,7 @@ func GroupCertificateAuthoritiesByType(cas []*CertificateAuthority) (*GroupedCer
|
|||
grouped := &GroupedCertificateAuthorities{
|
||||
DigiCert: []DigiCertCA{},
|
||||
Hydrant: []HydrantCA{},
|
||||
EST: []ESTProxyCA{},
|
||||
CustomScepProxy: []CustomSCEPProxyCA{},
|
||||
NDESSCEP: nil,
|
||||
Smallstep: []SmallstepSCEPProxyCA{},
|
||||
|
|
@ -476,6 +542,14 @@ func GroupCertificateAuthoritiesByType(cas []*CertificateAuthority) (*GroupedCer
|
|||
ClientID: *ca.ClientID,
|
||||
ClientSecret: *ca.ClientSecret,
|
||||
})
|
||||
case string(CATypeCustomESTProxy):
|
||||
grouped.EST = append(grouped.EST, ESTProxyCA{
|
||||
ID: ca.ID,
|
||||
Name: *ca.Name,
|
||||
URL: *ca.URL,
|
||||
Username: *ca.Username,
|
||||
Password: *ca.Password,
|
||||
})
|
||||
case string(CATypeCustomSCEPProxy):
|
||||
grouped.CustomScepProxy = append(grouped.CustomScepProxy, CustomSCEPProxyCA{
|
||||
ID: ca.ID,
|
||||
|
|
@ -582,6 +656,22 @@ func ValidateCertificateAuthoritiesSpec(incoming interface{}) (*GroupedCertifica
|
|||
groupedCAs.Hydrant = hydrantData
|
||||
}
|
||||
|
||||
if ESTCA, ok := spec.(map[string]any)["custom_est_proxy"]; !ok || ESTCA == nil {
|
||||
groupedCAs.EST = []ESTProxyCA{}
|
||||
} else {
|
||||
// We unmarshal EST CA integration into its dedicated type for additional validation
|
||||
estJSON, err := json.Marshal(spec.(map[string]any)["custom_est_proxy"])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("org_settings.certificate_authorities.custom_est_proxy cannot be marshalled into JSON: %w", err)
|
||||
}
|
||||
var estData []ESTProxyCA
|
||||
err = json.Unmarshal(estJSON, &estData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("org_settings.certificate_authorities.custom_est_proxy cannot be parsed: %w", err)
|
||||
}
|
||||
groupedCAs.EST = estData
|
||||
}
|
||||
|
||||
// TODO(sca): confirm this
|
||||
if smallstepSCEPCA, ok := spec.(map[string]interface{})["smallstep"]; !ok || smallstepSCEPCA == nil {
|
||||
groupedCAs.Smallstep = []SmallstepSCEPProxyCA{}
|
||||
|
|
|
|||
20
server/fleet/est_ca.go
Normal file
20
server/fleet/est_ca.go
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
package fleet
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type ESTCertificate struct {
|
||||
Certificate []byte
|
||||
}
|
||||
|
||||
type ESTService interface {
|
||||
// ValidateESTURL validates that the provided URL in the ESTProxyCA is reachable via the
|
||||
// /cacerts endpoint. It is not responsible for checking the credentials.
|
||||
ValidateESTURL(ctx context.Context, estProxyCA ESTProxyCA) error
|
||||
// GetCertificate retrieves a certificate from the EST CA using the provided CSR which must
|
||||
// be in base64 encoded PKCS#10 format(i.e. PEM format without the header, footer or newlines).
|
||||
// The CSR format must match the template configured on the EST server. The certificate is
|
||||
// returned in a similar format as the CSR(i.e. base64 encoded PKCS#7)
|
||||
GetCertificate(ctx context.Context, estProxyCA ESTProxyCA, csr string) (*ESTCertificate, error)
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package fleet
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type HydrantCertificate struct {
|
||||
Certificate []byte
|
||||
}
|
||||
|
||||
type HydrantService interface {
|
||||
// ValidateHydrantURL validates that the provided URL in the HydrantCA is reachable via the
|
||||
// /cacerts endpoint. It is not responsible for checking the credentials.
|
||||
ValidateHydrantURL(ctx context.Context, hydrantCA HydrantCA) error
|
||||
// GetCertificate retrieves a certificate from the Hydrant CA using the provided CSR which must
|
||||
// be in base64 encoded PKCS#10 format(i.e. PEM format without the header, footer or newlines).
|
||||
// The CSR format must match the template configured on the Hydrant server. The certificate is
|
||||
// returned in a similar format as the CSR(i.e. base64 encoded PKCS#7)
|
||||
GetCertificate(ctx context.Context, hydrantCA HydrantCA, csr string) (*HydrantCertificate, error)
|
||||
}
|
||||
|
|
@ -109,6 +109,13 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
|
|||
ClientSecret: "client-secret",
|
||||
}
|
||||
|
||||
goodESTCA := fleet.ESTProxyCA{
|
||||
Name: "VALID_EST",
|
||||
URL: mockHydrantServer.URL,
|
||||
Username: "username",
|
||||
Password: "password",
|
||||
}
|
||||
|
||||
// goodSmallstepCA is a base object for testing with a valid Smallstep SCEP CA. Copy it to override specific fields in tests.
|
||||
goodSmallstepCA := fleet.SmallstepSCEPProxyCA{
|
||||
Name: "VALID_SMALLSTEP_SCEP",
|
||||
|
|
@ -136,6 +143,13 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
|
|||
},
|
||||
DryRun: dryRun,
|
||||
}, nil
|
||||
case fleet.ESTProxyCA:
|
||||
return batchApplyCertificateAuthoritiesRequest{
|
||||
CertificateAuthorities: fleet.GroupedCertificateAuthorities{
|
||||
EST: []fleet.ESTProxyCA{v},
|
||||
},
|
||||
DryRun: dryRun,
|
||||
}, nil
|
||||
case fleet.NDESSCEPProxyCA:
|
||||
return batchApplyCertificateAuthoritiesRequest{
|
||||
CertificateAuthorities: fleet.GroupedCertificateAuthorities{
|
||||
|
|
@ -423,6 +437,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
|
|||
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
|
||||
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
|
||||
Hydrant: []fleet.HydrantCA{goodHydrantCA},
|
||||
EST: []fleet.ESTProxyCA{goodESTCA},
|
||||
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
|
||||
},
|
||||
DryRun: false,
|
||||
|
|
@ -445,6 +460,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
|
|||
DigiCert: []fleet.DigiCertCA{goodDigiCertCA, testCopy},
|
||||
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
|
||||
Hydrant: []fleet.HydrantCA{goodHydrantCA},
|
||||
EST: []fleet.ESTProxyCA{goodESTCA},
|
||||
},
|
||||
DryRun: false,
|
||||
}
|
||||
|
|
@ -461,6 +477,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
|
|||
DigiCert: []fleet.DigiCertCA{goodDigiCertCA, testCopy},
|
||||
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
|
||||
Hydrant: []fleet.HydrantCA{goodHydrantCA},
|
||||
EST: []fleet.ESTProxyCA{goodESTCA},
|
||||
},
|
||||
DryRun: false,
|
||||
}
|
||||
|
|
@ -474,6 +491,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
|
|||
DigiCert: []fleet.DigiCertCA{goodDigiCertCA, testCopy},
|
||||
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
|
||||
Hydrant: []fleet.HydrantCA{goodHydrantCA},
|
||||
EST: []fleet.ESTProxyCA{goodESTCA},
|
||||
},
|
||||
DryRun: false,
|
||||
}
|
||||
|
|
@ -487,6 +505,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
|
|||
DigiCert: []fleet.DigiCertCA{goodDigiCertCA, testCopy},
|
||||
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
|
||||
Hydrant: []fleet.HydrantCA{goodHydrantCA},
|
||||
EST: []fleet.ESTProxyCA{goodESTCA},
|
||||
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
|
||||
},
|
||||
DryRun: false,
|
||||
|
|
@ -764,6 +783,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
|
|||
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
|
||||
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
|
||||
Hydrant: []fleet.HydrantCA{goodHydrantCA},
|
||||
EST: []fleet.ESTProxyCA{goodESTCA},
|
||||
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
|
||||
},
|
||||
DryRun: false,
|
||||
|
|
@ -786,6 +806,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
|
|||
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
|
||||
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA, testCopy},
|
||||
Hydrant: []fleet.HydrantCA{goodHydrantCA},
|
||||
EST: []fleet.ESTProxyCA{goodESTCA},
|
||||
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
|
||||
},
|
||||
DryRun: false,
|
||||
|
|
@ -803,6 +824,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
|
|||
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
|
||||
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA, testCopy},
|
||||
Hydrant: []fleet.HydrantCA{goodHydrantCA},
|
||||
EST: []fleet.ESTProxyCA{goodESTCA},
|
||||
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
|
||||
},
|
||||
DryRun: false,
|
||||
|
|
@ -817,6 +839,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
|
|||
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
|
||||
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA, testCopy},
|
||||
Hydrant: []fleet.HydrantCA{goodHydrantCA},
|
||||
EST: []fleet.ESTProxyCA{goodESTCA},
|
||||
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
|
||||
},
|
||||
DryRun: false,
|
||||
|
|
@ -831,6 +854,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
|
|||
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
|
||||
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA, testCopy},
|
||||
Hydrant: []fleet.HydrantCA{goodHydrantCA},
|
||||
EST: []fleet.ESTProxyCA{goodESTCA},
|
||||
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
|
||||
},
|
||||
DryRun: false,
|
||||
|
|
@ -984,6 +1008,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
|
|||
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
|
||||
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
|
||||
Hydrant: []fleet.HydrantCA{goodHydrantCA},
|
||||
EST: []fleet.ESTProxyCA{goodESTCA},
|
||||
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
|
||||
},
|
||||
DryRun: false,
|
||||
|
|
@ -1006,6 +1031,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
|
|||
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
|
||||
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
|
||||
Hydrant: []fleet.HydrantCA{goodHydrantCA},
|
||||
EST: []fleet.ESTProxyCA{goodESTCA},
|
||||
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA, testCopy},
|
||||
},
|
||||
DryRun: false,
|
||||
|
|
@ -1023,6 +1049,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
|
|||
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
|
||||
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
|
||||
Hydrant: []fleet.HydrantCA{goodHydrantCA},
|
||||
EST: []fleet.ESTProxyCA{goodESTCA},
|
||||
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA, testCopy},
|
||||
},
|
||||
DryRun: false,
|
||||
|
|
@ -1037,6 +1064,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
|
|||
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
|
||||
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
|
||||
Hydrant: []fleet.HydrantCA{goodHydrantCA},
|
||||
EST: []fleet.ESTProxyCA{goodESTCA},
|
||||
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA, testCopy},
|
||||
},
|
||||
DryRun: false,
|
||||
|
|
@ -1051,6 +1079,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
|
|||
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
|
||||
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
|
||||
Hydrant: []fleet.HydrantCA{goodHydrantCA},
|
||||
EST: []fleet.ESTProxyCA{goodESTCA},
|
||||
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA, testCopy},
|
||||
},
|
||||
DryRun: false,
|
||||
|
|
@ -1240,6 +1269,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
|
|||
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
|
||||
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
|
||||
Hydrant: []fleet.HydrantCA{goodHydrantCA},
|
||||
EST: []fleet.ESTProxyCA{goodESTCA},
|
||||
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
|
||||
},
|
||||
DryRun: false,
|
||||
|
|
@ -1262,6 +1292,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
|
|||
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
|
||||
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
|
||||
Hydrant: []fleet.HydrantCA{goodHydrantCA, testCopy},
|
||||
EST: []fleet.ESTProxyCA{goodESTCA},
|
||||
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
|
||||
},
|
||||
DryRun: false,
|
||||
|
|
@ -1279,6 +1310,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
|
|||
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
|
||||
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
|
||||
Hydrant: []fleet.HydrantCA{goodHydrantCA, testCopy},
|
||||
EST: []fleet.ESTProxyCA{goodESTCA},
|
||||
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
|
||||
},
|
||||
DryRun: false,
|
||||
|
|
@ -1293,6 +1325,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
|
|||
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
|
||||
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
|
||||
Hydrant: []fleet.HydrantCA{goodHydrantCA, testCopy},
|
||||
EST: []fleet.ESTProxyCA{goodESTCA},
|
||||
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
|
||||
},
|
||||
DryRun: false,
|
||||
|
|
@ -1307,6 +1340,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
|
|||
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
|
||||
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
|
||||
Hydrant: []fleet.HydrantCA{goodHydrantCA, testCopy},
|
||||
EST: []fleet.ESTProxyCA{goodESTCA},
|
||||
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
|
||||
},
|
||||
DryRun: false,
|
||||
|
|
@ -1316,6 +1350,130 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
|
|||
|
||||
// TODO(hca): hydrant happy path and other specific tests
|
||||
})
|
||||
|
||||
t.Run("custom est", func(t *testing.T) {
|
||||
// run common invalid name test cases
|
||||
t.Run("invalid name", func(t *testing.T) {
|
||||
for _, tc := range invalidNameTestCases {
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
testCopy := goodESTCA
|
||||
testCopy.Name = tc.name
|
||||
req, err := newApplyRequest(testCopy, false)
|
||||
require.NoError(t, err)
|
||||
res := s.Do("POST", "/api/v1/fleet/spec/certificate_authorities", req, http.StatusUnprocessableEntity)
|
||||
errMsg := extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "certificate_authorities.custom_est_proxy")
|
||||
require.Contains(t, errMsg, tc.errMessage)
|
||||
})
|
||||
}
|
||||
})
|
||||
// run common invalid url test cases
|
||||
t.Run("invalid url", func(t *testing.T) {
|
||||
for _, tc := range invalidURLTestCases {
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
testCopy := goodESTCA
|
||||
testCopy.URL = tc.url
|
||||
req, err := newApplyRequest(testCopy, false)
|
||||
require.NoError(t, err)
|
||||
res := s.Do("POST", "/api/v1/fleet/spec/certificate_authorities", req, http.StatusUnprocessableEntity)
|
||||
errMsg := extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "certificate_authorities.custom_est_proxy")
|
||||
if tc.errMessage == "Invalid URL" {
|
||||
require.Contains(t, errMsg, "Invalid EST URL")
|
||||
} else {
|
||||
require.Contains(t, errMsg, tc.errMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// run additional duplicate name scenarios
|
||||
t.Run("duplicate names", func(t *testing.T) {
|
||||
// create one of each CA
|
||||
req := batchApplyCertificateAuthoritiesRequest{
|
||||
CertificateAuthorities: fleet.GroupedCertificateAuthorities{
|
||||
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
|
||||
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
|
||||
Hydrant: []fleet.HydrantCA{goodHydrantCA},
|
||||
EST: []fleet.ESTProxyCA{goodESTCA},
|
||||
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
|
||||
},
|
||||
DryRun: false,
|
||||
}
|
||||
_ = s.Do("POST", "/api/v1/fleet/spec/certificate_authorities", req, http.StatusOK)
|
||||
s.checkAppliedCAs(t, s.ds, req.CertificateAuthorities)
|
||||
|
||||
t.Cleanup(func() {
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
_, _ = q.ExecContext(context.Background(), "DELETE FROM certificate_authorities")
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
// try to create est ca proxy with same name as another est ca
|
||||
testCopy := goodESTCA
|
||||
testCopy.Username = "some-other-client-id"
|
||||
duplicateReq := batchApplyCertificateAuthoritiesRequest{
|
||||
CertificateAuthorities: fleet.GroupedCertificateAuthorities{
|
||||
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
|
||||
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
|
||||
Hydrant: []fleet.HydrantCA{goodHydrantCA},
|
||||
EST: []fleet.ESTProxyCA{goodESTCA, testCopy},
|
||||
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
|
||||
},
|
||||
DryRun: false,
|
||||
}
|
||||
res := s.Do("POST", "/api/v1/fleet/spec/certificate_authorities", duplicateReq, http.StatusUnprocessableEntity)
|
||||
errMsg := extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "certificate_authorities.custom_est_proxy")
|
||||
require.Contains(t, errMsg, "name is already used by another Custom EST Proxy certificate authority")
|
||||
|
||||
// try to create a custom est ca with same name as another digicert
|
||||
testCopy = goodESTCA
|
||||
testCopy.Name = goodDigiCertCA.Name
|
||||
duplicateReq = batchApplyCertificateAuthoritiesRequest{
|
||||
CertificateAuthorities: fleet.GroupedCertificateAuthorities{
|
||||
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
|
||||
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
|
||||
Hydrant: []fleet.HydrantCA{goodHydrantCA},
|
||||
EST: []fleet.ESTProxyCA{goodESTCA, testCopy},
|
||||
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
|
||||
},
|
||||
DryRun: false,
|
||||
}
|
||||
s.Do("POST", "/api/v1/fleet/spec/certificate_authorities", duplicateReq, http.StatusOK)
|
||||
|
||||
// try to create est with same name as another custom scep
|
||||
testCopy = goodESTCA
|
||||
testCopy.Name = goodCustomSCEPCA.Name
|
||||
duplicateReq = batchApplyCertificateAuthoritiesRequest{
|
||||
CertificateAuthorities: fleet.GroupedCertificateAuthorities{
|
||||
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
|
||||
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
|
||||
Hydrant: []fleet.HydrantCA{goodHydrantCA},
|
||||
EST: []fleet.ESTProxyCA{goodESTCA, testCopy},
|
||||
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
|
||||
},
|
||||
DryRun: false,
|
||||
}
|
||||
s.Do("POST", "/api/v1/fleet/spec/certificate_authorities", duplicateReq, http.StatusOK)
|
||||
|
||||
// try to create eset ca with same name as another smallstep
|
||||
testCopy = goodESTCA
|
||||
testCopy.Name = goodSmallstepCA.Name
|
||||
duplicateReq = batchApplyCertificateAuthoritiesRequest{
|
||||
CertificateAuthorities: fleet.GroupedCertificateAuthorities{
|
||||
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
|
||||
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
|
||||
Hydrant: []fleet.HydrantCA{goodHydrantCA},
|
||||
EST: []fleet.ESTProxyCA{goodESTCA, testCopy},
|
||||
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
|
||||
},
|
||||
DryRun: false,
|
||||
}
|
||||
s.Do("POST", "/api/v1/fleet/spec/certificate_authorities", duplicateReq, http.StatusOK)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) checkAppliedCAs(t *testing.T, ds fleet.Datastore, expectedCAs fleet.GroupedCertificateAuthorities) {
|
||||
|
|
@ -1371,6 +1529,22 @@ func (s *integrationMDMTestSuite) checkAppliedCAs(t *testing.T, ds fleet.Datasto
|
|||
assert.Empty(t, gotCAs.Hydrant)
|
||||
}
|
||||
|
||||
if len(expectedCAs.EST) != 0 {
|
||||
assert.Len(t, gotCAs.EST, len(expectedCAs.EST))
|
||||
wantByName := make(map[string]fleet.ESTProxyCA)
|
||||
gotByName := make(map[string]fleet.ESTProxyCA)
|
||||
for _, ca := range expectedCAs.EST {
|
||||
wantByName[ca.Name] = ca
|
||||
}
|
||||
for _, ca := range gotCAs.EST {
|
||||
ca.ID = 0 // ignore IDs when comparing
|
||||
gotByName[ca.Name] = ca
|
||||
}
|
||||
assert.Equal(t, wantByName, gotByName)
|
||||
} else {
|
||||
assert.Empty(t, gotCAs.EST)
|
||||
}
|
||||
|
||||
if expectedCAs.NDESSCEP != nil {
|
||||
assert.NotNil(t, gotCAs.NDESSCEP)
|
||||
gotCAs.NDESSCEP.ID = 0 // ignore ID when comparing
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/ee/server/scim"
|
||||
eeservice "github.com/fleetdm/fleet/v4/ee/server/service"
|
||||
"github.com/fleetdm/fleet/v4/ee/server/service/digicert"
|
||||
"github.com/fleetdm/fleet/v4/ee/server/service/est"
|
||||
"github.com/fleetdm/fleet/v4/ee/server/service/hostidentity"
|
||||
"github.com/fleetdm/fleet/v4/ee/server/service/hostidentity/httpsig"
|
||||
"github.com/fleetdm/fleet/v4/ee/server/service/hydrant"
|
||||
"github.com/fleetdm/fleet/v4/server/config"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/license"
|
||||
|
|
@ -76,7 +76,7 @@ func newTestServiceWithConfig(t *testing.T, ds fleet.Datastore, fleetConfig conf
|
|||
c clock.Clock = clock.C
|
||||
scepConfigService = eeservice.NewSCEPConfigService(logger, nil)
|
||||
digiCertService = digicert.NewService(digicert.WithLogger(logger))
|
||||
hydrantService = hydrant.NewService(hydrant.WithLogger(logger))
|
||||
estCAService = est.NewService(est.WithLogger(logger))
|
||||
conditionalAccessMicrosoftProxy ConditionalAccessMicrosoftProxy
|
||||
|
||||
mdmStorage fleet.MDMAppleStore
|
||||
|
|
@ -249,7 +249,7 @@ func newTestServiceWithConfig(t *testing.T, ds fleet.Datastore, fleetConfig conf
|
|||
keyValueStore,
|
||||
scepConfigService,
|
||||
digiCertService,
|
||||
hydrantService,
|
||||
estCAService,
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
|
|
|||
|
|
@ -12,8 +12,7 @@ import (
|
|||
func main() {
|
||||
// Ensure there's a make target specified.
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Println("Usage: fdm <command> [--option=value ...] -- [make-options]")
|
||||
os.Exit(1)
|
||||
os.Args = append(os.Args, "help")
|
||||
}
|
||||
|
||||
// Determine the path to the top-level directory (where the Makefile resides).
|
||||
|
|
|
|||
Loading…
Reference in a new issue