From 37722a925f45eca645dc7e0e2c57ec2eb49ac80d Mon Sep 17 00:00:00 2001 From: Dante Catalfamo <43040593+dantecatalfamo@users.noreply.github.com> Date: Tue, 4 Nov 2025 16:27:15 -0500 Subject: [PATCH] EST certificate proxy backend and configs (#34689) #34275 --- cmd/fleet/serve.go | 4 +- cmd/fleetctl/fleetctl/generate_gitops.go | 9 + cmd/fleetctl/fleetctl/generate_gitops_test.go | 8 + .../expectedOrgSettings-insecure.yaml | 5 + .../generateGitops/expectedOrgSettings.yaml | 19 +- .../generateGitops/test_dir_free/default.yml | 7 +- .../test_dir_premium/default.yml | 5 + docs/Contributing/reference/audit-logs.md | 45 ++++ ee/server/service/certificate_authorities.go | 202 ++++++++++++++++-- .../service/certificate_authorities_test.go | 6 +- .../{hydrant/hydrant.go => est/est.go} | 56 ++--- ee/server/service/request_certificate.go | 51 +++-- ee/server/service/request_certificate_test.go | 35 ++- ee/server/service/service.go | 6 +- .../mysql/certificate_authorities.go | 22 ++ .../mysql/certificate_authorities_test.go | 46 ++++ ...20251104112849_AddESTEnumValueToCATable.go | 25 +++ server/datastore/mysql/schema.sql | 6 +- server/fleet/activities.go | 48 +++++ server/fleet/certificate_authorities.go | 94 +++++++- server/fleet/est_ca.go | 20 ++ server/fleet/hydrant.go | 20 -- ...ntegration_certificate_authorities_test.go | 174 +++++++++++++++ server/service/testing_utils.go | 6 +- tools/fdm/main.go | 3 +- 25 files changed, 815 insertions(+), 107 deletions(-) rename ee/server/service/{hydrant/hydrant.go => est/est.go} (54%) create mode 100644 server/datastore/mysql/migrations/tables/20251104112849_AddESTEnumValueToCATable.go create mode 100644 server/fleet/est_ca.go delete mode 100644 server/fleet/hydrant.go diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index 78c9af96bd..464be66280 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -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() { diff --git a/cmd/fleetctl/fleetctl/generate_gitops.go b/cmd/fleetctl/fleetctl/generate_gitops.go index a4b648feed..fd8c52ac02 100644 --- a/cmd/fleetctl/fleetctl/generate_gitops.go +++ b/cmd/fleetctl/fleetctl/generate_gitops.go @@ -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") diff --git a/cmd/fleetctl/fleetctl/generate_gitops_test.go b/cmd/fleetctl/fleetctl/generate_gitops_test.go index 4ba2fb8b0f..ca977e4cee 100644 --- a/cmd/fleetctl/fleetctl/generate_gitops_test.go +++ b/cmd/fleetctl/fleetctl/generate_gitops_test.go @@ -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", diff --git a/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedOrgSettings-insecure.yaml b/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedOrgSettings-insecure.yaml index ad3e62382e..6907e916b2 100644 --- a/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedOrgSettings-insecure.yaml +++ b/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedOrgSettings-insecure.yaml @@ -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 diff --git a/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedOrgSettings.yaml b/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedOrgSettings.yaml index ae4b365a11..5f7ff643df 100644 --- a/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedOrgSettings.yaml +++ b/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedOrgSettings.yaml @@ -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: diff --git a/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_free/default.yml b/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_free/default.yml index bb4c3a7303..90150a73b0 100644 --- a/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_free/default.yml +++ b/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_free/default.yml @@ -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 diff --git a/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/default.yml b/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/default.yml index 6f7467c777..b0f63f5c9f 100644 --- a/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/default.yml +++ b/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/default.yml @@ -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 diff --git a/docs/Contributing/reference/audit-logs.md b/docs/Contributing/reference/audit-logs.md index 5cec3ca971..83d4bca567 100644 --- a/docs/Contributing/reference/audit-logs.md +++ b/docs/Contributing/reference/audit-logs.md @@ -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. diff --git a/ee/server/service/certificate_authorities.go b/ee/server/service/certificate_authorities.go index 508a20fae9..ee3f42190a 100644 --- a/ee/server/service/certificate_authorities.go +++ b/ee/server/service/certificate_authorities.go @@ -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 diff --git a/ee/server/service/certificate_authorities_test.go b/ee/server/service/certificate_authorities_test.go index 8f62996dc4..f9b47038de 100644 --- a/ee/server/service/certificate_authorities_test.go +++ b/ee/server/service/certificate_authorities_test.go @@ -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 }, diff --git a/ee/server/service/hydrant/hydrant.go b/ee/server/service/est/est.go similarity index 54% rename from ee/server/service/hydrant/hydrant.go rename to ee/server/service/est/est.go index 6a9bd69441..234a0771d8 100644 --- a/ee/server/service/hydrant/hydrant.go +++ b/ee/server/service/est/est.go @@ -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 } diff --git a/ee/server/service/request_certificate.go b/ee/server/service/request_certificate.go index 90cb0e8a51..93e6eb09bc 100644 --- a/ee/server/service/request_certificate.go +++ b/ee/server/service/request_certificate.go @@ -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 } diff --git a/ee/server/service/request_certificate_test.go b/ee/server/service/request_certificate_test.go index ad6dd530ba..13ecb92263 100644 --- a/ee/server/service/request_certificate_test.go +++ b/ee/server/service/request_certificate_test.go @@ -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) { diff --git a/ee/server/service/service.go b/ee/server/service/service.go index 770f8a7860..eaeb18d40d 100644 --- a/ee/server/service/service.go +++ b/ee/server/service/service.go @@ -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 diff --git a/server/datastore/mysql/certificate_authorities.go b/server/datastore/mysql/certificate_authorities.go index 11823df6ce..0a273698e3 100644 --- a/server/datastore/mysql/certificate_authorities.go +++ b/server/datastore/mysql/certificate_authorities.go @@ -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 = ?") diff --git a/server/datastore/mysql/certificate_authorities_test.go b/server/datastore/mysql/certificate_authorities_test.go index 4b4d639d68..9645c34477 100644 --- a/server/datastore/mysql/certificate_authorities_test.go +++ b/server/datastore/mysql/certificate_authorities_test.go @@ -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] diff --git a/server/datastore/mysql/migrations/tables/20251104112849_AddESTEnumValueToCATable.go b/server/datastore/mysql/migrations/tables/20251104112849_AddESTEnumValueToCATable.go new file mode 100644 index 0000000000..1d0ed30990 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20251104112849_AddESTEnumValueToCATable.go @@ -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 +} diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index 578c557733..2d4ebfccc2 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -227,7 +227,7 @@ CREATE TABLE `carve_metadata` ( /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `certificate_authorities` ( `id` int NOT NULL AUTO_INCREMENT, - `type` enum('digicert','ndes_scep_proxy','custom_scep_proxy','hydrant','smallstep') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `type` enum('digicert','ndes_scep_proxy','custom_scep_proxy','hydrant','smallstep','custom_est_proxy') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `url` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `api_token_encrypted` blob, @@ -1638,9 +1638,9 @@ CREATE TABLE `migration_status_tables` ( `is_applied` tinyint(1) NOT NULL, `tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) -) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=438 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=439 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,20241021224359,1,'2020-01-01 01:01:01'),(323,20241022140321,1,'2020-01-01 01:01:01'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'),(327,20241110152839,1,'2020-01-01 01:01:01'),(328,20241110152840,1,'2020-01-01 01:01:01'),(329,20241110152841,1,'2020-01-01 01:01:01'),(330,20241116233322,1,'2020-01-01 01:01:01'),(331,20241122171434,1,'2020-01-01 01:01:01'),(332,20241125150614,1,'2020-01-01 01:01:01'),(333,20241203125346,1,'2020-01-01 01:01:01'),(334,20241203130032,1,'2020-01-01 01:01:01'),(335,20241205122800,1,'2020-01-01 01:01:01'),(336,20241209164540,1,'2020-01-01 01:01:01'),(337,20241210140021,1,'2020-01-01 01:01:01'),(338,20241219180042,1,'2020-01-01 01:01:01'),(339,20241220100000,1,'2020-01-01 01:01:01'),(340,20241220114903,1,'2020-01-01 01:01:01'),(341,20241220114904,1,'2020-01-01 01:01:01'),(342,20241224000000,1,'2020-01-01 01:01:01'),(343,20241230000000,1,'2020-01-01 01:01:01'),(344,20241231112624,1,'2020-01-01 01:01:01'),(345,20250102121439,1,'2020-01-01 01:01:01'),(346,20250121094045,1,'2020-01-01 01:01:01'),(347,20250121094500,1,'2020-01-01 01:01:01'),(348,20250121094600,1,'2020-01-01 01:01:01'),(349,20250121094700,1,'2020-01-01 01:01:01'),(350,20250124194347,1,'2020-01-01 01:01:01'),(351,20250127162751,1,'2020-01-01 01:01:01'),(352,20250213104005,1,'2020-01-01 01:01:01'),(353,20250214205657,1,'2020-01-01 01:01:01'),(354,20250217093329,1,'2020-01-01 01:01:01'),(355,20250219090511,1,'2020-01-01 01:01:01'),(356,20250219100000,1,'2020-01-01 01:01:01'),(357,20250219142401,1,'2020-01-01 01:01:01'),(358,20250224184002,1,'2020-01-01 01:01:01'),(359,20250225085436,1,'2020-01-01 01:01:01'),(360,20250226000000,1,'2020-01-01 01:01:01'),(361,20250226153445,1,'2020-01-01 01:01:01'),(362,20250304162702,1,'2020-01-01 01:01:01'),(363,20250306144233,1,'2020-01-01 01:01:01'),(364,20250313163430,1,'2020-01-01 01:01:01'),(365,20250317130944,1,'2020-01-01 01:01:01'),(366,20250318165922,1,'2020-01-01 01:01:01'),(367,20250320132525,1,'2020-01-01 01:01:01'),(368,20250320200000,1,'2020-01-01 01:01:01'),(369,20250326161930,1,'2020-01-01 01:01:01'),(370,20250326161931,1,'2020-01-01 01:01:01'),(371,20250331042354,1,'2020-01-01 01:01:01'),(372,20250331154206,1,'2020-01-01 01:01:01'),(373,20250401155831,1,'2020-01-01 01:01:01'),(374,20250408133233,1,'2020-01-01 01:01:01'),(375,20250410104321,1,'2020-01-01 01:01:01'),(376,20250421085116,1,'2020-01-01 01:01:01'),(377,20250422095806,1,'2020-01-01 01:01:01'),(378,20250424153059,1,'2020-01-01 01:01:01'),(379,20250430103833,1,'2020-01-01 01:01:01'),(380,20250430112622,1,'2020-01-01 01:01:01'),(381,20250501162727,1,'2020-01-01 01:01:01'),(382,20250502154517,1,'2020-01-01 01:01:01'),(383,20250502222222,1,'2020-01-01 01:01:01'),(384,20250507170845,1,'2020-01-01 01:01:01'),(385,20250513162912,1,'2020-01-01 01:01:01'),(386,20250519161614,1,'2020-01-01 01:01:01'),(387,20250519170000,1,'2020-01-01 01:01:01'),(388,20250520153848,1,'2020-01-01 01:01:01'),(389,20250528115932,1,'2020-01-01 01:01:01'),(390,20250529102706,1,'2020-01-01 01:01:01'),(391,20250603105558,1,'2020-01-01 01:01:01'),(392,20250609102714,1,'2020-01-01 01:01:01'),(393,20250609112613,1,'2020-01-01 01:01:01'),(394,20250613103810,1,'2020-01-01 01:01:01'),(395,20250616193950,1,'2020-01-01 01:01:01'),(396,20250624140757,1,'2020-01-01 01:01:01'),(397,20250626130239,1,'2020-01-01 01:01:01'),(398,20250629131032,1,'2020-01-01 01:01:01'),(399,20250701155654,1,'2020-01-01 01:01:01'),(400,20250707095725,1,'2020-01-01 01:01:01'),(401,20250716152435,1,'2020-01-01 01:01:01'),(402,20250718091828,1,'2020-01-01 01:01:01'),(403,20250728122229,1,'2020-01-01 01:01:01'),(404,20250731122715,1,'2020-01-01 01:01:01'),(405,20250731151000,1,'2020-01-01 01:01:01'),(406,20250803000000,1,'2020-01-01 01:01:01'),(407,20250805083116,1,'2020-01-01 01:01:01'),(408,20250807140441,1,'2020-01-01 01:01:01'),(409,20250808000000,1,'2020-01-01 01:01:01'),(410,20250811155036,1,'2020-01-01 01:01:01'),(411,20250813205039,1,'2020-01-01 01:01:01'),(412,20250814123333,1,'2020-01-01 01:01:01'),(413,20250815130115,1,'2020-01-01 01:01:01'),(414,20250816115553,1,'2020-01-01 01:01:01'),(415,20250817154557,1,'2020-01-01 01:01:01'),(416,20250825113751,1,'2020-01-01 01:01:01'),(417,20250827113140,1,'2020-01-01 01:01:01'),(418,20250828120836,1,'2020-01-01 01:01:01'),(419,20250902112642,1,'2020-01-01 01:01:01'),(420,20250904091745,1,'2020-01-01 01:01:01'),(421,20250905090000,1,'2020-01-01 01:01:01'),(422,20250922083056,1,'2020-01-01 01:01:01'),(423,20250923120000,1,'2020-01-01 01:01:01'),(424,20250926123048,1,'2020-01-01 01:01:01'),(425,20251015103505,1,'2020-01-01 01:01:01'),(426,20251015103600,1,'2020-01-01 01:01:01'),(427,20251015103700,1,'2020-01-01 01:01:01'),(428,20251015103800,1,'2020-01-01 01:01:01'),(429,20251015103900,1,'2020-01-01 01:01:01'),(430,20251028140000,1,'2020-01-01 01:01:01'),(431,20251028140100,1,'2020-01-01 01:01:01'),(432,20251028140110,1,'2020-01-01 01:01:01'),(433,20251028140200,1,'2020-01-01 01:01:01'),(434,20251028140300,1,'2020-01-01 01:01:01'),(435,20251028140400,1,'2020-01-01 01:01:01'),(436,20251031154558,1,'2020-01-01 01:01:01'),(437,20251103160848,1,'2020-01-01 01:01:01'); +INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,20241021224359,1,'2020-01-01 01:01:01'),(323,20241022140321,1,'2020-01-01 01:01:01'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'),(327,20241110152839,1,'2020-01-01 01:01:01'),(328,20241110152840,1,'2020-01-01 01:01:01'),(329,20241110152841,1,'2020-01-01 01:01:01'),(330,20241116233322,1,'2020-01-01 01:01:01'),(331,20241122171434,1,'2020-01-01 01:01:01'),(332,20241125150614,1,'2020-01-01 01:01:01'),(333,20241203125346,1,'2020-01-01 01:01:01'),(334,20241203130032,1,'2020-01-01 01:01:01'),(335,20241205122800,1,'2020-01-01 01:01:01'),(336,20241209164540,1,'2020-01-01 01:01:01'),(337,20241210140021,1,'2020-01-01 01:01:01'),(338,20241219180042,1,'2020-01-01 01:01:01'),(339,20241220100000,1,'2020-01-01 01:01:01'),(340,20241220114903,1,'2020-01-01 01:01:01'),(341,20241220114904,1,'2020-01-01 01:01:01'),(342,20241224000000,1,'2020-01-01 01:01:01'),(343,20241230000000,1,'2020-01-01 01:01:01'),(344,20241231112624,1,'2020-01-01 01:01:01'),(345,20250102121439,1,'2020-01-01 01:01:01'),(346,20250121094045,1,'2020-01-01 01:01:01'),(347,20250121094500,1,'2020-01-01 01:01:01'),(348,20250121094600,1,'2020-01-01 01:01:01'),(349,20250121094700,1,'2020-01-01 01:01:01'),(350,20250124194347,1,'2020-01-01 01:01:01'),(351,20250127162751,1,'2020-01-01 01:01:01'),(352,20250213104005,1,'2020-01-01 01:01:01'),(353,20250214205657,1,'2020-01-01 01:01:01'),(354,20250217093329,1,'2020-01-01 01:01:01'),(355,20250219090511,1,'2020-01-01 01:01:01'),(356,20250219100000,1,'2020-01-01 01:01:01'),(357,20250219142401,1,'2020-01-01 01:01:01'),(358,20250224184002,1,'2020-01-01 01:01:01'),(359,20250225085436,1,'2020-01-01 01:01:01'),(360,20250226000000,1,'2020-01-01 01:01:01'),(361,20250226153445,1,'2020-01-01 01:01:01'),(362,20250304162702,1,'2020-01-01 01:01:01'),(363,20250306144233,1,'2020-01-01 01:01:01'),(364,20250313163430,1,'2020-01-01 01:01:01'),(365,20250317130944,1,'2020-01-01 01:01:01'),(366,20250318165922,1,'2020-01-01 01:01:01'),(367,20250320132525,1,'2020-01-01 01:01:01'),(368,20250320200000,1,'2020-01-01 01:01:01'),(369,20250326161930,1,'2020-01-01 01:01:01'),(370,20250326161931,1,'2020-01-01 01:01:01'),(371,20250331042354,1,'2020-01-01 01:01:01'),(372,20250331154206,1,'2020-01-01 01:01:01'),(373,20250401155831,1,'2020-01-01 01:01:01'),(374,20250408133233,1,'2020-01-01 01:01:01'),(375,20250410104321,1,'2020-01-01 01:01:01'),(376,20250421085116,1,'2020-01-01 01:01:01'),(377,20250422095806,1,'2020-01-01 01:01:01'),(378,20250424153059,1,'2020-01-01 01:01:01'),(379,20250430103833,1,'2020-01-01 01:01:01'),(380,20250430112622,1,'2020-01-01 01:01:01'),(381,20250501162727,1,'2020-01-01 01:01:01'),(382,20250502154517,1,'2020-01-01 01:01:01'),(383,20250502222222,1,'2020-01-01 01:01:01'),(384,20250507170845,1,'2020-01-01 01:01:01'),(385,20250513162912,1,'2020-01-01 01:01:01'),(386,20250519161614,1,'2020-01-01 01:01:01'),(387,20250519170000,1,'2020-01-01 01:01:01'),(388,20250520153848,1,'2020-01-01 01:01:01'),(389,20250528115932,1,'2020-01-01 01:01:01'),(390,20250529102706,1,'2020-01-01 01:01:01'),(391,20250603105558,1,'2020-01-01 01:01:01'),(392,20250609102714,1,'2020-01-01 01:01:01'),(393,20250609112613,1,'2020-01-01 01:01:01'),(394,20250613103810,1,'2020-01-01 01:01:01'),(395,20250616193950,1,'2020-01-01 01:01:01'),(396,20250624140757,1,'2020-01-01 01:01:01'),(397,20250626130239,1,'2020-01-01 01:01:01'),(398,20250629131032,1,'2020-01-01 01:01:01'),(399,20250701155654,1,'2020-01-01 01:01:01'),(400,20250707095725,1,'2020-01-01 01:01:01'),(401,20250716152435,1,'2020-01-01 01:01:01'),(402,20250718091828,1,'2020-01-01 01:01:01'),(403,20250728122229,1,'2020-01-01 01:01:01'),(404,20250731122715,1,'2020-01-01 01:01:01'),(405,20250731151000,1,'2020-01-01 01:01:01'),(406,20250803000000,1,'2020-01-01 01:01:01'),(407,20250805083116,1,'2020-01-01 01:01:01'),(408,20250807140441,1,'2020-01-01 01:01:01'),(409,20250808000000,1,'2020-01-01 01:01:01'),(410,20250811155036,1,'2020-01-01 01:01:01'),(411,20250813205039,1,'2020-01-01 01:01:01'),(412,20250814123333,1,'2020-01-01 01:01:01'),(413,20250815130115,1,'2020-01-01 01:01:01'),(414,20250816115553,1,'2020-01-01 01:01:01'),(415,20250817154557,1,'2020-01-01 01:01:01'),(416,20250825113751,1,'2020-01-01 01:01:01'),(417,20250827113140,1,'2020-01-01 01:01:01'),(418,20250828120836,1,'2020-01-01 01:01:01'),(419,20250902112642,1,'2020-01-01 01:01:01'),(420,20250904091745,1,'2020-01-01 01:01:01'),(421,20250905090000,1,'2020-01-01 01:01:01'),(422,20250922083056,1,'2020-01-01 01:01:01'),(423,20250923120000,1,'2020-01-01 01:01:01'),(424,20250926123048,1,'2020-01-01 01:01:01'),(425,20251015103505,1,'2020-01-01 01:01:01'),(426,20251015103600,1,'2020-01-01 01:01:01'),(427,20251015103700,1,'2020-01-01 01:01:01'),(428,20251015103800,1,'2020-01-01 01:01:01'),(429,20251015103900,1,'2020-01-01 01:01:01'),(430,20251028140000,1,'2020-01-01 01:01:01'),(431,20251028140100,1,'2020-01-01 01:01:01'),(432,20251028140110,1,'2020-01-01 01:01:01'),(433,20251028140200,1,'2020-01-01 01:01:01'),(434,20251028140300,1,'2020-01-01 01:01:01'),(435,20251028140400,1,'2020-01-01 01:01:01'),(436,20251031154558,1,'2020-01-01 01:01:01'),(437,20251103160848,1,'2020-01-01 01:01:01'),(438,20251104112849,1,'2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `mobile_device_management_solutions` ( diff --git a/server/fleet/activities.go b/server/fleet/activities.go index e1f3aa9dc6..de29d0fe34 100644 --- a/server/fleet/activities.go +++ b/server/fleet/activities.go @@ -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"` } diff --git a/server/fleet/certificate_authorities.go b/server/fleet/certificate_authorities.go index 1c44c01c83..0a87e17c87 100644 --- a/server/fleet/certificate_authorities.go +++ b/server/fleet/certificate_authorities.go @@ -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{} diff --git a/server/fleet/est_ca.go b/server/fleet/est_ca.go new file mode 100644 index 0000000000..2d1954af77 --- /dev/null +++ b/server/fleet/est_ca.go @@ -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) +} diff --git a/server/fleet/hydrant.go b/server/fleet/hydrant.go deleted file mode 100644 index c87b9a9b5b..0000000000 --- a/server/fleet/hydrant.go +++ /dev/null @@ -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) -} diff --git a/server/service/integration_certificate_authorities_test.go b/server/service/integration_certificate_authorities_test.go index b420fd311a..0616f6a014 100644 --- a/server/service/integration_certificate_authorities_test.go +++ b/server/service/integration_certificate_authorities_test.go @@ -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 diff --git a/server/service/testing_utils.go b/server/service/testing_utils.go index 265cdb1835..e7aad3c07a 100644 --- a/server/service/testing_utils.go +++ b/server/service/testing_utils.go @@ -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) diff --git a/tools/fdm/main.go b/tools/fdm/main.go index a134018504..723ae1d72a 100644 --- a/tools/fdm/main.go +++ b/tools/fdm/main.go @@ -12,8 +12,7 @@ import ( func main() { // Ensure there's a make target specified. if len(os.Args) < 2 { - fmt.Println("Usage: fdm [--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).