From 1adc51b2a49be5e11c7b8a34b2f7d38c6e40a372 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Tue, 1 Oct 2024 09:18:58 -0500 Subject: [PATCH 01/27] Added configs --- cmd/fleet/serve.go | 7 ++ ee/server/service/scep_proxy.go | 110 ++++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 + server/datastore/mysql/app_configs.go | 29 +++++++ server/fleet/integrations.go | 9 +++ server/fleet/mdm.go | 3 + server/mdm/scep/server/endpoint.go | 32 ++++++++ server/mdm/scep/server/transport.go | 49 ++++++++++++ server/service/appconfig.go | 35 ++++++++ server/service/handler.go | 18 +++++ 11 files changed, 295 insertions(+) create mode 100644 ee/server/service/scep_proxy.go diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index 02353ffe3183..8af84d88a998 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -1108,6 +1108,13 @@ the way that the Fleet server works. } } + // SCEP proxy (for NDES, etc.) + if license.IsPremium() { + if err = service.RegisterSCEPProxy(rootMux, logger); err != nil { + initFatal(err, "setup SCEP proxy") + } + } + if config.Prometheus.BasicAuth.Username != "" && config.Prometheus.BasicAuth.Password != "" { rootMux.Handle("/metrics", basicAuthHandler( config.Prometheus.BasicAuth.Username, diff --git a/ee/server/service/scep_proxy.go b/ee/server/service/scep_proxy.go new file mode 100644 index 000000000000..ff426dd9f8cb --- /dev/null +++ b/ee/server/service/scep_proxy.go @@ -0,0 +1,110 @@ +package service + +import ( + "context" + "errors" + "io" + "net/http" + "regexp" + "strings" + + "github.com/Azure/go-ntlmssp" + "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" + "github.com/fleetdm/fleet/v4/server/fleet" + scepclient "github.com/fleetdm/fleet/v4/server/mdm/scep/client" + scepserver "github.com/fleetdm/fleet/v4/server/mdm/scep/server" + "github.com/go-kit/log" + "golang.org/x/text/encoding/unicode" + "golang.org/x/text/transform" +) + +var _ scepserver.Service = (*scepProxyService)(nil) +var challengeRegex = regexp.MustCompile(`(?i)The enrollment challenge password is: (?P\S*)`) + +const fullPasswordCache = "The password cache is full." + +type scepProxyService struct { + // info logging is implemented in the service middleware layer. + debugLogger log.Logger +} + +func (svc *scepProxyService) GetCACaps(_ context.Context) ([]byte, error) { + return nil, errors.New("not implemented") +} + +func (svc *scepProxyService) GetCACert(_ context.Context, _ string) ([]byte, int, error) { + return nil, 0, errors.New("not implemented") +} + +func (svc *scepProxyService) PKIOperation(_ context.Context, data []byte) ([]byte, error) { + return nil, errors.New("not implemented") +} + +func (svc *scepProxyService) GetNextCACert(ctx context.Context) ([]byte, error) { + return nil, errors.New("not implemented") +} + +// NewSCEPProxyService creates a new scep proxy service +func NewSCEPProxyService(logger log.Logger) scepserver.Service { + return &scepProxyService{ + debugLogger: logger, + } +} + +func ValidateNDESSCEPAdminURL(ctx context.Context, proxy *fleet.NDESSCEPProxyIntegration) error { + adminURL, username, password := proxy.AdminURL, proxy.Username, proxy.Password + // Get the challenge from NDES + client := &http.Client{ + Transport: ntlmssp.Negotiator{ + RoundTripper: &http.Transport{}, + }, + } + req, err := http.NewRequest(http.MethodGet, adminURL, http.NoBody) + if err != nil { + return ctxerr.Wrap(ctx, err, "creating request") + } + req.SetBasicAuth(username, password) + resp, err := client.Do(req) + if err != nil { + return ctxerr.Wrap(ctx, err, "sending request") + } + // Make a transformer that converts MS-Win default to UTF8: + win16be := unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM) + // Make a transformer that is like win16be, but abides by BOM: + utf16bom := unicode.BOMOverride(win16be.NewDecoder()) + + // Make a Reader that uses utf16bom: + unicodeReader := transform.NewReader(resp.Body, utf16bom) + bodyText, err := io.ReadAll(unicodeReader) + htmlString := string(bodyText) + + matches := challengeRegex.FindStringSubmatch(htmlString) + challenge := "" + if matches != nil { + challenge = matches[challengeRegex.SubexpIndex("password")] + } + if challenge == "" { + if strings.Contains(htmlString, fullPasswordCache) { + return ctxerr.New(ctx, + "the password cache is full; please increase the number of cached passwords by NDES; by default, NDES caches 5 password and they expire 60 minutes after they are created") + } + return ctxerr.New(ctx, "could not retrieve the enrollment challenge password") + } + return nil +} + +func ValidateNDESSCEPURL(ctx context.Context, proxy *fleet.NDESSCEPProxyIntegration, logger log.Logger) error { + client, err := scepclient.New(proxy.URL, logger) + if err != nil { + return ctxerr.Wrap(ctx, err, "creating SCEP client") + } + + _, certNum, err := client.GetCACert(ctx, "") + if err != nil { + return ctxerr.Wrap(ctx, err, "could not retrieve CA certificate from SCEP URL") + } + if certNum < 1 { + return ctxerr.New(ctx, "SCEP URL did not return a CA certificate") + } + return nil +} diff --git a/go.mod b/go.mod index 83670b64fdd1..144be92910eb 100644 --- a/go.mod +++ b/go.mod @@ -162,6 +162,7 @@ require ( github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect github.com/BurntSushi/toml v1.3.2 // indirect github.com/DisgoOrg/disgohook v1.4.3 // indirect github.com/DisgoOrg/log v1.1.0 // indirect diff --git a/go.sum b/go.sum index 8bf177e83697..fabc33d24932 100644 --- a/go.sum +++ b/go.sum @@ -145,6 +145,8 @@ github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+Z github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= diff --git a/server/datastore/mysql/app_configs.go b/server/datastore/mysql/app_configs.go index 1b8bf9b4b4c6..2f1f735768d1 100644 --- a/server/datastore/mysql/app_configs.go +++ b/server/datastore/mysql/app_configs.go @@ -1,6 +1,7 @@ package mysql import ( + "bytes" "context" "database/sql" "encoding/json" @@ -49,6 +50,20 @@ func appConfigDB(ctx context.Context, q sqlx.QueryerContext) (*fleet.AppConfig, } func (ds *Datastore) SaveAppConfig(ctx context.Context, info *fleet.AppConfig) error { + // Check if passwords need to be encrypted + if info.Integrations.NDESSCEPProxy != nil { + if info.Integrations.NDESSCEPProxy.Password != "" && info.Integrations.NDESSCEPProxy.Password != fleet.MaskedPassword { + err := ds.insertOrReplaceConfigAsset(ctx, fleet.MDMConfigAsset{ + Name: fleet.MDMAssetNDESPassword, + Value: []byte(info.Integrations.NDESSCEPProxy.Password), + }) + if err != nil { + return ctxerr.Wrap(ctx, err, "processing NDES SCEP proxy password") + } + } + info.Integrations.NDESSCEPProxy.Password = fleet.MaskedPassword + } + configBytes, err := json.Marshal(info) if err != nil { return ctxerr.Wrap(ctx, err, "marshaling config") @@ -67,6 +82,20 @@ func (ds *Datastore) SaveAppConfig(ctx context.Context, info *fleet.AppConfig) e }) } +func (ds *Datastore) insertOrReplaceConfigAsset(ctx context.Context, asset fleet.MDMConfigAsset) error { + assets, err := ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{asset.Name}) + if err != nil { + return ctxerr.Wrap(ctx, err, "get all mdm config assets by name") + } + if len(assets) == 0 { + return ds.InsertMDMConfigAssets(ctx, []fleet.MDMConfigAsset{asset}) + } else if !bytes.Equal(assets[asset.Name].Value, asset.Value) { + return ds.ReplaceMDMConfigAssets(ctx, []fleet.MDMConfigAsset{asset}) + } + // asset already exists and is the same, so not need to update + return nil +} + func (ds *Datastore) VerifyEnrollSecret(ctx context.Context, secret string) (*fleet.EnrollSecret, error) { var s fleet.EnrollSecret err := sqlx.GetContext(ctx, ds.reader(ctx), &s, "SELECT team_id FROM enroll_secrets WHERE secret = ?", secret) diff --git a/server/fleet/integrations.go b/server/fleet/integrations.go index a4fab4c0f749..4ecc8db7ea91 100644 --- a/server/fleet/integrations.go +++ b/server/fleet/integrations.go @@ -352,11 +352,20 @@ type GoogleCalendarIntegration struct { ApiKey map[string]string `json:"api_key_json"` } +// NDESSCEPProxyIntegration configures SCEP proxy for NDES SCEP server. Premium feature. +type NDESSCEPProxyIntegration struct { + URL string `json:"url"` + AdminURL string `json:"admin_url"` + Username string `json:"username"` + Password string `json:"password"` // not stored here -- encrypted in DB +} + // Integrations configures the integrations with external systems. type Integrations struct { Jira []*JiraIntegration `json:"jira"` Zendesk []*ZendeskIntegration `json:"zendesk"` GoogleCalendar []*GoogleCalendarIntegration `json:"google_calendar"` + NDESSCEPProxy *NDESSCEPProxyIntegration `json:"ndes_scep_proxy"` } func ValidateEnabledActivitiesWebhook(webhook ActivitiesWebhookSettings, invalid *InvalidArgumentError) { diff --git a/server/fleet/mdm.go b/server/fleet/mdm.go index 0e5cf329717c..f5afed1e45e2 100644 --- a/server/fleet/mdm.go +++ b/server/fleet/mdm.go @@ -676,6 +676,9 @@ const ( // Deprecated: VPP tokens are now stored in the vpp_tokens table, they are // not in mdm_config_assets anymore. MDMAssetVPPTokenDeprecated MDMAssetName = "vpp_token" + // MDMAssetNDESPassword is the password used to retrieve SCEP challenge from + // NDES SCEP server. It is used by Fleet's SCEP proxy. + MDMAssetNDESPassword MDMAssetName = "ndes_password" ) type MDMConfigAsset struct { diff --git a/server/mdm/scep/server/endpoint.go b/server/mdm/scep/server/endpoint.go index e865a3341653..1d23b27ea6e5 100644 --- a/server/mdm/scep/server/endpoint.go +++ b/server/mdm/scep/server/endpoint.go @@ -101,6 +101,14 @@ func MakeServerEndpoints(svc Service) *Endpoints { } } +func MakeServerEndpointsWithIdentifier(svc Service) *Endpoints { + e := MakeSCEPEndpointWithIdentifier(svc) + return &Endpoints{ + GetEndpoint: e, + PostEndpoint: e, + } +} + // MakeClientEndpoints returns an Endpoints struct where each endpoint invokes // the corresponding method on the remote instance, via a transport/http.Client. // Useful in a SCEP client. @@ -157,6 +165,30 @@ type SCEPRequest struct { func (r SCEPRequest) scepOperation() string { return r.Operation } +func MakeSCEPEndpointWithIdentifier(svc Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(SCEPRequestWithIdentifier) + resp := SCEPResponse{operation: req.Operation} + switch req.Operation { + case "GetCACaps": + resp.Data, resp.Err = svc.GetCACaps(ctx) + case "GetCACert": + resp.Data, resp.CACertNum, resp.Err = svc.GetCACert(ctx, string(req.Message)) + case "PKIOperation": + resp.Data, resp.Err = svc.PKIOperation(ctx, req.Message) + default: + return nil, &BadRequestError{Message: "operation not implemented"} + } + return resp, nil + } +} + +// SCEPRequestWithIdentifier is a SCEP server request. +type SCEPRequestWithIdentifier struct { + SCEPRequest + Identifier string `url:"identifier"` +} + // SCEPResponse is a SCEP server response. // Business errors will be encoded as a CertRep message // with pkiStatus FAILURE and a failInfo attribute. diff --git a/server/mdm/scep/server/transport.go b/server/mdm/scep/server/transport.go index 69f704e3c8a5..a55d06b35875 100644 --- a/server/mdm/scep/server/transport.go +++ b/server/mdm/scep/server/transport.go @@ -11,6 +11,7 @@ import ( "net/http" "net/url" + "github.com/go-kit/kit/transport" kithttp "github.com/go-kit/kit/transport/http" kitlog "github.com/go-kit/log" "github.com/gorilla/mux" @@ -40,6 +41,29 @@ func MakeHTTPHandler(e *Endpoints, svc Service, logger kitlog.Logger) http.Handl return r } +func MakeHTTPHandlerWithIdentifier(e *Endpoints, rootPath string, logger kitlog.Logger) http.Handler { + opts := []kithttp.ServerOption{ + kithttp.ServerErrorHandler(transport.NewLogErrorHandler(logger)), + kithttp.ServerFinalizer(logutil.NewHTTPLogger(logger).LoggingFinalizer), + } + + r := mux.NewRouter() + r.Path(rootPath + "{identifier}").Methods("GET").Handler(kithttp.NewServer( + e.GetEndpoint, + decodeSCEPRequestWithIdentifier, + encodeSCEPResponse, + opts..., + )) + r.Path(rootPath + "{identifier}").Methods("POST").Handler(kithttp.NewServer( + e.PostEndpoint, + decodeSCEPRequestWithIdentifier, + encodeSCEPResponse, + opts..., + )) + + return r +} + // EncodeSCEPRequest encodes a SCEP HTTP Request. Used by the client. func EncodeSCEPRequest(ctx context.Context, r *http.Request, request interface{}) error { req := request.(SCEPRequest) @@ -98,6 +122,31 @@ func decodeSCEPRequest(ctx context.Context, r *http.Request) (interface{}, error return request, nil } +func decodeSCEPRequestWithIdentifier(_ context.Context, r *http.Request) (interface{}, error) { + msg, err := message(r) + if err != nil { + return nil, err + } + defer r.Body.Close() + + operation := r.URL.Query().Get("operation") + identifier := mux.Vars(r)["identifier"] + // TODO: verify identifier + if len(operation) == 0 { + return nil, &BadRequestError{Message: "missing operation"} + } + + request := SCEPRequestWithIdentifier{ + SCEPRequest: SCEPRequest{ + Message: msg, + Operation: r.URL.Query().Get("operation"), + }, + Identifier: identifier, + } + + return request, nil +} + // extract message from request func message(r *http.Request) ([]byte, error) { switch r.Method { diff --git a/server/service/appconfig.go b/server/service/appconfig.go index 22998d24d601..ca4da04505dd 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -14,6 +14,7 @@ import ( "net/http" "net/url" + eeservice "github.com/fleetdm/fleet/v4/ee/server/service" "github.com/fleetdm/fleet/v4/pkg/optjson" "github.com/fleetdm/fleet/v4/pkg/rawjson" "github.com/fleetdm/fleet/v4/server/authz" @@ -424,6 +425,40 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle return nil, ctxerr.Wrap(ctx, err, "validating VPP token assignments") } + // Validate NDES SCEP URLs if they changed. Validation is done in both dry run and normal mode. + if license.IsPremium() && newAppConfig.Integrations.NDESSCEPProxy != nil { + validateAdminURL, validateSCEPURL := false, false + if oldAppConfig.Integrations.NDESSCEPProxy == nil { + validateAdminURL, validateSCEPURL = true, true + } else { + newSCEPProxy := newAppConfig.Integrations.NDESSCEPProxy + oldSCEPProxy := oldAppConfig.Integrations.NDESSCEPProxy + if newSCEPProxy.URL != oldSCEPProxy.URL { + validateSCEPURL = true + } + if newSCEPProxy.AdminURL != oldSCEPProxy.AdminURL || newSCEPProxy.Username != oldSCEPProxy.Username { + validateAdminURL = true + } else if newSCEPProxy.Password != "" && newSCEPProxy.Password != fleet.MaskedPassword { + // We do not update password if it is empty or masked + validateAdminURL = true + } + } + + if validateAdminURL { + if err = eeservice.ValidateNDESSCEPAdminURL(ctx, newAppConfig.Integrations.NDESSCEPProxy); err != nil { + invalid.Append("integrations.ndes_scep_proxy", err.Error()) + } + } + + if validateSCEPURL { + if err = eeservice.ValidateNDESSCEPURL(ctx, newAppConfig.Integrations.NDESSCEPProxy, svc.logger); err != nil { + invalid.Append("integrations.ndes_scep_proxy.url", err.Error()) + } + } + } else { + appConfig.Integrations.NDESSCEPProxy = nil + } + if invalid.HasErrors() { return nil, ctxerr.Wrap(ctx, invalid) } diff --git a/server/service/handler.go b/server/service/handler.go index 7443436b8e42..a7041c83af9f 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -7,6 +7,7 @@ import ( "net/http" "regexp" + eeservice "github.com/fleetdm/fleet/v4/ee/server/service" "github.com/fleetdm/fleet/v4/server/config" "github.com/fleetdm/fleet/v4/server/contexts/logging" "github.com/fleetdm/fleet/v4/server/contexts/publicip" @@ -1123,6 +1124,23 @@ func registerSCEP( return nil } +func RegisterSCEPProxy( + rootMux *http.ServeMux, + logger kitlog.Logger, +) error { + scepService := eeservice.NewSCEPProxyService( + kitlog.With(logger, "component", "scep-proxy-service"), + ) + scepLogger := kitlog.With(logger, "component", "http-scep-proxy") + e := scepserver.MakeServerEndpointsWithIdentifier(scepService) + e.GetEndpoint = scepserver.EndpointLoggingMiddleware(scepLogger)(e.GetEndpoint) + e.PostEndpoint = scepserver.EndpointLoggingMiddleware(scepLogger)(e.PostEndpoint) + rootPath := "/mdm/scep/proxy/" + scepHandler := scepserver.MakeHTTPHandlerWithIdentifier(e, rootPath, scepLogger) + rootMux.Handle(rootPath, scepHandler) + return nil +} + // NanoMDMLogger is a logger adapter for nanomdm. type NanoMDMLogger struct { logger kitlog.Logger From 00af4022f42d7aa4dc9d59f6c506d63dc45e5191 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Tue, 1 Oct 2024 10:15:36 -0500 Subject: [PATCH 02/27] Test fixes. --- .../expectedGetConfigAppConfigJson.json | 3 +- .../expectedGetConfigAppConfigYaml.yml | 1 + ...ectedGetConfigIncludeServerConfigJson.json | 3 +- ...pectedGetConfigIncludeServerConfigYaml.yml | 1 + .../macosSetupExpectedAppConfigEmpty.yml | 1 + .../macosSetupExpectedAppConfigSet.yml | 1 + ee/server/service/scep_proxy.go | 11 ++-- server/datastore/mysql/app_configs.go | 14 ++++- server/datastore/mysql/app_configs_test.go | 63 +++++++++++++++++++ server/datastore/mysql/schema.sql | 2 +- 10 files changed, 91 insertions(+), 9 deletions(-) diff --git a/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json b/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json index ece537e7b95f..f1fca92952d0 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json +++ b/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json @@ -90,7 +90,8 @@ "integrations": { "jira": null, "zendesk": null, - "google_calendar": null + "google_calendar": null, + "ndes_scep_proxy": null }, "mdm": { "apple_bm_terms_expired": false, diff --git a/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml b/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml index 5f19ffcb8ac0..a79e0f0b3681 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml +++ b/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml @@ -16,6 +16,7 @@ spec: integrations: google_calendar: null jira: null + ndes_scep_proxy: null zendesk: null mdm: apple_bm_terms_expired: false diff --git a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json index 9fa625b676af..ce6f6c4c22bb 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json +++ b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json @@ -140,7 +140,8 @@ "integrations": { "jira": null, "zendesk": null, - "google_calendar": null + "google_calendar": null, + "ndes_scep_proxy": null }, "update_interval": { "osquery_detail": "1h0m0s", diff --git a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml index b28ab395b3e9..bc910c8ede31 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml +++ b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml @@ -16,6 +16,7 @@ spec: integrations: google_calendar: null jira: null + ndes_scep_proxy: null zendesk: null mdm: apple_business_manager: null diff --git a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml index 02adaad3acfc..c2b0a4fed468 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml @@ -16,6 +16,7 @@ spec: integrations: google_calendar: null jira: null + ndes_scep_proxy: null zendesk: null mdm: apple_business_manager: diff --git a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml index 2dd2f93adf1f..b2c5fec52e85 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml @@ -16,6 +16,7 @@ spec: integrations: google_calendar: null jira: null + ndes_scep_proxy: null zendesk: null mdm: apple_business_manager: diff --git a/ee/server/service/scep_proxy.go b/ee/server/service/scep_proxy.go index ff426dd9f8cb..cd70dbe7a8ea 100644 --- a/ee/server/service/scep_proxy.go +++ b/ee/server/service/scep_proxy.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/Azure/go-ntlmssp" + "github.com/fleetdm/fleet/v4/pkg/fleethttp" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" scepclient "github.com/fleetdm/fleet/v4/server/mdm/scep/client" @@ -54,10 +55,9 @@ func NewSCEPProxyService(logger log.Logger) scepserver.Service { func ValidateNDESSCEPAdminURL(ctx context.Context, proxy *fleet.NDESSCEPProxyIntegration) error { adminURL, username, password := proxy.AdminURL, proxy.Username, proxy.Password // Get the challenge from NDES - client := &http.Client{ - Transport: ntlmssp.Negotiator{ - RoundTripper: &http.Transport{}, - }, + client := fleethttp.NewClient() + client.Transport = ntlmssp.Negotiator{ + RoundTripper: fleethttp.NewTransport(), } req, err := http.NewRequest(http.MethodGet, adminURL, http.NoBody) if err != nil { @@ -76,6 +76,9 @@ func ValidateNDESSCEPAdminURL(ctx context.Context, proxy *fleet.NDESSCEPProxyInt // Make a Reader that uses utf16bom: unicodeReader := transform.NewReader(resp.Body, utf16bom) bodyText, err := io.ReadAll(unicodeReader) + if err != nil { + return ctxerr.Wrap(ctx, err, "reading response body") + } htmlString := string(bodyText) matches := challengeRegex.FindStringSubmatch(htmlString) diff --git a/server/datastore/mysql/app_configs.go b/server/datastore/mysql/app_configs.go index 2f1f735768d1..74d093c40577 100644 --- a/server/datastore/mysql/app_configs.go +++ b/server/datastore/mysql/app_configs.go @@ -85,11 +85,21 @@ func (ds *Datastore) SaveAppConfig(ctx context.Context, info *fleet.AppConfig) e func (ds *Datastore) insertOrReplaceConfigAsset(ctx context.Context, asset fleet.MDMConfigAsset) error { assets, err := ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{asset.Name}) if err != nil { + if fleet.IsNotFound(err) { + return ds.InsertMDMConfigAssets(ctx, []fleet.MDMConfigAsset{asset}) + } return ctxerr.Wrap(ctx, err, "get all mdm config assets by name") } if len(assets) == 0 { - return ds.InsertMDMConfigAssets(ctx, []fleet.MDMConfigAsset{asset}) - } else if !bytes.Equal(assets[asset.Name].Value, asset.Value) { + // Should never happen + return ctxerr.New(ctx, fmt.Sprintf("no asset found for name %s", asset.Name)) + } + currentAsset, ok := assets[asset.Name] + if !ok { + // Should never happen + return ctxerr.New(ctx, fmt.Sprintf("asset not found for name %s", asset.Name)) + } + if !bytes.Equal(currentAsset.Value, asset.Value) { return ds.ReplaceMDMConfigAssets(ctx, []fleet.MDMConfigAsset{asset}) } // asset already exists and is the same, so not need to update diff --git a/server/datastore/mysql/app_configs_test.go b/server/datastore/mysql/app_configs_test.go index 5357860ebdca..e8ada564ed58 100644 --- a/server/datastore/mysql/app_configs_test.go +++ b/server/datastore/mysql/app_configs_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/fleetdm/fleet/v4/pkg/optjson" + "github.com/fleetdm/fleet/v4/server/contexts/ctxdb" "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/fleet" @@ -35,6 +36,7 @@ func TestAppConfig(t *testing.T) { {"Backwards Compatibility", testAppConfigBackwardsCompatibility}, {"GetConfigEnableDiskEncryption", testGetConfigEnableDiskEncryption}, {"IsEnrollSecretAvailable", testIsEnrollSecretAvailable}, + {"NDESSCEPProxyPassword", testNDESSCEPProxyPassword}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { @@ -533,3 +535,64 @@ func testIsEnrollSecretAvailable(t *testing.T, ds *Datastore) { } } + +func testNDESSCEPProxyPassword(t *testing.T, ds *Datastore) { + ctx := context.Background() + ctx = ctxdb.BypassCachedMysql(ctx, true) + defer TruncateTables(t, ds) + + ac, err := ds.AppConfig(ctx) + require.NoError(t, err) + + adminURL := "https://localhost:8080/mscep_admin/" + username := "admin" + url := "https://localhost:8080/mscep/mscep.dll" + password := "password" + + ac.Integrations.NDESSCEPProxy = &fleet.NDESSCEPProxyIntegration{ + AdminURL: adminURL, + Username: username, + Password: password, + URL: url, + } + + err = ds.SaveAppConfig(ctx, ac) + require.NoError(t, err) + + checkProxyConfig := func() { + result, err := ds.AppConfig(ctx) + require.NoError(t, err) + require.NotNil(t, result.Integrations.NDESSCEPProxy) + assert.Equal(t, url, result.Integrations.NDESSCEPProxy.URL) + assert.Equal(t, adminURL, result.Integrations.NDESSCEPProxy.AdminURL) + assert.Equal(t, username, result.Integrations.NDESSCEPProxy.Username) + assert.Equal(t, fleet.MaskedPassword, result.Integrations.NDESSCEPProxy.Password) + } + + checkProxyConfig() + + checkPassword := func() { + assets, err := ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetNDESPassword}) + require.NoError(t, err) + require.Len(t, assets, 1) + assert.Equal(t, password, string(assets[fleet.MDMAssetNDESPassword].Value)) + } + checkPassword() + + // Set password to masked password -- should not update + ac.Integrations.NDESSCEPProxy.Password = fleet.MaskedPassword + err = ds.SaveAppConfig(ctx, ac) + require.NoError(t, err) + checkProxyConfig() + checkPassword() + + // Set password to empty -- should not update + url = "https://newurl.com" + ac.Integrations.NDESSCEPProxy.Password = "" + ac.Integrations.NDESSCEPProxy.URL = url + err = ds.SaveAppConfig(ctx, ac) + require.NoError(t, err) + checkProxyConfig() + checkPassword() + +} diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index 5755e67d0f0a..c45f4d3a3fc8 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -65,7 +65,7 @@ CREATE TABLE `app_config_json` ( UNIQUE KEY `id` (`id`) ) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"ios_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_setup\": {\"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false, \"enable_release_device_manually\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"ipados_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"windows_updates\": {\"deadline_days\": null, \"grace_period_days\": null}, \"windows_settings\": {\"custom_settings\": null}, \"apple_bm_terms_expired\": false, \"apple_business_manager\": null, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"volume_purchasing_program\": null, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null, \"google_calendar\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"query_report_cap\": 0, \"scripts_disabled\": false, \"deferred_save_host\": false, \"live_query_disabled\": false, \"ai_features_disabled\": false, \"query_reports_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"activities_webhook\": {\"destination_url\": \"\", \"enable_activities_webhook\": false}, \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}, \"activity_expiry_settings\": {\"activity_expiry_window\": 0, \"activity_expiry_enabled\": false}}','2020-01-01 01:01:01','2020-01-01 01:01:01'); +INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"ios_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_setup\": {\"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false, \"enable_release_device_manually\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"ipados_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"windows_updates\": {\"deadline_days\": null, \"grace_period_days\": null}, \"windows_settings\": {\"custom_settings\": null}, \"apple_bm_terms_expired\": false, \"apple_business_manager\": null, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"volume_purchasing_program\": null, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null, \"google_calendar\": null, \"ndes_scep_proxy\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"query_report_cap\": 0, \"scripts_disabled\": false, \"deferred_save_host\": false, \"live_query_disabled\": false, \"ai_features_disabled\": false, \"query_reports_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"activities_webhook\": {\"destination_url\": \"\", \"enable_activities_webhook\": false}, \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}, \"activity_expiry_settings\": {\"activity_expiry_window\": 0, \"activity_expiry_enabled\": false}}','2020-01-01 01:01:01','2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `calendar_events` ( From e42a4c8afe954aba796663ff310576b3e23294eb Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Tue, 1 Oct 2024 10:51:57 -0500 Subject: [PATCH 03/27] Added admin url test. --- ee/server/service/scep_proxy.go | 4 + ee/server/service/scep_proxy_test.go | 85 +++++++++++++++++++ .../testdata/mscep_admin_cache_full.html | 1 + .../testdata/mscep_admin_password.html | 1 + .../generated_files/appconfig.txt | 5 ++ 5 files changed, 96 insertions(+) create mode 100644 ee/server/service/scep_proxy_test.go create mode 100644 ee/server/service/testdata/mscep_admin_cache_full.html create mode 100644 ee/server/service/testdata/mscep_admin_password.html diff --git a/ee/server/service/scep_proxy.go b/ee/server/service/scep_proxy.go index cd70dbe7a8ea..49b562079398 100644 --- a/ee/server/service/scep_proxy.go +++ b/ee/server/service/scep_proxy.go @@ -3,6 +3,7 @@ package service import ( "context" "errors" + "fmt" "io" "net/http" "regexp" @@ -68,6 +69,9 @@ func ValidateNDESSCEPAdminURL(ctx context.Context, proxy *fleet.NDESSCEPProxyInt if err != nil { return ctxerr.Wrap(ctx, err, "sending request") } + if resp.StatusCode != http.StatusOK { + return ctxerr.New(ctx, fmt.Sprintf("unexpected status code: %d", resp.StatusCode)) + } // Make a transformer that converts MS-Win default to UTF8: win16be := unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM) // Make a transformer that is like win16be, but abides by BOM: diff --git a/ee/server/service/scep_proxy_test.go b/ee/server/service/scep_proxy_test.go new file mode 100644 index 000000000000..1a1a5777c643 --- /dev/null +++ b/ee/server/service/scep_proxy_test.go @@ -0,0 +1,85 @@ +package service + +import ( + "context" + "encoding/binary" + "net/http" + "net/http/httptest" + "os" + "syscall" + "testing" + "unicode/utf16" + + "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestValidateNDESSCEPAdminURL(t *testing.T) { + var returnPage func() []byte + returnStatus := http.StatusOK + ndesAdminServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(returnStatus) + if returnStatus == http.StatusOK { + _, err := w.Write(returnPage()) + require.NoError(t, err) + } + })) + t.Cleanup(ndesAdminServer.Close) + + proxy := &fleet.NDESSCEPProxyIntegration{ + AdminURL: ndesAdminServer.URL, + Username: "admin", + Password: "password", + } + + returnStatus = http.StatusNotFound + err := ValidateNDESSCEPAdminURL(context.Background(), proxy) + assert.ErrorContains(t, err, "unexpected status code") + + // We need to convert the HTML page to UTF-16 encoding, which is used by Windows servers + returnPageFromFile := func(path string) []byte { + dat, err := os.ReadFile(path) + require.NoError(t, err) + datUTF16, err := utf16FromString(string(dat)) + require.NoError(t, err) + byteData := make([]byte, len(datUTF16)*2) + for i, v := range datUTF16 { + binary.BigEndian.PutUint16(byteData[i*2:], v) + } + return byteData + } + + // Catch ths issue when NDES password cache is full + returnStatus = http.StatusOK + returnPage = func() []byte { + return returnPageFromFile("./testdata/mscep_admin_cache_full.html") + } + err = ValidateNDESSCEPAdminURL(context.Background(), proxy) + assert.ErrorContains(t, err, "the password cache is full") + + // Nothing returned + returnPage = func() []byte { + return []byte{} + } + err = ValidateNDESSCEPAdminURL(context.Background(), proxy) + assert.ErrorContains(t, err, "could not retrieve the enrollment challenge password") + + // All good + returnPage = func() []byte { + return returnPageFromFile("./testdata/mscep_admin_password.html") + } + err = ValidateNDESSCEPAdminURL(context.Background(), proxy) + assert.NoError(t, err) +} + +// utf16FromString returns the UTF-16 encoding of the UTF-8 string s, with a terminating NUL added. +// If s contains a NUL byte at any location, it returns (nil, syscall.EINVAL). +func utf16FromString(s string) ([]uint16, error) { + for i := 0; i < len(s); i++ { + if s[i] == 0 { + return nil, syscall.EINVAL + } + } + return utf16.Encode([]rune(s + "\x00")), nil +} diff --git a/ee/server/service/testdata/mscep_admin_cache_full.html b/ee/server/service/testdata/mscep_admin_cache_full.html new file mode 100644 index 000000000000..9f87b8ac5847 --- /dev/null +++ b/ee/server/service/testdata/mscep_admin_cache_full.html @@ -0,0 +1 @@ +Network Device Enrollment Service
Network Device Enrollment Service

Network Device Enrollment Service allows you to obtain certificates for routers or other network devices using the Simple Certificate Enrollment Protocol (SCEP).

The password cache is full.

Network Device Enrollment Service stores unused password for later use. By default, passwords are stored for 60 minutes.

Use one of the existing passwords. If you cannot use an existing password:

  • Wait until one or more existing passwords expire (by default passwords expire 60 minutes after they are created).
  • Restart Internet Information Services (IIS).
  • Configure the service to cache more than 5 passwords.

For more information see Using Network Device Enrollment Service .

\ No newline at end of file diff --git a/ee/server/service/testdata/mscep_admin_password.html b/ee/server/service/testdata/mscep_admin_password.html new file mode 100644 index 000000000000..c2462ee90cc9 --- /dev/null +++ b/ee/server/service/testdata/mscep_admin_password.html @@ -0,0 +1 @@ +Network Device Enrollment Service
Network Device Enrollment Service

Network Device Enrollment Service allows you to obtain certificates for routers or other network devices using the Simple Certificate Enrollment Protocol (SCEP).

To complete certificate enrollment for your network device you will need the following information:

The thumbprint (hash value) for the CA certificate is: A656FA66 AB12B433 A2DA5CF7 CC153D9A

The enrollment challenge password is: 8CE317021F690069

This password can be used only once and will expire within 60 minutes.

Each enrollment requires a new challenge password. You can refresh this web page to obtain a new challenge password.

For more information see Using Network Device Enrollment Service .

\ No newline at end of file diff --git a/tools/cloner-check/generated_files/appconfig.txt b/tools/cloner-check/generated_files/appconfig.txt index 86388d153e44..47299f2daa42 100644 --- a/tools/cloner-check/generated_files/appconfig.txt +++ b/tools/cloner-check/generated_files/appconfig.txt @@ -95,6 +95,11 @@ github.com/fleetdm/fleet/v4/server/fleet/ZendeskIntegration EnableSoftwareVulner github.com/fleetdm/fleet/v4/server/fleet/Integrations GoogleCalendar []*fleet.GoogleCalendarIntegration github.com/fleetdm/fleet/v4/server/fleet/GoogleCalendarIntegration Domain string github.com/fleetdm/fleet/v4/server/fleet/GoogleCalendarIntegration ApiKey map[string]string +github.com/fleetdm/fleet/v4/server/fleet/Integrations NDESSCEPProxy *fleet.NDESSCEPProxyIntegration +github.com/fleetdm/fleet/v4/server/fleet/NDESSCEPProxyIntegration URL string +github.com/fleetdm/fleet/v4/server/fleet/NDESSCEPProxyIntegration AdminURL string +github.com/fleetdm/fleet/v4/server/fleet/NDESSCEPProxyIntegration Username string +github.com/fleetdm/fleet/v4/server/fleet/NDESSCEPProxyIntegration Password string github.com/fleetdm/fleet/v4/server/fleet/AppConfig MDM fleet.MDM github.com/fleetdm/fleet/v4/server/fleet/MDM DeprecatedAppleBMDefaultTeam string github.com/fleetdm/fleet/v4/server/fleet/MDM AppleBusinessManager optjson.Slice[github.com/fleetdm/fleet/v4/server/fleet.MDMAppleABMAssignmentInfo] From 65b3a15ad4d51c133c34f444785820ba2331b528 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Tue, 1 Oct 2024 14:48:51 -0500 Subject: [PATCH 04/27] Finished config testing. --- ee/server/service/scep_proxy.go | 4 +- ee/server/service/scep_proxy_test.go | 62 ++++++++++ ee/server/service/testdata/testca/ca.key | 54 +++++++++ ee/server/service/testdata/testca/ca.pem | 30 +++++ server/datastore/mysql/app_configs_test.go | 10 +- server/fleet/app.go | 3 + server/service/appconfig.go | 73 +++++------ server/service/appconfig_test.go | 135 +++++++++++++++++++++ 8 files changed, 334 insertions(+), 37 deletions(-) create mode 100644 ee/server/service/testdata/testca/ca.key create mode 100644 ee/server/service/testdata/testca/ca.pem diff --git a/ee/server/service/scep_proxy.go b/ee/server/service/scep_proxy.go index 49b562079398..d7575ec4e3dd 100644 --- a/ee/server/service/scep_proxy.go +++ b/ee/server/service/scep_proxy.go @@ -106,11 +106,11 @@ func ValidateNDESSCEPURL(ctx context.Context, proxy *fleet.NDESSCEPProxyIntegrat return ctxerr.Wrap(ctx, err, "creating SCEP client") } - _, certNum, err := client.GetCACert(ctx, "") + certs, _, err := client.GetCACert(ctx, "") if err != nil { return ctxerr.Wrap(ctx, err, "could not retrieve CA certificate from SCEP URL") } - if certNum < 1 { + if len(certs) == 0 { return ctxerr.New(ctx, "SCEP URL did not return a CA certificate") } return nil diff --git a/ee/server/service/scep_proxy_test.go b/ee/server/service/scep_proxy_test.go index 1a1a5777c643..97527508e825 100644 --- a/ee/server/service/scep_proxy_test.go +++ b/ee/server/service/scep_proxy_test.go @@ -2,6 +2,7 @@ package service import ( "context" + "crypto/x509" "encoding/binary" "net/http" "net/http/httptest" @@ -11,11 +12,18 @@ import ( "unicode/utf16" "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/mdm/scep/depot" + filedepot "github.com/fleetdm/fleet/v4/server/mdm/scep/depot/file" + scepserver "github.com/fleetdm/fleet/v4/server/mdm/scep/server" + kitlog "github.com/go-kit/log" + "github.com/gorilla/mux" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestValidateNDESSCEPAdminURL(t *testing.T) { + t.Parallel() + var returnPage func() []byte returnStatus := http.StatusOK ndesAdminServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -73,6 +81,22 @@ func TestValidateNDESSCEPAdminURL(t *testing.T) { assert.NoError(t, err) } +func TestValidateNDESSCEPURL(t *testing.T) { + t.Parallel() + srv := newSCEPServer(t) + + proxy := &fleet.NDESSCEPProxyIntegration{ + URL: srv.URL + "/scep", + } + err := ValidateNDESSCEPURL(context.Background(), proxy, kitlog.NewNopLogger()) + assert.NoError(t, err) + + proxy.URL = srv.URL + "/bozo" + err = ValidateNDESSCEPURL(context.Background(), proxy, kitlog.NewNopLogger()) + assert.ErrorContains(t, err, "could not retrieve CA certificate") + +} + // utf16FromString returns the UTF-16 encoding of the UTF-8 string s, with a terminating NUL added. // If s contains a NUL byte at any location, it returns (nil, syscall.EINVAL). func utf16FromString(s string) ([]uint16, error) { @@ -83,3 +107,41 @@ func utf16FromString(s string) ([]uint16, error) { } return utf16.Encode([]rune(s + "\x00")), nil } + +func newSCEPServer(t *testing.T) *httptest.Server { + var err error + var certDepot depot.Depot // cert storage + depotPath := "./testdata/testca" + t.Cleanup(func() { + _ = os.Remove("./testdata/testca/serial") + _ = os.Remove("./testdata/testca/index.txt") + }) + certDepot, err = filedepot.NewFileDepot(depotPath) + if err != nil { + t.Fatal(err) + } + certDepot = &noopDepot{certDepot} + crt, key, err := certDepot.CA([]byte{}) + if err != nil { + t.Fatal(err) + } + var svc scepserver.Service // scep service + svc, err = scepserver.NewService(crt[0], key, scepserver.NopCSRSigner()) + if err != nil { + t.Fatal(err) + } + logger := kitlog.NewNopLogger() + e := scepserver.MakeServerEndpoints(svc) + scepHandler := scepserver.MakeHTTPHandler(e, svc, logger) + r := mux.NewRouter() + r.Handle("/scep", scepHandler) + server := httptest.NewServer(r) + t.Cleanup(server.Close) + return server +} + +type noopDepot struct{ depot.Depot } + +func (d *noopDepot) Put(_ string, _ *x509.Certificate) error { + return nil +} diff --git a/ee/server/service/testdata/testca/ca.key b/ee/server/service/testdata/testca/ca.key new file mode 100644 index 000000000000..1614d5541c9e --- /dev/null +++ b/ee/server/service/testdata/testca/ca.key @@ -0,0 +1,54 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,85186376af1462c4 + +jY1gAV22U2GeDZW0cVFw41uABS7fe7zKQen4aQQvkFJ5DPRlZFMI2qZ2MI1Oo265 +ek1Of/pcV52ct139NuVg9JZwkPPog40xUDn72IanZ2ZvJl/dcoHFn99T816Hu0p7 +YGKpvyyy3VYuxaarZV2aUNFye/o4bnAh4P0db6qa7/sGAKAhJ9PusNcSAXWWMz1l +QSCkdD30KrZOtf39StVnNSf2vPWAjAR/w3fEOVKqEehANP7yptDVOthiqrN+p58Q +kOGf3RnA5BJ21EY09W8rbgKE18EPI0UH9LVZEvRabZd4e/VSRNL6leNO7AiMLqA0 +3P7DAvjQDeTpYu3tZNWKFlmXv28KjwNo/4mclQTM8k5nkpQcLhGEVJanMlz0NAvZ ++BOgHGt1Vd8fXp5vBEMIz0tj7jJJnyEyd6tLRc7Pm7GaFjRy70rr/ZCO27HnKeWi +BVwFmZqG6Bcc1WvOGH/w549q/Xq1B3EgjSCShd7WQqINXMFOLJRg+MVZ9/EgWTrT +AEMkWwozb7hDJ56IdjE6PUop/5nbH87YjXHreV4kaGxgz3xD+sj6MGl8uw2JeT7b +6mFrK2d+704NU0Z56w8i0ydYeNn1uFmZqL9alYTDmAjOORXAR/ApvY6ctPXSpPTW +bXgv7LNWbcD8cNWuf/24dpI+kxbrIsKdGmucjQQ066Ce9qa2Kr7HEpH/CxxKCuBx +9KTHpb2ZZI1j6Zd9DQarEQm2D9fPaqEIq9XH46tNq8twXXEaYSTwwodSkwlovB5n +e4HlbiSuHB78ej2lQyFKquqWVYMRQ3dk5CUem/4XPF0L8dPvnifoQgBMpvJvzCG5 +BsIDQXKf0qLhQPrXwemhgY8fnZqDpuRTD6mdEXoPqvJC5L+3hzpPXHtCU94oqIbq +z4lkG1ARi9yS+WfUbXXZO95+7EBBg4lXzEZvXjqY6epUVjWCnoa873H9zfMZBuLL +XkxMQyDOnXaqYeqNsCahbdH4zuobR1SCNL4nt3iSaADaN6Lezwz8LPHxoM1kG1i6 +fvPa/uRo9aVsfWsovO+od2VmqLh1sfPZoOenZSKQsAVPYmEuV8XXVJ/B8NVvNTrt +DrfAR+vFe41liMHdTUndo8uG9/IO7JNC8u98zWjyvcr6cCukqE9H60Y9QvDgaSK5 +yD/D7B4ZAp6UjHOtD+jY1mjV+aL/2XeHJyQaDczHUKm1Vd5Um2c5f6NkZycrbtGE +7z5lR2SccnbbG6XVngYiZxdMLZCrQUnSfhke+zhzYM2Ng/7fxyz3mTyG5EYvxreU +6i0Psceh+vD9IEGHYbpRfV4Uozmk7AhEOfQN3ZDXZTA4LB5Svp7j3DcOmAGtCWGx +PWA3su4KzwrW40b5ommDhndPZNoSJfsrw2GJHV62AdfIxmAi5zvALJ31YdYvZsz2 +e8cf1Cl5oxeF/jgewEy6RTSkOUjvb0iTfVgreu4Tk/sBW37jdKhfW32INasCgEYb +0fq9DLXVcDk2neH/Sb78cE26JNXS3EtW1V4dvdkhvOjqRFP8O8vFggLi1mFQltAH +pmV143MSNkC/ikyOBahpQjGu89HZ0sLnJr2kzKf5LJTcN7kYAfxRejS7ofUByME1 +O9mrHOZGGNVNIgNesBXv42UEd0/SzwF4UKxHY72sEoTNLXliroaJORYbbvWw4GDI +91/vHKJMqMimoC37soS16wrsP/SabzusUXBayHD/PLkkmHBPV9++cO79b+HbVB0Q +6OpxBY7u6QhZnfTJv/W/InG404pumq8oz6bt7bXurbfC2QzviNHuyZ/IenbQ/y41 +K5URD3fdFYLC3OS38SSBBq32yncjJam0FOj2joUZ4iAAXSju1NSDskT8WbVy3BOq +tdTxekrxM9w98p17Og+Uf8966H2mQUIrz53Umc9V1974TVWdu0Y862ghJGSeLEbH +617VGwNN9hINdQE+iYaAVvbogEKSdCfljyVdIx1MuS1jeae5wUgReqqE+bopYgJm +oIXlVNI7tWX2y3JdG1vqCqKpq/UDzciLxAUdyGgwZESt9T3mvqQcdvWxsfREBGwX +XzbiDiGoom735dOOaGxvmyZUtJi7r5AonzJpR+qaRWoHNr8cqeU9be1wxBZ57Kln +2eKpwPIwdBTwxCjnc/kstuTsR45M8G37zOgh9XK38jS6FB/FzFytHtt9oPQBZBeb +3A6p7kqbbb4ynAgDiGEz9ExNNIQf3hQo9RAiaL2WeS9FTFB02hq5QgsgrGVXrR2V +45CKzP874sMPYP8xFQvmrMAXDy//zBXaOrNJHyNOVrtDLPerBNIC6GSKtFp30ynz +Te6GHFhcwqWfrN8N1l2oM79xvc2aKlsvI+YN0xTQklxqSdyCJSdhRUxmCIMN1JM0 +13Ean0HtO+z9u/nH3T2GtAhNySJAPAOXIAAER/74WNXNJNi7SmptNtWJOKKeKK3m +Jon7XC3Bx5NTnTM6UjrrXvwXvsJyf8G+SlkoZXZx9izgQYAANAsSblieSvPVppwM +/EfU6HIby2cBLQ1wTJiEDjYu7E1JKpAPBhqL0cN7aJea9tV7bmjoqzKhbwxACHkI +ymOZ1BDIF67M5fCLFCnCZEJcl2sgx4bRBaP6+p0uRWhplrus+8x1LAtNyB+V17in +nXacPqGELgqv+F6embq03retfaCbIwLwQYmaMU+QHg9jHc9j1AIf6fHSxhIRUPUz +PWMhy7dJdUcmm2GX2EGBrr7jH+H2y33W7y+0I2a4s5WdpIWsYUMFiBU+M+qJdAwY +O/n1Q8ZPdKdY9+c2RMzeO6Zvyc7f1hwoOy0FuYi748qaELV6rx1Tr2MDWl5/uhUa +vYMF4RshsKJY9OCUKvL9waqELZf4zEPyu875ZLm9eoJV2MFcokUuPcpAN+ljj6mx +S+1O9/kRioHo7FMs9rU3bHbCMbphLc0NdI363L/sM2kSFjRWxYv87z5fEQAoZGQR +d7HePVRbp09GC9Jk9p28F6ysgqS7PwlreRRp3Dj5vFJ422QviUWTP/jLj1QfukQR +0KXZhKhs0iSmfW9vlFnADS32l67fmycHMlN9yktvzcytm6dZ/XiQMHVDhZPlIGVC +frJ2R1MhmAdFEgIPZZGuoHeXFdlYq9HMpM9lbykJ1L7M36XqaW6GgRTnhf2g4iKJ +-----END RSA PRIVATE KEY----- diff --git a/ee/server/service/testdata/testca/ca.pem b/ee/server/service/testdata/testca/ca.pem new file mode 100644 index 000000000000..037b296cdee8 --- /dev/null +++ b/ee/server/service/testdata/testca/ca.pem @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFODCCAyCgAwIBAgIBATANBgkqhkiG9w0BAQsFADAtMQwwCgYDVQQGEwNVU0Ex +EDAOBgNVBAoTB2V0Y2QtY2ExCzAJBgNVBAsTAkNBMB4XDTE2MDUyOTEzNDcwNVoX +DTI2MDUyOTEzNDcwOFowLTEMMAoGA1UEBhMDVVNBMRAwDgYDVQQKEwdldGNkLWNh +MQswCQYDVQQLEwJDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALEG +S866Uf79znmx8+BakJ17tox8VYem0NZzPc2jF4RVWXfT481Yz9jdsjZubMCFuJiI +JzpMBT7RzXvZvuzMzZEe77Tb0mM+83t5kVwWWuxkEz7HQn0tWxuLR7NGaAi5MH53 +pcSGRNH8RgC7WdhyQ/3HwNGWObe0wQT69tfz1pHDSvNR9v7DS9KIiGsMc+dcqayz +n3YQuwEV8nD1KGenxEFjFh0NsP5FKrzDrsvzdFOWLJ3jedfDCSQSe0y33syZIYAQ +wS2/b+io6GMWDQemcirN9QiI1NGkcN9zioPRuYPxkaxGNa0O+3cTgA8egTFMigvI +4ZFsmERfZkJM4sBMK1uUmxXKb87nA1zooPvPk1KGQChXBEnrkHPbkP1VO+yYOS4m +t9LDweGVS6GoC5vjqQgymOHecaNfKpBnU6t7fP/aEZUF+6mxRKofolR/hTknkVNc +q2nrXEJpz8J73Iq8rkL0rNAEu1h83npPAoUgdFhwHzlq9ShRbz+ZQTxdAv5MOVs+ +6F9qcmbv/6C4xc1N1xH2NAJ8aFZTxsw4ny43hi7DgyRh1LJxcb2Bp7JMaD56CMSA +0zJqxIiV5kGUwbmrBjXMyvjYzx/0qI3j3bZl3p8BjZgyjkvOP0nArP3bby5mEUYx +i7+YgPm8dfGIzPh19I4oFReszOJl+JrdLnbf45efAgMBAAGjYzBhMA4GA1UdDwEB +/wQEAwICBDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBT6XD/PBaV7GbFEnxOm +3OJ3deamkzAfBgNVHSMEGDAWgBT6XD/PBaV7GbFEnxOm3OJ3deamkzANBgkqhkiG +9w0BAQsFAAOCAgEAC6yBHrRElZ7ovDrqjVBf8fLG+nINETPJ/kPTlTNtvqClLaeE +NKPH6JVp0/uusoKmqvE0LxyBEdP7waHQVq2XnfYggDCNjAUFxdv7OKAwlBjJ0JGs +5RsJ9DEehyLecnDDDhte92M2xUcfMet1BmuizLDDKaUU17sI1g/UNE+c7hViZA2J +e+wezVOUZqCY0pICsm4ar8JBY/pfUZ+1J00AZJtXuVWqK5GYGkrLZ7ZjNzzDF0cY +UmJxki5rj11XpCCQOZjVB+Pp3t7YpUOey1EC+1fKKrdS40zaRS3VVgh+Guavs5HV +egBzKDQUuRrZDbodJSv28RYlVbFTmkl3hGGNE0l2v0L2XHasZHoBkDZzz9nLuiI8 +ZdhWS+fn7dbswN9WzzB+dPzKS1WkTj5RXL/luI/7+fYNQyvIJYdnNCegyi2C2yTD +a/vmFJkBU+uLHWsW9a8R5Ca7A91ltJobTJE3uwxdXuZMTrmlWKsEbhqHCqO7d0j8 +IgYGxDo9ysfA4AOiNDxlp7lXxV/JFOsuGXNdFKcDFykLZ5u21X9ho9fptWJDP9JN +NNOXjC0Jv2UGZrHze6IqyL5JqxOGpK22PQIwpZwExwijUom+LH5VEXK1zpXzwC93 +WXWVtGOW4yEqv0VTn7vafIeM5GBTJ44ggpkp4RpFWoBMZcAFj8gE/9AUaHo= +-----END CERTIFICATE----- diff --git a/server/datastore/mysql/app_configs_test.go b/server/datastore/mysql/app_configs_test.go index e8ada564ed58..231a13c83c6a 100644 --- a/server/datastore/mysql/app_configs_test.go +++ b/server/datastore/mysql/app_configs_test.go @@ -586,7 +586,7 @@ func testNDESSCEPProxyPassword(t *testing.T, ds *Datastore) { checkProxyConfig() checkPassword() - // Set password to empty -- should not update + // Set password to empty -- password should not update url = "https://newurl.com" ac.Integrations.NDESSCEPProxy.Password = "" ac.Integrations.NDESSCEPProxy.URL = url @@ -595,4 +595,12 @@ func testNDESSCEPProxyPassword(t *testing.T, ds *Datastore) { checkProxyConfig() checkPassword() + // Set password to a new value + password = "newpassword" + ac.Integrations.NDESSCEPProxy.Password = password + err = ds.SaveAppConfig(ctx, ac) + require.NoError(t, err) + checkProxyConfig() + checkPassword() + } diff --git a/server/fleet/app.go b/server/fleet/app.go index 603ba2e0ab88..ce0c0eed75b9 100644 --- a/server/fleet/app.go +++ b/server/fleet/app.go @@ -520,6 +520,9 @@ func (c *AppConfig) Obfuscate() { for _, zdIntegration := range c.Integrations.Zendesk { zdIntegration.APIToken = MaskedPassword } + if c.Integrations.NDESSCEPProxy != nil { + c.Integrations.NDESSCEPProxy.Password = MaskedPassword + } } // Clone implements cloner. diff --git a/server/service/appconfig.go b/server/service/appconfig.go index ca4da04505dd..50462a25c0af 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -29,6 +29,10 @@ import ( "golang.org/x/text/unicode/norm" ) +// Functions that can be overwritten in tests +var validateNDESSCEPAdminURL = eeservice.ValidateNDESSCEPAdminURL +var validateNDESSCEPURL = eeservice.ValidateNDESSCEPURL + //////////////////////////////////////////////////////////////////////////////// // Get AppConfig //////////////////////////////////////////////////////////////////////////////// @@ -324,6 +328,37 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle } } + // Validate NDES SCEP URLs if they changed. Validation is done in both dry run and normal mode. + if license.IsPremium() && newAppConfig.Integrations.NDESSCEPProxy != nil { + validateAdminURL, validateSCEPURL := false, false + if oldAppConfig.Integrations.NDESSCEPProxy == nil { + validateAdminURL, validateSCEPURL = true, true + } else { + newSCEPProxy := newAppConfig.Integrations.NDESSCEPProxy + oldSCEPProxy := oldAppConfig.Integrations.NDESSCEPProxy + if newSCEPProxy.URL != oldSCEPProxy.URL { + validateSCEPURL = true + } + if newSCEPProxy.AdminURL != oldSCEPProxy.AdminURL || + newSCEPProxy.Username != oldSCEPProxy.Username || + (newSCEPProxy.Password != "" && newSCEPProxy.Password != fleet.MaskedPassword) { + validateAdminURL = true + } + } + + if validateAdminURL { + if err = validateNDESSCEPAdminURL(ctx, newAppConfig.Integrations.NDESSCEPProxy); err != nil { + invalid.Append("integrations.ndes_scep_proxy", err.Error()) + } + } + + if validateSCEPURL { + if err = validateNDESSCEPURL(ctx, newAppConfig.Integrations.NDESSCEPProxy, svc.logger); err != nil { + invalid.Append("integrations.ndes_scep_proxy.url", err.Error()) + } + } + } + // We apply the config that is incoming to the old one appConfig.EnableStrictDecoding() if err := json.Unmarshal(p, &appConfig); err != nil { @@ -331,6 +366,10 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle return nil, ctxerr.Wrap(ctx, err) } + if !license.IsPremium() && appConfig.Integrations.NDESSCEPProxy != nil { + appConfig.Integrations.NDESSCEPProxy = nil + } + // EnableDiskEncryption is an optjson.Bool field in order to support the // legacy field under "mdm.macos_settings". If the field provided to the // PATCH endpoint is set but invalid (that is, "enable_disk_encryption": @@ -425,40 +464,6 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle return nil, ctxerr.Wrap(ctx, err, "validating VPP token assignments") } - // Validate NDES SCEP URLs if they changed. Validation is done in both dry run and normal mode. - if license.IsPremium() && newAppConfig.Integrations.NDESSCEPProxy != nil { - validateAdminURL, validateSCEPURL := false, false - if oldAppConfig.Integrations.NDESSCEPProxy == nil { - validateAdminURL, validateSCEPURL = true, true - } else { - newSCEPProxy := newAppConfig.Integrations.NDESSCEPProxy - oldSCEPProxy := oldAppConfig.Integrations.NDESSCEPProxy - if newSCEPProxy.URL != oldSCEPProxy.URL { - validateSCEPURL = true - } - if newSCEPProxy.AdminURL != oldSCEPProxy.AdminURL || newSCEPProxy.Username != oldSCEPProxy.Username { - validateAdminURL = true - } else if newSCEPProxy.Password != "" && newSCEPProxy.Password != fleet.MaskedPassword { - // We do not update password if it is empty or masked - validateAdminURL = true - } - } - - if validateAdminURL { - if err = eeservice.ValidateNDESSCEPAdminURL(ctx, newAppConfig.Integrations.NDESSCEPProxy); err != nil { - invalid.Append("integrations.ndes_scep_proxy", err.Error()) - } - } - - if validateSCEPURL { - if err = eeservice.ValidateNDESSCEPURL(ctx, newAppConfig.Integrations.NDESSCEPProxy, svc.logger); err != nil { - invalid.Append("integrations.ndes_scep_proxy.url", err.Error()) - } - } - } else { - appConfig.Integrations.NDESSCEPProxy = nil - } - if invalid.HasErrors() { return nil, ctxerr.Wrap(ctx, invalid) } diff --git a/server/service/appconfig_test.go b/server/service/appconfig_test.go index 2c6e6bc10849..65e4d6c094e3 100644 --- a/server/service/appconfig_test.go +++ b/server/service/appconfig_test.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "database/sql" "encoding/json" + "errors" "fmt" "net/http" "net/http/httptest" @@ -24,6 +25,7 @@ import ( nanodep_mock "github.com/fleetdm/fleet/v4/server/mock/nanodep" "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/test" + "github.com/go-kit/log" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -1394,3 +1396,136 @@ func TestModifyEnableAnalytics(t *testing.T) { }) } } + +func TestModifyAppConfigForNDESSCEPProxy(t *testing.T) { + t.Parallel() + ds := new(mock.Store) + svc, ctx := newTestService(t, ds, nil, nil) + scepURL := "https://example.com/mscep/mscep.dll" + adminURL := "https://example.com/mscep_admin/" + username := "user" + password := "password" + + appConfig := &fleet.AppConfig{ + OrgInfo: fleet.OrgInfo{ + OrgName: "Test", + }, + ServerSettings: fleet.ServerSettings{ + ServerURL: "https://localhost:8080", + }, + } + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + if appConfig.Integrations.NDESSCEPProxy != nil { + appConfig.Integrations.NDESSCEPProxy.Password = fleet.MaskedPassword + } + return appConfig, nil + } + ds.SaveAppConfigFunc = func(ctx context.Context, conf *fleet.AppConfig) error { + appConfig = conf + return nil + } + + jsonPayloadBase := ` +{ + "integrations": { + "ndes_scep_proxy": { + "url": "%s", + "admin_url": "%s", + "username": "%s", + "password": "%s" + } + } +} +` + jsonPayload := fmt.Sprintf(jsonPayloadBase, scepURL, adminURL, username, password) + admin := &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)} + ctx = viewer.NewContext(ctx, viewer.Viewer{User: admin}) + + // SCEP proxy not configured for free users + ac, err := svc.ModifyAppConfig(ctx, []byte(jsonPayload), fleet.ApplySpecOptions{}) + require.NoError(t, err) + assert.Nil(t, ac.Integrations.NDESSCEPProxy) + + origValidateNDESSCEPURL := validateNDESSCEPURL + origValidateNDESSCEPAdminURL := validateNDESSCEPAdminURL + t.Cleanup(func() { + validateNDESSCEPURL = origValidateNDESSCEPURL + validateNDESSCEPAdminURL = origValidateNDESSCEPAdminURL + }) + validateNDESSCEPURLCalled := false + validateNDESSCEPURL = func(_ context.Context, _ *fleet.NDESSCEPProxyIntegration, _ log.Logger) error { + validateNDESSCEPURLCalled = true + return nil + } + validateNDESSCEPAdminURLCalled := false + validateNDESSCEPAdminURL = func(_ context.Context, _ *fleet.NDESSCEPProxyIntegration) error { + validateNDESSCEPAdminURLCalled = true + return nil + } + + svc, ctx = newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}}) + ctx = viewer.NewContext(ctx, viewer.Viewer{User: admin}) + ac, err = svc.ModifyAppConfig(ctx, []byte(jsonPayload), fleet.ApplySpecOptions{}) + require.NoError(t, err) + checkSCEPProxy := func() { + require.NotNil(t, ac.Integrations.NDESSCEPProxy) + assert.Equal(t, scepURL, ac.Integrations.NDESSCEPProxy.URL) + assert.Equal(t, adminURL, ac.Integrations.NDESSCEPProxy.AdminURL) + assert.Equal(t, username, ac.Integrations.NDESSCEPProxy.Username) + assert.Equal(t, fleet.MaskedPassword, ac.Integrations.NDESSCEPProxy.Password) + } + checkSCEPProxy() + assert.True(t, validateNDESSCEPURLCalled) + assert.True(t, validateNDESSCEPAdminURLCalled) + + // Validation not done if there is no change + appConfig = ac + validateNDESSCEPURLCalled = false + validateNDESSCEPAdminURLCalled = false + jsonPayload = fmt.Sprintf(jsonPayloadBase, scepURL, adminURL, username, fleet.MaskedPassword) + ac, err = svc.ModifyAppConfig(ctx, []byte(jsonPayload), fleet.ApplySpecOptions{}) + require.NoError(t, err) + checkSCEPProxy() + assert.False(t, validateNDESSCEPURLCalled) + assert.False(t, validateNDESSCEPAdminURLCalled) + + // Validation done for SCEP URL. Password is blank, which is not considered a change. + scepURL = "https://new.com/mscep/mscep.dll" + jsonPayload = fmt.Sprintf(jsonPayloadBase, scepURL, adminURL, username, "") + ac, err = svc.ModifyAppConfig(ctx, []byte(jsonPayload), fleet.ApplySpecOptions{}) + require.NoError(t, err) + checkSCEPProxy() + assert.True(t, validateNDESSCEPURLCalled) + assert.False(t, validateNDESSCEPAdminURLCalled) + appConfig = ac + validateNDESSCEPURLCalled = false + validateNDESSCEPAdminURLCalled = false + + // Validation done for SCEP admin URL + adminURL = "https://new.com/mscep_admin/" + jsonPayload = fmt.Sprintf(jsonPayloadBase, scepURL, adminURL, username, fleet.MaskedPassword) + ac, err = svc.ModifyAppConfig(ctx, []byte(jsonPayload), fleet.ApplySpecOptions{}) + require.NoError(t, err) + checkSCEPProxy() + assert.False(t, validateNDESSCEPURLCalled) + assert.True(t, validateNDESSCEPAdminURLCalled) + + // Validation fails + validateNDESSCEPURLCalled = false + validateNDESSCEPAdminURLCalled = false + validateNDESSCEPURL = func(_ context.Context, _ *fleet.NDESSCEPProxyIntegration, _ log.Logger) error { + validateNDESSCEPURLCalled = true + return errors.New("**invalid** 1") + } + validateNDESSCEPAdminURL = func(_ context.Context, _ *fleet.NDESSCEPProxyIntegration) error { + validateNDESSCEPAdminURLCalled = true + return errors.New("**invalid** 2") + } + scepURL = "https://new2.com/mscep/mscep.dll" + jsonPayload = fmt.Sprintf(jsonPayloadBase, scepURL, adminURL, username, password) + ac, err = svc.ModifyAppConfig(ctx, []byte(jsonPayload), fleet.ApplySpecOptions{}) + assert.ErrorContains(t, err, "**invalid**") + assert.True(t, validateNDESSCEPURLCalled) + assert.True(t, validateNDESSCEPAdminURLCalled) + +} From 191cb39c832e4e9f25f5d2747395933d79059bf0 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Wed, 2 Oct 2024 10:37:52 -0500 Subject: [PATCH 05/27] Minor copy change. --- ee/server/service/scep_proxy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ee/server/service/scep_proxy.go b/ee/server/service/scep_proxy.go index d7575ec4e3dd..9a630a039735 100644 --- a/ee/server/service/scep_proxy.go +++ b/ee/server/service/scep_proxy.go @@ -93,7 +93,7 @@ func ValidateNDESSCEPAdminURL(ctx context.Context, proxy *fleet.NDESSCEPProxyInt if challenge == "" { if strings.Contains(htmlString, fullPasswordCache) { return ctxerr.New(ctx, - "the password cache is full; please increase the number of cached passwords by NDES; by default, NDES caches 5 password and they expire 60 minutes after they are created") + "the password cache is full; please increase the number of cached passwords in NDES; by default, NDES caches 5 passwords and they expire 60 minutes after they are created") } return ctxerr.New(ctx, "could not retrieve the enrollment challenge password") } From 4b008ea860e4089e5782dd934531b57745bb28ed Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Wed, 2 Oct 2024 16:09:28 -0500 Subject: [PATCH 06/27] Switched to optjson pattern. --- ee/server/service/scep_proxy.go | 4 +- ee/server/service/scep_proxy_test.go | 4 +- pkg/optjson/optjson.go | 37 +++++++++++++ pkg/optjson/optjson_test.go | 53 ++++++++++++++++++ server/datastore/mysql/app_configs.go | 10 ++-- server/datastore/mysql/app_configs_test.go | 30 ++++++----- server/fleet/app.go | 4 +- server/fleet/integrations.go | 4 +- server/service/appconfig.go | 17 +++--- server/service/appconfig_test.go | 63 ++++++++++++++++++---- 10 files changed, 183 insertions(+), 43 deletions(-) diff --git a/ee/server/service/scep_proxy.go b/ee/server/service/scep_proxy.go index 9a630a039735..b1f8e86203ec 100644 --- a/ee/server/service/scep_proxy.go +++ b/ee/server/service/scep_proxy.go @@ -53,7 +53,7 @@ func NewSCEPProxyService(logger log.Logger) scepserver.Service { } } -func ValidateNDESSCEPAdminURL(ctx context.Context, proxy *fleet.NDESSCEPProxyIntegration) error { +func ValidateNDESSCEPAdminURL(ctx context.Context, proxy fleet.NDESSCEPProxyIntegration) error { adminURL, username, password := proxy.AdminURL, proxy.Username, proxy.Password // Get the challenge from NDES client := fleethttp.NewClient() @@ -100,7 +100,7 @@ func ValidateNDESSCEPAdminURL(ctx context.Context, proxy *fleet.NDESSCEPProxyInt return nil } -func ValidateNDESSCEPURL(ctx context.Context, proxy *fleet.NDESSCEPProxyIntegration, logger log.Logger) error { +func ValidateNDESSCEPURL(ctx context.Context, proxy fleet.NDESSCEPProxyIntegration, logger log.Logger) error { client, err := scepclient.New(proxy.URL, logger) if err != nil { return ctxerr.Wrap(ctx, err, "creating SCEP client") diff --git a/ee/server/service/scep_proxy_test.go b/ee/server/service/scep_proxy_test.go index 97527508e825..9fe9f2a6c82f 100644 --- a/ee/server/service/scep_proxy_test.go +++ b/ee/server/service/scep_proxy_test.go @@ -35,7 +35,7 @@ func TestValidateNDESSCEPAdminURL(t *testing.T) { })) t.Cleanup(ndesAdminServer.Close) - proxy := &fleet.NDESSCEPProxyIntegration{ + proxy := fleet.NDESSCEPProxyIntegration{ AdminURL: ndesAdminServer.URL, Username: "admin", Password: "password", @@ -85,7 +85,7 @@ func TestValidateNDESSCEPURL(t *testing.T) { t.Parallel() srv := newSCEPServer(t) - proxy := &fleet.NDESSCEPProxyIntegration{ + proxy := fleet.NDESSCEPProxyIntegration{ URL: srv.URL + "/scep", } err := ValidateNDESSCEPURL(context.Background(), proxy, kitlog.NewNopLogger()) diff --git a/pkg/optjson/optjson.go b/pkg/optjson/optjson.go index dbc44dd5e7c0..f306db305ad4 100644 --- a/pkg/optjson/optjson.go +++ b/pkg/optjson/optjson.go @@ -169,3 +169,40 @@ func (s *Slice[T]) UnmarshalJSON(data []byte) error { s.Valid = true return nil } + +type Any[T any] struct { + Set bool + Valid bool + Value T + ZeroValue func() T +} + +func (s Any[T]) MarshalJSON() ([]byte, error) { + if !s.Valid { + return []byte("null"), nil + } + return json.Marshal(s.Value) +} + +func (s *Any[T]) UnmarshalJSON(data []byte) error { + // If this method was called, the value was set. + s.Set = true + s.Valid = false + + if bytes.Equal(data, []byte("null")) { + // The key was set to null, blank the value if possible + if s.ZeroValue != nil { + s.Value = s.ZeroValue() + } + return nil + } + + // The key isn't set to null + var v T + if err := json.Unmarshal(data, &v); err != nil { + return err + } + s.Value = v + s.Valid = true + return nil +} diff --git a/pkg/optjson/optjson_test.go b/pkg/optjson/optjson_test.go index 2a1d01e1433a..963ec1f7980a 100644 --- a/pkg/optjson/optjson_test.go +++ b/pkg/optjson/optjson_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -440,3 +441,55 @@ func TestSliceWithinStruct(t *testing.T) { } }) } + +func TestAny(t *testing.T) { + t.Parallel() + type Item struct { + ID int `json:"id"` + Name string `json:"name"` + } + SetItem := func(item Item) Any[Item] { + return Any[Item]{Set: true, Valid: true, Value: item} + } + + zeroValue := func() Item { return Item{ID: -1} } + + cases := []struct { + data string + wantErr string + wantRes Any[Item] + marshalAs string + zeroValue func() Item + }{ + {data: `{ "id": 1, "name": "bozo" }`, wantErr: "", wantRes: SetItem(Item{ID: 1, Name: "bozo"}), + marshalAs: `{"id":1,"name":"bozo"}`}, + {data: `null`, wantErr: "", wantRes: Any[Item]{Set: true, Valid: false}, marshalAs: `null`}, + {data: `null`, wantErr: "", wantRes: Any[Item]{Set: true, Valid: false, Value: Item{ID: -1}}, + marshalAs: `null`, zeroValue: zeroValue}, + {data: `[]`, wantErr: "cannot unmarshal array", wantRes: Any[Item]{Set: true, Valid: false, Value: Item{}}, marshalAs: `null`}, + } + + for _, c := range cases { + t.Run(c.data, func(t *testing.T) { + var s Any[Item] + s.ZeroValue = c.zeroValue + err := json.Unmarshal([]byte(c.data), &s) + + if c.wantErr != "" { + require.Error(t, err) + require.ErrorContains(t, err, c.wantErr) + } else { + require.NoError(t, err) + } + assert.Equal(t, c.wantRes.Set, s.Set) + assert.Equal(t, c.wantRes.Valid, s.Valid) + assert.Equal(t, c.wantRes.Value.ID, s.Value.ID) + assert.Equal(t, c.wantRes.Value.Name, s.Value.Name) + // Don't compare ZeroValue, it's a function + + b, err := json.Marshal(s) + require.NoError(t, err) + require.Equal(t, c.marshalAs, string(b)) + }) + } +} diff --git a/server/datastore/mysql/app_configs.go b/server/datastore/mysql/app_configs.go index 74d093c40577..7221ed0467ec 100644 --- a/server/datastore/mysql/app_configs.go +++ b/server/datastore/mysql/app_configs.go @@ -51,17 +51,19 @@ func appConfigDB(ctx context.Context, q sqlx.QueryerContext) (*fleet.AppConfig, func (ds *Datastore) SaveAppConfig(ctx context.Context, info *fleet.AppConfig) error { // Check if passwords need to be encrypted - if info.Integrations.NDESSCEPProxy != nil { - if info.Integrations.NDESSCEPProxy.Password != "" && info.Integrations.NDESSCEPProxy.Password != fleet.MaskedPassword { + if info.Integrations.NDESSCEPProxy.Valid { + if info.Integrations.NDESSCEPProxy.Set && + info.Integrations.NDESSCEPProxy.Value.Password != "" && + info.Integrations.NDESSCEPProxy.Value.Password != fleet.MaskedPassword { err := ds.insertOrReplaceConfigAsset(ctx, fleet.MDMConfigAsset{ Name: fleet.MDMAssetNDESPassword, - Value: []byte(info.Integrations.NDESSCEPProxy.Password), + Value: []byte(info.Integrations.NDESSCEPProxy.Value.Password), }) if err != nil { return ctxerr.Wrap(ctx, err, "processing NDES SCEP proxy password") } } - info.Integrations.NDESSCEPProxy.Password = fleet.MaskedPassword + info.Integrations.NDESSCEPProxy.Value.Password = fleet.MaskedPassword } configBytes, err := json.Marshal(info) diff --git a/server/datastore/mysql/app_configs_test.go b/server/datastore/mysql/app_configs_test.go index 231a13c83c6a..d7daddd5ad40 100644 --- a/server/datastore/mysql/app_configs_test.go +++ b/server/datastore/mysql/app_configs_test.go @@ -549,11 +549,15 @@ func testNDESSCEPProxyPassword(t *testing.T, ds *Datastore) { url := "https://localhost:8080/mscep/mscep.dll" password := "password" - ac.Integrations.NDESSCEPProxy = &fleet.NDESSCEPProxyIntegration{ - AdminURL: adminURL, - Username: username, - Password: password, - URL: url, + ac.Integrations.NDESSCEPProxy = optjson.Any[fleet.NDESSCEPProxyIntegration]{ + Valid: true, + Set: true, + Value: fleet.NDESSCEPProxyIntegration{ + AdminURL: adminURL, + Username: username, + Password: password, + URL: url, + }, } err = ds.SaveAppConfig(ctx, ac) @@ -563,10 +567,10 @@ func testNDESSCEPProxyPassword(t *testing.T, ds *Datastore) { result, err := ds.AppConfig(ctx) require.NoError(t, err) require.NotNil(t, result.Integrations.NDESSCEPProxy) - assert.Equal(t, url, result.Integrations.NDESSCEPProxy.URL) - assert.Equal(t, adminURL, result.Integrations.NDESSCEPProxy.AdminURL) - assert.Equal(t, username, result.Integrations.NDESSCEPProxy.Username) - assert.Equal(t, fleet.MaskedPassword, result.Integrations.NDESSCEPProxy.Password) + assert.Equal(t, url, result.Integrations.NDESSCEPProxy.Value.URL) + assert.Equal(t, adminURL, result.Integrations.NDESSCEPProxy.Value.AdminURL) + assert.Equal(t, username, result.Integrations.NDESSCEPProxy.Value.Username) + assert.Equal(t, fleet.MaskedPassword, result.Integrations.NDESSCEPProxy.Value.Password) } checkProxyConfig() @@ -580,7 +584,7 @@ func testNDESSCEPProxyPassword(t *testing.T, ds *Datastore) { checkPassword() // Set password to masked password -- should not update - ac.Integrations.NDESSCEPProxy.Password = fleet.MaskedPassword + ac.Integrations.NDESSCEPProxy.Value.Password = fleet.MaskedPassword err = ds.SaveAppConfig(ctx, ac) require.NoError(t, err) checkProxyConfig() @@ -588,8 +592,8 @@ func testNDESSCEPProxyPassword(t *testing.T, ds *Datastore) { // Set password to empty -- password should not update url = "https://newurl.com" - ac.Integrations.NDESSCEPProxy.Password = "" - ac.Integrations.NDESSCEPProxy.URL = url + ac.Integrations.NDESSCEPProxy.Value.Password = "" + ac.Integrations.NDESSCEPProxy.Value.URL = url err = ds.SaveAppConfig(ctx, ac) require.NoError(t, err) checkProxyConfig() @@ -597,7 +601,7 @@ func testNDESSCEPProxyPassword(t *testing.T, ds *Datastore) { // Set password to a new value password = "newpassword" - ac.Integrations.NDESSCEPProxy.Password = password + ac.Integrations.NDESSCEPProxy.Value.Password = password err = ds.SaveAppConfig(ctx, ac) require.NoError(t, err) checkProxyConfig() diff --git a/server/fleet/app.go b/server/fleet/app.go index ce0c0eed75b9..c56c3bebfa0a 100644 --- a/server/fleet/app.go +++ b/server/fleet/app.go @@ -520,8 +520,8 @@ func (c *AppConfig) Obfuscate() { for _, zdIntegration := range c.Integrations.Zendesk { zdIntegration.APIToken = MaskedPassword } - if c.Integrations.NDESSCEPProxy != nil { - c.Integrations.NDESSCEPProxy.Password = MaskedPassword + if c.Integrations.NDESSCEPProxy.Valid { + c.Integrations.NDESSCEPProxy.Value.Password = MaskedPassword } } diff --git a/server/fleet/integrations.go b/server/fleet/integrations.go index 4ecc8db7ea91..887db9a54ee2 100644 --- a/server/fleet/integrations.go +++ b/server/fleet/integrations.go @@ -8,6 +8,7 @@ import ( "strconv" "strings" + "github.com/fleetdm/fleet/v4/pkg/optjson" "github.com/fleetdm/fleet/v4/server/service/externalsvc" ) @@ -365,7 +366,8 @@ type Integrations struct { Jira []*JiraIntegration `json:"jira"` Zendesk []*ZendeskIntegration `json:"zendesk"` GoogleCalendar []*GoogleCalendarIntegration `json:"google_calendar"` - NDESSCEPProxy *NDESSCEPProxyIntegration `json:"ndes_scep_proxy"` + // NDESSCEPProxy settings. In JSON, not specifying this field means keep current setting, null means clear settings. + NDESSCEPProxy optjson.Any[NDESSCEPProxyIntegration] `json:"ndes_scep_proxy"` } func ValidateEnabledActivitiesWebhook(webhook ActivitiesWebhookSettings, invalid *InvalidArgumentError) { diff --git a/server/service/appconfig.go b/server/service/appconfig.go index 50462a25c0af..ebc9cef9d9de 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -329,13 +329,14 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle } // Validate NDES SCEP URLs if they changed. Validation is done in both dry run and normal mode. - if license.IsPremium() && newAppConfig.Integrations.NDESSCEPProxy != nil { + if license.IsPremium() && newAppConfig.Integrations.NDESSCEPProxy.Set && newAppConfig.Integrations.NDESSCEPProxy.Valid { + validateAdminURL, validateSCEPURL := false, false - if oldAppConfig.Integrations.NDESSCEPProxy == nil { + newSCEPProxy := newAppConfig.Integrations.NDESSCEPProxy.Value + if !oldAppConfig.Integrations.NDESSCEPProxy.Valid { validateAdminURL, validateSCEPURL = true, true } else { - newSCEPProxy := newAppConfig.Integrations.NDESSCEPProxy - oldSCEPProxy := oldAppConfig.Integrations.NDESSCEPProxy + oldSCEPProxy := oldAppConfig.Integrations.NDESSCEPProxy.Value if newSCEPProxy.URL != oldSCEPProxy.URL { validateSCEPURL = true } @@ -347,13 +348,13 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle } if validateAdminURL { - if err = validateNDESSCEPAdminURL(ctx, newAppConfig.Integrations.NDESSCEPProxy); err != nil { + if err = validateNDESSCEPAdminURL(ctx, newSCEPProxy); err != nil { invalid.Append("integrations.ndes_scep_proxy", err.Error()) } } if validateSCEPURL { - if err = validateNDESSCEPURL(ctx, newAppConfig.Integrations.NDESSCEPProxy, svc.logger); err != nil { + if err = validateNDESSCEPURL(ctx, newSCEPProxy, svc.logger); err != nil { invalid.Append("integrations.ndes_scep_proxy.url", err.Error()) } } @@ -366,8 +367,8 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle return nil, ctxerr.Wrap(ctx, err) } - if !license.IsPremium() && appConfig.Integrations.NDESSCEPProxy != nil { - appConfig.Integrations.NDESSCEPProxy = nil + if !license.IsPremium() { + appConfig.Integrations.NDESSCEPProxy.Valid = false } // EnableDiskEncryption is an optjson.Bool field in order to support the diff --git a/server/service/appconfig_test.go b/server/service/appconfig_test.go index 65e4d6c094e3..e5abbcd6ce0a 100644 --- a/server/service/appconfig_test.go +++ b/server/service/appconfig_test.go @@ -1415,8 +1415,8 @@ func TestModifyAppConfigForNDESSCEPProxy(t *testing.T) { }, } ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { - if appConfig.Integrations.NDESSCEPProxy != nil { - appConfig.Integrations.NDESSCEPProxy.Password = fleet.MaskedPassword + if appConfig.Integrations.NDESSCEPProxy.Valid { + appConfig.Integrations.NDESSCEPProxy.Value.Password = fleet.MaskedPassword } return appConfig, nil } @@ -1424,6 +1424,9 @@ func TestModifyAppConfigForNDESSCEPProxy(t *testing.T) { appConfig = conf return nil } + ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) { + return []*fleet.ABMToken{{ID: 1}}, nil + } jsonPayloadBase := ` { @@ -1444,7 +1447,7 @@ func TestModifyAppConfigForNDESSCEPProxy(t *testing.T) { // SCEP proxy not configured for free users ac, err := svc.ModifyAppConfig(ctx, []byte(jsonPayload), fleet.ApplySpecOptions{}) require.NoError(t, err) - assert.Nil(t, ac.Integrations.NDESSCEPProxy) + assert.False(t, ac.Integrations.NDESSCEPProxy.Valid) origValidateNDESSCEPURL := validateNDESSCEPURL origValidateNDESSCEPAdminURL := validateNDESSCEPAdminURL @@ -1453,12 +1456,12 @@ func TestModifyAppConfigForNDESSCEPProxy(t *testing.T) { validateNDESSCEPAdminURL = origValidateNDESSCEPAdminURL }) validateNDESSCEPURLCalled := false - validateNDESSCEPURL = func(_ context.Context, _ *fleet.NDESSCEPProxyIntegration, _ log.Logger) error { + validateNDESSCEPURL = func(_ context.Context, _ fleet.NDESSCEPProxyIntegration, _ log.Logger) error { validateNDESSCEPURLCalled = true return nil } validateNDESSCEPAdminURLCalled := false - validateNDESSCEPAdminURL = func(_ context.Context, _ *fleet.NDESSCEPProxyIntegration) error { + validateNDESSCEPAdminURL = func(_ context.Context, _ fleet.NDESSCEPProxyIntegration) error { validateNDESSCEPAdminURLCalled = true return nil } @@ -1469,10 +1472,10 @@ func TestModifyAppConfigForNDESSCEPProxy(t *testing.T) { require.NoError(t, err) checkSCEPProxy := func() { require.NotNil(t, ac.Integrations.NDESSCEPProxy) - assert.Equal(t, scepURL, ac.Integrations.NDESSCEPProxy.URL) - assert.Equal(t, adminURL, ac.Integrations.NDESSCEPProxy.AdminURL) - assert.Equal(t, username, ac.Integrations.NDESSCEPProxy.Username) - assert.Equal(t, fleet.MaskedPassword, ac.Integrations.NDESSCEPProxy.Password) + assert.Equal(t, scepURL, ac.Integrations.NDESSCEPProxy.Value.URL) + assert.Equal(t, adminURL, ac.Integrations.NDESSCEPProxy.Value.AdminURL) + assert.Equal(t, username, ac.Integrations.NDESSCEPProxy.Value.Username) + assert.Equal(t, fleet.MaskedPassword, ac.Integrations.NDESSCEPProxy.Value.Password) } checkSCEPProxy() assert.True(t, validateNDESSCEPURLCalled) @@ -1489,6 +1492,15 @@ func TestModifyAppConfigForNDESSCEPProxy(t *testing.T) { assert.False(t, validateNDESSCEPURLCalled) assert.False(t, validateNDESSCEPAdminURLCalled) + // Validation not done if there is no change, part 2 + validateNDESSCEPURLCalled = false + validateNDESSCEPAdminURLCalled = false + ac, err = svc.ModifyAppConfig(ctx, []byte(`{"integrations":{}}`), fleet.ApplySpecOptions{}) + require.NoError(t, err) + checkSCEPProxy() + assert.False(t, validateNDESSCEPURLCalled) + assert.False(t, validateNDESSCEPAdminURLCalled) + // Validation done for SCEP URL. Password is blank, which is not considered a change. scepURL = "https://new.com/mscep/mscep.dll" jsonPayload = fmt.Sprintf(jsonPayloadBase, scepURL, adminURL, username, "") @@ -1513,11 +1525,11 @@ func TestModifyAppConfigForNDESSCEPProxy(t *testing.T) { // Validation fails validateNDESSCEPURLCalled = false validateNDESSCEPAdminURLCalled = false - validateNDESSCEPURL = func(_ context.Context, _ *fleet.NDESSCEPProxyIntegration, _ log.Logger) error { + validateNDESSCEPURL = func(_ context.Context, _ fleet.NDESSCEPProxyIntegration, _ log.Logger) error { validateNDESSCEPURLCalled = true return errors.New("**invalid** 1") } - validateNDESSCEPAdminURL = func(_ context.Context, _ *fleet.NDESSCEPProxyIntegration) error { + validateNDESSCEPAdminURL = func(_ context.Context, _ fleet.NDESSCEPProxyIntegration) error { validateNDESSCEPAdminURLCalled = true return errors.New("**invalid** 2") } @@ -1528,4 +1540,33 @@ func TestModifyAppConfigForNDESSCEPProxy(t *testing.T) { assert.True(t, validateNDESSCEPURLCalled) assert.True(t, validateNDESSCEPAdminURLCalled) + // Reset validation + validateNDESSCEPURLCalled = false + validateNDESSCEPURL = func(_ context.Context, _ fleet.NDESSCEPProxyIntegration, _ log.Logger) error { + validateNDESSCEPURLCalled = true + return nil + } + validateNDESSCEPAdminURLCalled = false + validateNDESSCEPAdminURL = func(_ context.Context, _ fleet.NDESSCEPProxyIntegration) error { + validateNDESSCEPAdminURLCalled = true + return nil + } + + // Config cleared with explicit null + validateNDESSCEPURLCalled = false + validateNDESSCEPAdminURLCalled = false + payload := ` +{ + "integrations": { + "ndes_scep_proxy": null + } +} +` + ac, err = svc.ModifyAppConfig(ctx, []byte(payload), fleet.ApplySpecOptions{}) + require.NoError(t, err) + assert.False(t, ac.Integrations.NDESSCEPProxy.Valid) + // Also check what was saved. + assert.False(t, appConfig.Integrations.NDESSCEPProxy.Valid) + assert.False(t, validateNDESSCEPURLCalled) + assert.False(t, validateNDESSCEPAdminURLCalled) } From 31845ce2df7553c160776798260e526bcfebd929 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Wed, 2 Oct 2024 17:16:21 -0500 Subject: [PATCH 07/27] Added input preprocessing, and cleanup. --- server/fleet/utils.go | 9 +++++ server/service/appconfig.go | 36 ++++++++++++------- server/service/appconfig_test.go | 7 ++-- .../generated_files/appconfig.txt | 6 +++- 4 files changed, 42 insertions(+), 16 deletions(-) diff --git a/server/fleet/utils.go b/server/fleet/utils.go index 7095585d6ebf..ddab26e8e797 100644 --- a/server/fleet/utils.go +++ b/server/fleet/utils.go @@ -4,8 +4,10 @@ import ( "encoding/json" "errors" "io" + "strings" "github.com/fatih/color" + "golang.org/x/text/unicode/norm" ) func WriteExpiredLicenseBanner(w io.Writer) { @@ -56,3 +58,10 @@ func JSONStrictDecode(r io.Reader, v interface{}) error { return nil } + +func Preprocess(input string) string { + // Remove leading/trailing whitespace. + input = strings.TrimSpace(input) + // Normalize Unicode characters. + return norm.NFC.String(input) +} diff --git a/server/service/appconfig.go b/server/service/appconfig.go index ebc9cef9d9de..98eaaa289095 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -328,11 +328,32 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle } } + // We apply the config that is incoming to the old one + appConfig.EnableStrictDecoding() + if err := json.Unmarshal(p, &appConfig); err != nil { + err = fleet.NewUserMessageError(err, http.StatusBadRequest) + return nil, ctxerr.Wrap(ctx, err) + } + // Validate NDES SCEP URLs if they changed. Validation is done in both dry run and normal mode. - if license.IsPremium() && newAppConfig.Integrations.NDESSCEPProxy.Set && newAppConfig.Integrations.NDESSCEPProxy.Valid { + switch { + case !license.IsPremium(): + appConfig.Integrations.NDESSCEPProxy.Valid = false + case !newAppConfig.Integrations.NDESSCEPProxy.Set: + // Nothing is set -- keep the old value + appConfig.Integrations.NDESSCEPProxy = oldAppConfig.Integrations.NDESSCEPProxy + case !newAppConfig.Integrations.NDESSCEPProxy.Valid: + // User is explicitly clearing this setting + appConfig.Integrations.NDESSCEPProxy.Valid = false + default: + // User is updating the setting + appConfig.Integrations.NDESSCEPProxy.Value.URL = fleet.Preprocess(newAppConfig.Integrations.NDESSCEPProxy.Value.URL) + appConfig.Integrations.NDESSCEPProxy.Value.AdminURL = fleet.Preprocess(newAppConfig.Integrations.NDESSCEPProxy.Value.AdminURL) + appConfig.Integrations.NDESSCEPProxy.Value.Username = fleet.Preprocess(newAppConfig.Integrations.NDESSCEPProxy.Value.Username) + // do not preprocess password validateAdminURL, validateSCEPURL := false, false - newSCEPProxy := newAppConfig.Integrations.NDESSCEPProxy.Value + newSCEPProxy := appConfig.Integrations.NDESSCEPProxy.Value if !oldAppConfig.Integrations.NDESSCEPProxy.Valid { validateAdminURL, validateSCEPURL = true, true } else { @@ -360,17 +381,6 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle } } - // We apply the config that is incoming to the old one - appConfig.EnableStrictDecoding() - if err := json.Unmarshal(p, &appConfig); err != nil { - err = fleet.NewUserMessageError(err, http.StatusBadRequest) - return nil, ctxerr.Wrap(ctx, err) - } - - if !license.IsPremium() { - appConfig.Integrations.NDESSCEPProxy.Valid = false - } - // EnableDiskEncryption is an optjson.Bool field in order to support the // legacy field under "mdm.macos_settings". If the field provided to the // PATCH endpoint is set but invalid (that is, "enable_disk_encryption": diff --git a/server/service/appconfig_test.go b/server/service/appconfig_test.go index e5abbcd6ce0a..d653c4c303a9 100644 --- a/server/service/appconfig_test.go +++ b/server/service/appconfig_test.go @@ -1427,6 +1427,9 @@ func TestModifyAppConfigForNDESSCEPProxy(t *testing.T) { ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) { return []*fleet.ABMToken{{ID: 1}}, nil } + ds.SaveABMTokenFunc = func(ctx context.Context, token *fleet.ABMToken) error { + return nil + } jsonPayloadBase := ` { @@ -1485,9 +1488,9 @@ func TestModifyAppConfigForNDESSCEPProxy(t *testing.T) { appConfig = ac validateNDESSCEPURLCalled = false validateNDESSCEPAdminURLCalled = false - jsonPayload = fmt.Sprintf(jsonPayloadBase, scepURL, adminURL, username, fleet.MaskedPassword) + jsonPayload = fmt.Sprintf(jsonPayloadBase, " "+scepURL, adminURL+" ", " "+username+" ", fleet.MaskedPassword) ac, err = svc.ModifyAppConfig(ctx, []byte(jsonPayload), fleet.ApplySpecOptions{}) - require.NoError(t, err) + require.NoError(t, err, jsonPayload) checkSCEPProxy() assert.False(t, validateNDESSCEPURLCalled) assert.False(t, validateNDESSCEPAdminURLCalled) diff --git a/tools/cloner-check/generated_files/appconfig.txt b/tools/cloner-check/generated_files/appconfig.txt index 47299f2daa42..9588b0cb5bcd 100644 --- a/tools/cloner-check/generated_files/appconfig.txt +++ b/tools/cloner-check/generated_files/appconfig.txt @@ -95,11 +95,15 @@ github.com/fleetdm/fleet/v4/server/fleet/ZendeskIntegration EnableSoftwareVulner github.com/fleetdm/fleet/v4/server/fleet/Integrations GoogleCalendar []*fleet.GoogleCalendarIntegration github.com/fleetdm/fleet/v4/server/fleet/GoogleCalendarIntegration Domain string github.com/fleetdm/fleet/v4/server/fleet/GoogleCalendarIntegration ApiKey map[string]string -github.com/fleetdm/fleet/v4/server/fleet/Integrations NDESSCEPProxy *fleet.NDESSCEPProxyIntegration +github.com/fleetdm/fleet/v4/server/fleet/Integrations NDESSCEPProxy optjson.Any[github.com/fleetdm/fleet/v4/server/fleet.NDESSCEPProxyIntegration] +github.com/fleetdm/fleet/v4/pkg/optjson/Any[github.com/fleetdm/fleet/v4/server/fleet.NDESSCEPProxyIntegration] Set bool +github.com/fleetdm/fleet/v4/pkg/optjson/Any[github.com/fleetdm/fleet/v4/server/fleet.NDESSCEPProxyIntegration] Valid bool +github.com/fleetdm/fleet/v4/pkg/optjson/Any[github.com/fleetdm/fleet/v4/server/fleet.NDESSCEPProxyIntegration] Value fleet.NDESSCEPProxyIntegration github.com/fleetdm/fleet/v4/server/fleet/NDESSCEPProxyIntegration URL string github.com/fleetdm/fleet/v4/server/fleet/NDESSCEPProxyIntegration AdminURL string github.com/fleetdm/fleet/v4/server/fleet/NDESSCEPProxyIntegration Username string github.com/fleetdm/fleet/v4/server/fleet/NDESSCEPProxyIntegration Password string +github.com/fleetdm/fleet/v4/pkg/optjson/Any[github.com/fleetdm/fleet/v4/server/fleet.NDESSCEPProxyIntegration] ZeroValue func() fleet.NDESSCEPProxyIntegration github.com/fleetdm/fleet/v4/server/fleet/AppConfig MDM fleet.MDM github.com/fleetdm/fleet/v4/server/fleet/MDM DeprecatedAppleBMDefaultTeam string github.com/fleetdm/fleet/v4/server/fleet/MDM AppleBusinessManager optjson.Slice[github.com/fleetdm/fleet/v4/server/fleet.MDMAppleABMAssignmentInfo] From 521771e9873a11e4519427102af3ae8a4e3e9478 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Wed, 2 Oct 2024 17:22:04 -0500 Subject: [PATCH 08/27] Cleaned up error messages to closer match Figma expectations. --- ee/server/service/scep_proxy.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ee/server/service/scep_proxy.go b/ee/server/service/scep_proxy.go index b1f8e86203ec..1359d5fdfa25 100644 --- a/ee/server/service/scep_proxy.go +++ b/ee/server/service/scep_proxy.go @@ -95,7 +95,8 @@ func ValidateNDESSCEPAdminURL(ctx context.Context, proxy fleet.NDESSCEPProxyInte return ctxerr.New(ctx, "the password cache is full; please increase the number of cached passwords in NDES; by default, NDES caches 5 passwords and they expire 60 minutes after they are created") } - return ctxerr.New(ctx, "could not retrieve the enrollment challenge password") + return ctxerr.New(ctx, + "could not retrieve the enrollment challenge password; invalid admin URL or credentials; please correct and try again") } return nil } @@ -103,12 +104,12 @@ func ValidateNDESSCEPAdminURL(ctx context.Context, proxy fleet.NDESSCEPProxyInte func ValidateNDESSCEPURL(ctx context.Context, proxy fleet.NDESSCEPProxyIntegration, logger log.Logger) error { client, err := scepclient.New(proxy.URL, logger) if err != nil { - return ctxerr.Wrap(ctx, err, "creating SCEP client") + return ctxerr.Wrap(ctx, err, "creating SCEP client; invalid SCEP URL; please correct and try again") } certs, _, err := client.GetCACert(ctx, "") if err != nil { - return ctxerr.Wrap(ctx, err, "could not retrieve CA certificate from SCEP URL") + return ctxerr.Wrap(ctx, err, "could not retrieve CA certificate from SCEP URL; invalid SCEP URL; please correct and try again") } if len(certs) == 0 { return ctxerr.New(ctx, "SCEP URL did not return a CA certificate") From e01d1cca8a2e72fb08835678e191539bad30d6e7 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Wed, 2 Oct 2024 18:01:27 -0500 Subject: [PATCH 09/27] Removing parallel --- server/service/appconfig_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/server/service/appconfig_test.go b/server/service/appconfig_test.go index d653c4c303a9..9967c85d3a7e 100644 --- a/server/service/appconfig_test.go +++ b/server/service/appconfig_test.go @@ -1398,7 +1398,6 @@ func TestModifyEnableAnalytics(t *testing.T) { } func TestModifyAppConfigForNDESSCEPProxy(t *testing.T) { - t.Parallel() ds := new(mock.Store) svc, ctx := newTestService(t, ds, nil, nil) scepURL := "https://example.com/mscep/mscep.dll" From ece1a87e3a3382fbd3d91a31d0730228aa3e1946 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Thu, 3 Oct 2024 08:16:37 -0500 Subject: [PATCH 10/27] Debugging unexpected fail, and line number issue. --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 9e63214ebd9a..0a4e4a2f124a 100644 --- a/Makefile +++ b/Makefile @@ -139,6 +139,7 @@ dump-test-schema: go run ./tools/dbutils ./server/datastore/mysql/schema.sql test-go: dump-test-schema generate-mock + cat --number ./server/service/appconfig_test.go go test -tags full,fts5,netgo ${GO_TEST_EXTRA_FLAGS_VAR} -parallel 8 -coverprofile=coverage.txt -covermode=atomic -coverpkg=github.com/fleetdm/fleet/v4/... ./cmd/... ./ee/... ./orbit/pkg/... ./orbit/cmd/orbit ./pkg/... ./server/... ./tools/... analyze-go: From 30041f970eb38344e112966cd3e42528fdd8d145 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Thu, 3 Oct 2024 09:47:52 -0500 Subject: [PATCH 11/27] Fixed test finally. --- server/service/appconfig_test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/service/appconfig_test.go b/server/service/appconfig_test.go index 9967c85d3a7e..d9556ae5517d 100644 --- a/server/service/appconfig_test.go +++ b/server/service/appconfig_test.go @@ -1399,7 +1399,7 @@ func TestModifyEnableAnalytics(t *testing.T) { func TestModifyAppConfigForNDESSCEPProxy(t *testing.T) { ds := new(mock.Store) - svc, ctx := newTestService(t, ds, nil, nil) + svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierFree}}) scepURL := "https://example.com/mscep/mscep.dll" adminURL := "https://example.com/mscep_admin/" username := "user" @@ -1429,6 +1429,9 @@ func TestModifyAppConfigForNDESSCEPProxy(t *testing.T) { ds.SaveABMTokenFunc = func(ctx context.Context, token *fleet.ABMToken) error { return nil } + ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) { + return []*fleet.VPPTokenDB{}, nil + } jsonPayloadBase := ` { From 1e87a76c147f5b19a5ac318da5dcf8aec298778f Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Fri, 4 Oct 2024 10:59:26 -0500 Subject: [PATCH 12/27] Initial implementation of Apple MDM profile variable insertion. --- ee/server/service/errors.go | 20 ++- ee/server/service/scep_proxy.go | 25 +-- pkg/optjson/optjson.go | 14 +- pkg/optjson/optjson_test.go | 12 +- server/datastore/mysql/hosts.go | 14 ++ server/fleet/datastore.go | 2 + server/mdm/apple/apple_mdm.go | 3 + server/mock/datastore_mock.go | 12 ++ server/service/apple_mdm.go | 273 ++++++++++++++++++++++++++++++-- server/service/handler.go | 5 +- 10 files changed, 339 insertions(+), 41 deletions(-) diff --git a/ee/server/service/errors.go b/ee/server/service/errors.go index 873069aa6982..45f00558b7de 100644 --- a/ee/server/service/errors.go +++ b/ee/server/service/errors.go @@ -1,6 +1,8 @@ package service -import "github.com/fleetdm/fleet/v4/server/fleet" +import ( + "github.com/fleetdm/fleet/v4/server/fleet" +) type notFoundError struct { fleet.ErrorWithUUID @@ -15,3 +17,19 @@ func (e notFoundError) Error() string { func (e notFoundError) IsNotFound() bool { return true } + +type NDESInvalidError struct { + msg string +} + +func (e NDESInvalidError) Error() string { + return e.msg +} + +type NDESPasswordCacheFullError struct { + msg string +} + +func (e NDESPasswordCacheFullError) Error() string { + return e.msg +} diff --git a/ee/server/service/scep_proxy.go b/ee/server/service/scep_proxy.go index 1359d5fdfa25..f54dcca75a5d 100644 --- a/ee/server/service/scep_proxy.go +++ b/ee/server/service/scep_proxy.go @@ -54,6 +54,11 @@ func NewSCEPProxyService(logger log.Logger) scepserver.Service { } func ValidateNDESSCEPAdminURL(ctx context.Context, proxy fleet.NDESSCEPProxyIntegration) error { + _, err := GetNDESSCEPChallenge(ctx, proxy) + return err +} + +func GetNDESSCEPChallenge(ctx context.Context, proxy fleet.NDESSCEPProxyIntegration) (string, error) { adminURL, username, password := proxy.AdminURL, proxy.Username, proxy.Password // Get the challenge from NDES client := fleethttp.NewClient() @@ -62,15 +67,17 @@ func ValidateNDESSCEPAdminURL(ctx context.Context, proxy fleet.NDESSCEPProxyInte } req, err := http.NewRequest(http.MethodGet, adminURL, http.NoBody) if err != nil { - return ctxerr.Wrap(ctx, err, "creating request") + return "", ctxerr.Wrap(ctx, err, "creating request") } req.SetBasicAuth(username, password) resp, err := client.Do(req) if err != nil { - return ctxerr.Wrap(ctx, err, "sending request") + return "", ctxerr.Wrap(ctx, err, "sending request") } if resp.StatusCode != http.StatusOK { - return ctxerr.New(ctx, fmt.Sprintf("unexpected status code: %d", resp.StatusCode)) + return "", ctxerr.Wrap(ctx, NDESInvalidError{msg: fmt.Sprintf( + "unexpected status code: %d; could not retrieve the enrollment challenge password; invalid admin URL or credentials; please correct and try again", + resp.StatusCode)}) } // Make a transformer that converts MS-Win default to UTF8: win16be := unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM) @@ -81,7 +88,7 @@ func ValidateNDESSCEPAdminURL(ctx context.Context, proxy fleet.NDESSCEPProxyInte unicodeReader := transform.NewReader(resp.Body, utf16bom) bodyText, err := io.ReadAll(unicodeReader) if err != nil { - return ctxerr.Wrap(ctx, err, "reading response body") + return "", ctxerr.Wrap(ctx, err, "reading response body") } htmlString := string(bodyText) @@ -92,13 +99,13 @@ func ValidateNDESSCEPAdminURL(ctx context.Context, proxy fleet.NDESSCEPProxyInte } if challenge == "" { if strings.Contains(htmlString, fullPasswordCache) { - return ctxerr.New(ctx, - "the password cache is full; please increase the number of cached passwords in NDES; by default, NDES caches 5 passwords and they expire 60 minutes after they are created") + return "", ctxerr.Wrap(ctx, + NDESPasswordCacheFullError{msg: "the password cache is full; please increase the number of cached passwords in NDES; by default, NDES caches 5 passwords and they expire 60 minutes after they are created"}) } - return ctxerr.New(ctx, - "could not retrieve the enrollment challenge password; invalid admin URL or credentials; please correct and try again") + return "", ctxerr.Wrap(ctx, + NDESInvalidError{msg: "could not retrieve the enrollment challenge password; invalid admin URL or credentials; please correct and try again"}) } - return nil + return challenge, nil } func ValidateNDESSCEPURL(ctx context.Context, proxy fleet.NDESSCEPProxyIntegration, logger log.Logger) error { diff --git a/pkg/optjson/optjson.go b/pkg/optjson/optjson.go index f306db305ad4..1270a0bb2b0c 100644 --- a/pkg/optjson/optjson.go +++ b/pkg/optjson/optjson.go @@ -171,10 +171,9 @@ func (s *Slice[T]) UnmarshalJSON(data []byte) error { } type Any[T any] struct { - Set bool - Valid bool - Value T - ZeroValue func() T + Set bool + Valid bool + Value T } func (s Any[T]) MarshalJSON() ([]byte, error) { @@ -190,10 +189,9 @@ func (s *Any[T]) UnmarshalJSON(data []byte) error { s.Valid = false if bytes.Equal(data, []byte("null")) { - // The key was set to null, blank the value if possible - if s.ZeroValue != nil { - s.Value = s.ZeroValue() - } + // The key was set to null, set value to zero/default value + var zero T + s.Value = zero return nil } diff --git a/pkg/optjson/optjson_test.go b/pkg/optjson/optjson_test.go index 963ec1f7980a..a8d18af450cf 100644 --- a/pkg/optjson/optjson_test.go +++ b/pkg/optjson/optjson_test.go @@ -452,27 +452,21 @@ func TestAny(t *testing.T) { return Any[Item]{Set: true, Valid: true, Value: item} } - zeroValue := func() Item { return Item{ID: -1} } - cases := []struct { data string wantErr string wantRes Any[Item] marshalAs string - zeroValue func() Item }{ {data: `{ "id": 1, "name": "bozo" }`, wantErr: "", wantRes: SetItem(Item{ID: 1, Name: "bozo"}), marshalAs: `{"id":1,"name":"bozo"}`}, {data: `null`, wantErr: "", wantRes: Any[Item]{Set: true, Valid: false}, marshalAs: `null`}, - {data: `null`, wantErr: "", wantRes: Any[Item]{Set: true, Valid: false, Value: Item{ID: -1}}, - marshalAs: `null`, zeroValue: zeroValue}, {data: `[]`, wantErr: "cannot unmarshal array", wantRes: Any[Item]{Set: true, Valid: false, Value: Item{}}, marshalAs: `null`}, } for _, c := range cases { t.Run(c.data, func(t *testing.T) { var s Any[Item] - s.ZeroValue = c.zeroValue err := json.Unmarshal([]byte(c.data), &s) if c.wantErr != "" { @@ -481,11 +475,7 @@ func TestAny(t *testing.T) { } else { require.NoError(t, err) } - assert.Equal(t, c.wantRes.Set, s.Set) - assert.Equal(t, c.wantRes.Valid, s.Valid) - assert.Equal(t, c.wantRes.Value.ID, s.Value.ID) - assert.Equal(t, c.wantRes.Value.Name, s.Value.Name) - // Don't compare ZeroValue, it's a function + assert.Equal(t, c.wantRes, s) b, err := json.Marshal(s) require.NoError(t, err) diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index 6026dcd9b140..daf6681a3b42 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -3708,6 +3708,20 @@ func (ds *Datastore) SetOrUpdateHostEmailsFromMdmIdpAccounts( ) } +func (ds *Datastore) GetHostEmails(ctx context.Context, hostUUID string, source string) ([]string, error) { + stmt := ` + SELECT email + FROM host_emails he + JOIN hosts h ON h.id = he.host_id + WHERE h.uuid = ? AND he.source = ? + ` + var emails []string + if err := sqlx.SelectContext(ctx, ds.reader(ctx), &emails, stmt, hostUUID, source); err != nil { + return nil, ctxerr.Wrap(ctx, err, "select host emails") + } + return emails, nil +} + // SetOrUpdateHostDisksSpace sets the available gigs and percentage of the // disks for the specified host. func (ds *Datastore) SetOrUpdateHostDisksSpace(ctx context.Context, hostID uint, gigsAvailable, percentAvailable, gigsTotal float64) error { diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 99b2cdb7d27c..afcc57969635 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -885,6 +885,8 @@ type Datastore interface { // SetOrUpdateHostEmailsFromMdmIdpAccounts sets or updates the host emails associated with the provided // host based on the MDM IdP account information associated with the provided fleet enrollment reference. SetOrUpdateHostEmailsFromMdmIdpAccounts(ctx context.Context, hostID uint, fleetEnrollmentRef string) error + // GetHostEmails returns the emails associated with the provided host for a given source, such as "google_chrome_profiles" + GetHostEmails(ctx context.Context, hostUUID string, source string) ([]string, error) SetOrUpdateHostDisksSpace(ctx context.Context, hostID uint, gigsAvailable, percentAvailable, gigsTotal float64) error SetOrUpdateHostDisksEncryption(ctx context.Context, hostID uint, encrypted bool) error // SetOrUpdateHostDiskEncryptionKey sets the base64, encrypted key for diff --git a/server/mdm/apple/apple_mdm.go b/server/mdm/apple/apple_mdm.go index 81ec4c8f52f6..2361c474cb66 100644 --- a/server/mdm/apple/apple_mdm.go +++ b/server/mdm/apple/apple_mdm.go @@ -49,6 +49,9 @@ const ( // FleetPayloadIdentifier is the value for the "PayloadIdentifier" // used by Fleet MDM on the enrollment profile. FleetPayloadIdentifier = "com.fleetdm.fleet.mdm.apple" + + // SCEPProxyPath is the HTTP path that serves the SCEP proxy service. The path is followed by identifier. + SCEPProxyPath = "/mdm/scep/proxy/" ) func ResolveAppleMDMURL(serverURL string) (string, error) { diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index a592559bdf5a..9723bc5d5b93 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -622,6 +622,8 @@ type UpdateMDMDataFunc func(ctx context.Context, hostID uint, enrolled bool) err type SetOrUpdateHostEmailsFromMdmIdpAccountsFunc func(ctx context.Context, hostID uint, fleetEnrollmentRef string) error +type GetHostEmailsFunc func(ctx context.Context, hostUUID string, source string) ([]string, error) + type SetOrUpdateHostDisksSpaceFunc func(ctx context.Context, hostID uint, gigsAvailable float64, percentAvailable float64, gigsTotal float64) error type SetOrUpdateHostDisksEncryptionFunc func(ctx context.Context, hostID uint, encrypted bool) error @@ -1994,6 +1996,9 @@ type DataStore struct { SetOrUpdateHostEmailsFromMdmIdpAccountsFunc SetOrUpdateHostEmailsFromMdmIdpAccountsFunc SetOrUpdateHostEmailsFromMdmIdpAccountsFuncInvoked bool + GetHostEmailsFunc GetHostEmailsFunc + GetHostEmailsFuncInvoked bool + SetOrUpdateHostDisksSpaceFunc SetOrUpdateHostDisksSpaceFunc SetOrUpdateHostDisksSpaceFuncInvoked bool @@ -4806,6 +4811,13 @@ func (s *DataStore) SetOrUpdateHostEmailsFromMdmIdpAccounts(ctx context.Context, return s.SetOrUpdateHostEmailsFromMdmIdpAccountsFunc(ctx, hostID, fleetEnrollmentRef) } +func (s *DataStore) GetHostEmails(ctx context.Context, hostUUID string, source string) ([]string, error) { + s.mu.Lock() + s.GetHostEmailsFuncInvoked = true + s.mu.Unlock() + return s.GetHostEmailsFunc(ctx, hostUUID, source) +} + func (s *DataStore) SetOrUpdateHostDisksSpace(ctx context.Context, hostID uint, gigsAvailable float64, percentAvailable float64, gigsTotal float64) error { s.mu.Lock() s.SetOrUpdateHostDisksSpaceFuncInvoked = true diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index c96777ea580e..d854a539092e 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -12,19 +12,23 @@ import ( "io" "mime/multipart" "net/http" + "net/url" "os" + "regexp" "strconv" "strings" "sync" "time" "github.com/docker/go-units" + eeservice "github.com/fleetdm/fleet/v4/ee/server/service" "github.com/fleetdm/fleet/v4/pkg/file" "github.com/fleetdm/fleet/v4/pkg/optjson" "github.com/fleetdm/fleet/v4/server" "github.com/fleetdm/fleet/v4/server/authz" "github.com/fleetdm/fleet/v4/server/config" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" + "github.com/fleetdm/fleet/v4/server/contexts/license" "github.com/fleetdm/fleet/v4/server/contexts/logging" "github.com/fleetdm/fleet/v4/server/contexts/viewer" "github.com/fleetdm/fleet/v4/server/fleet" @@ -47,6 +51,25 @@ import ( "go.mozilla.org/pkcs7" ) +const ( + // FleetVarNDESSCEPChallenge and other variables are used as $FLEET_VAR_. + // For example: $FLEET_VAR_NDES_SCEP_CHALLENGE + // Currently, we assume the variables are fully unique and not substrings of each other. + FleetVarNDESSCEPChallenge = "NDES_SCEP_CHALLENGE" + FleetVarNDESSCEPProxyURL = "NDES_SCEP_PROXY_URL" + FleetVarHostEndUserEmailIDP = "HOST_END_USER_EMAIL_IDP" +) + +var ( + profileVariableRegex = regexp.MustCompile(`(\$FLEET_VAR_(?P\w+))|(\${FLEET_VAR_(?P\w+)})`) + fleetVarNDESSCEPChallengeRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%s})`, FleetVarNDESSCEPChallenge, + FleetVarNDESSCEPChallenge)) + fleetVarNDESSCEPProxyURLRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%s})`, FleetVarNDESSCEPProxyURL, + FleetVarNDESSCEPProxyURL)) + fleetVarHostEndUserEmailIDP = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%s})`, FleetVarHostEndUserEmailIDP, + FleetVarHostEndUserEmailIDP)) +) + type getMDMAppleCommandResultsRequest struct { CommandUUID string `query:"command_uuid,optional"` } @@ -3168,6 +3191,16 @@ func ReconcileAppleDeclarations( return nil } +// install/removeTargets are maps from profileUUID -> command uuid and host +// UUIDs as the underlying MDM services are optimized to send one command to +// multiple hosts at the same time. Note that the same command uuid is used +// for all hosts in a given install/remove target operation. +type cmdTarget struct { + cmdUUID string + profIdent string + hostUUIDs []string +} + func ReconcileAppleProfiles( ctx context.Context, ds fleet.Datastore, @@ -3240,15 +3273,6 @@ func ReconcileAppleProfiles( // command. hostProfilesToCleanup := []*fleet.MDMAppleProfilePayload{} - // install/removeTargets are maps from profileUUID -> command uuid and host - // UUIDs as the underlying MDM services are optimized to send one command to - // multiple hosts at the same time. Note that the same command uuid is used - // for all hosts in a given install/remove target operation. - type cmdTarget struct { - cmdUUID string - profIdent string - hostUUIDs []string - } installTargets, removeTargets := make(map[string]*cmdTarget), make(map[string]*cmdTarget) for _, p := range toInstall { if pp, ok := profileIntersection.GetMatchingProfileInCurrentState(p); ok { @@ -3359,6 +3383,12 @@ func ReconcileAppleProfiles( return ctxerr.Wrap(ctx, err, "get profile contents") } + // Insert variables into profile contents + err = preprocessProfileContents(ctx, appConfig, ds, installTargets, profileContents) + if err != nil { + return err + } + type remoteResult struct { Err error CmdUUID string @@ -3438,6 +3468,231 @@ func ReconcileAppleProfiles( return nil } +func preprocessProfileContents( + ctx context.Context, + appConfig *fleet.AppConfig, + ds fleet.Datastore, + targets map[string]*cmdTarget, + profileContents map[string]mobileconfig.Mobileconfig, +) error { + + // This method replaces Fleet variables ($FLEET_VAR_) in the profile contents, generating a unique profile for each host. + // For a 2KB profile and 30K hosts, this method may generate ~60MB of profile data in memory. + + isNDESSCEPConfigured := func(profUUID string, target *cmdTarget) (bool, error) { + if !license.IsPremium(ctx) { + for _, hostUUID := range target.hostUUIDs { + err := ds.UpdateOrDeleteHostMDMAppleProfile(ctx, &fleet.HostMDMAppleProfile{ + CommandUUID: target.cmdUUID, + HostUUID: hostUUID, + Status: &fleet.MDMDeliveryFailed, + Detail: "NDES SCEP Proxy requires a Fleet Premium license.", + OperationType: fleet.MDMOperationTypeInstall, + }) + if err != nil { + return false, err + } + } + return false, nil + } + if !appConfig.Integrations.NDESSCEPProxy.Valid { + for _, hostUUID := range target.hostUUIDs { + err := ds.UpdateOrDeleteHostMDMAppleProfile(ctx, &fleet.HostMDMAppleProfile{ + CommandUUID: target.cmdUUID, + HostUUID: hostUUID, + Status: &fleet.MDMDeliveryFailed, + Detail: "NDES SCEP Proxy is not configured. " + + "Please configure in Settings > Integrations > Mobile Device Management > Simple Certificate Enrollment Protocol.", + OperationType: fleet.MDMOperationTypeInstall, + }) + if err != nil { + return false, err + } + } + } + return appConfig.Integrations.NDESSCEPProxy.Valid, nil + } + + var addedTargets map[string]*cmdTarget + for profUUID, target := range targets { + contents, ok := profileContents[profUUID] + if !ok { + // This should never happen + continue + } + + // Check if Fleet variables are present. + contentsStr := string(contents) + fleetVars := findFleetVariables(contentsStr) + if len(fleetVars) == 0 { + continue + } + + // Do common validation that applies to all hosts in the target + valid := true + for fleetVar := range fleetVars { + switch fleetVar { + case FleetVarNDESSCEPChallenge, FleetVarNDESSCEPProxyURL: + configured, err := isNDESSCEPConfigured(profUUID, target) + if err != nil { + return ctxerr.Wrap(ctx, err, "checking NDES SCEP configuration") + } + if !configured { + valid = false + break + } + default: + // Error out if we find an unknown variable + for _, hostUUID := range target.hostUUIDs { + err := ds.UpdateOrDeleteHostMDMAppleProfile(ctx, &fleet.HostMDMAppleProfile{ + CommandUUID: target.cmdUUID, + HostUUID: hostUUID, + Status: &fleet.MDMDeliveryFailed, + Detail: fmt.Sprintf("Unknown Fleet variable $FLEET_VAR_%s found in profile. Please update or remove.", + fleetVar), + OperationType: fleet.MDMOperationTypeInstall, + }) + if err != nil { + return ctxerr.Wrap(ctx, err, "updating host MDM Apple profile for unknown variable") + } + } + valid = false + break + } + } + if !valid { + // We marked the profile as failed, so we will not do any additional processing on it + delete(targets, profUUID) + continue + } + + // Currently, all supported Fleet variables are unique per host, so we split the profile into multiple profiles. + // We generate a new temporary profileUUID which is currently only used to install the profile. + // The profileUUID in host_mdm_apple_profiles is still the original profileUUID. + if addedTargets == nil { + addedTargets = make(map[string]*cmdTarget, 1) + } + for _, hostUUID := range target.hostUUIDs { + newProfUUID := uuid.NewString() + hostContents := contentsStr + + failed := false + for fleetVar := range fleetVars { + switch fleetVar { + case FleetVarNDESSCEPChallenge: + // Insert the SCEP challenge into the profile contents + challenge, err := eeservice.GetNDESSCEPChallenge(ctx, appConfig.Integrations.NDESSCEPProxy.Value) + if err != nil { + detail := "" + switch { + case errors.As(err, &eeservice.NDESInvalidError{}): + detail = fmt.Sprintf("Invalid NDES admin credentials. "+ + "Fleet couldn't populate $FLEET_VAR_%s. "+ + "Please update credentials in Settings > Integrations > Mobile Device Management > Simple Certificate Enrollment Protocol.", + FleetVarNDESSCEPChallenge) + case errors.As(err, &eeservice.NDESPasswordCacheFullError{}): + detail = fmt.Sprintf("The NDES password cache is full. "+ + "Fleet couldn't populate $FLEET_VAR_%s. "+ + "Please increase the number of cached passwords in NDES and try again.", + FleetVarNDESSCEPChallenge) + default: + detail = fmt.Sprintf("Fleet couldn't populate $FLEET_VAR_%s. %s", FleetVarNDESSCEPChallenge, err.Error()) + } + err := ds.UpdateOrDeleteHostMDMAppleProfile(ctx, &fleet.HostMDMAppleProfile{ + CommandUUID: target.cmdUUID, + HostUUID: hostUUID, + Status: &fleet.MDMDeliveryFailed, + Detail: detail, + OperationType: fleet.MDMOperationTypeInstall, + }) + if err != nil { + return ctxerr.Wrap(ctx, err, "updating host MDM Apple profile for NDES SCEP challenge") + } + failed = true + break + } + hostContents = fleetVarNDESSCEPChallengeRegexp.ReplaceAllString(hostContents, challenge) + case FleetVarNDESSCEPProxyURL: + // Insert the SCEP URL into the profile contents + proxyURL := fmt.Sprintf("%s%s%s", appConfig.ServerSettings.ServerURL, apple_mdm.SCEPProxyPath, + url.QueryEscape(fmt.Sprintf("%s,%s", hostUUID, profUUID))) + hostContents = fleetVarNDESSCEPProxyURLRegexp.ReplaceAllString(hostContents, proxyURL) + case FleetVarHostEndUserEmailIDP: + // Insert the end user email IDP into the profile contents + emails, err := ds.GetHostEmails(ctx, hostUUID, fleet.DeviceMappingMDMIdpAccounts) + if err != nil { + // This is a server error, so we exit. + return ctxerr.Wrap(ctx, err, "getting host emails") + } + if len(emails) == 0 { + // Error if we can't retrieve the end user email IDP + err := ds.UpdateOrDeleteHostMDMAppleProfile(ctx, &fleet.HostMDMAppleProfile{ + CommandUUID: target.cmdUUID, + HostUUID: hostUUID, + Status: &fleet.MDMDeliveryFailed, + Detail: fmt.Sprintf("There is no IdP email for this host. "+ + "Fleet couldn't populate $FLEET_VAR_%s. "+ + "[Learn more](https://fleetdm.com/learn-more-about/idp-email)", + FleetVarHostEndUserEmailIDP), + OperationType: fleet.MDMOperationTypeInstall, + }) + if err != nil { + return ctxerr.Wrap(ctx, err, "updating host MDM Apple profile for end user email IdP") + } + failed = true + break + } + hostContents = fleetVarHostEndUserEmailIDP.ReplaceAllString(hostContents, emails[0]) + default: + // This was handled in the above switch statement, so we should never reach this case + } + } + if !failed { + addedTargets[newProfUUID] = &cmdTarget{ + cmdUUID: target.cmdUUID, + profIdent: target.profIdent, + hostUUIDs: []string{hostUUID}, + } + } + } + // Remove the parent target, since we will use host-specific targets + delete(targets, profUUID) + } + if len(addedTargets) > 0 { + // Add the new host-specific targets to the original targets map + for profUUID, target := range addedTargets { + targets[profUUID] = target + } + } + return nil +} + +func findFleetVariables(contents string) map[string]interface{} { + var result map[string]interface{} + matches := profileVariableRegex.FindAllStringSubmatch(contents, -1) + if len(matches) == 0 { + return nil + } + nameToIndex := make(map[string]int, 2) + for i, name := range profileVariableRegex.SubexpNames() { + if name == "" { + continue + } + nameToIndex[name] = i + } + for _, match := range matches { + for _, i := range nameToIndex { + if match[i] != "" { + if result == nil { + result = make(map[string]interface{}) + } + result[match[i]] = struct{}{} + } + } + } + return result +} + // scepCertRenewalThresholdDays defines the number of days before a SCEP // certificate must be renewed. const scepCertRenewalThresholdDays = 180 diff --git a/server/service/handler.go b/server/service/handler.go index a7041c83af9f..9cc9ca6a7b82 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -1135,9 +1135,8 @@ func RegisterSCEPProxy( e := scepserver.MakeServerEndpointsWithIdentifier(scepService) e.GetEndpoint = scepserver.EndpointLoggingMiddleware(scepLogger)(e.GetEndpoint) e.PostEndpoint = scepserver.EndpointLoggingMiddleware(scepLogger)(e.PostEndpoint) - rootPath := "/mdm/scep/proxy/" - scepHandler := scepserver.MakeHTTPHandlerWithIdentifier(e, rootPath, scepLogger) - rootMux.Handle(rootPath, scepHandler) + scepHandler := scepserver.MakeHTTPHandlerWithIdentifier(e, apple_mdm.SCEPProxyPath, scepLogger) + rootMux.Handle(apple_mdm.SCEPProxyPath, scepHandler) return nil } From 9709598dea107201502587694c2d20fa2d597d6b Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Fri, 4 Oct 2024 12:02:45 -0500 Subject: [PATCH 13/27] Fix lint. --- tools/cloner-check/generated_files/appconfig.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/cloner-check/generated_files/appconfig.txt b/tools/cloner-check/generated_files/appconfig.txt index 9588b0cb5bcd..557865187b12 100644 --- a/tools/cloner-check/generated_files/appconfig.txt +++ b/tools/cloner-check/generated_files/appconfig.txt @@ -103,7 +103,6 @@ github.com/fleetdm/fleet/v4/server/fleet/NDESSCEPProxyIntegration URL string github.com/fleetdm/fleet/v4/server/fleet/NDESSCEPProxyIntegration AdminURL string github.com/fleetdm/fleet/v4/server/fleet/NDESSCEPProxyIntegration Username string github.com/fleetdm/fleet/v4/server/fleet/NDESSCEPProxyIntegration Password string -github.com/fleetdm/fleet/v4/pkg/optjson/Any[github.com/fleetdm/fleet/v4/server/fleet.NDESSCEPProxyIntegration] ZeroValue func() fleet.NDESSCEPProxyIntegration github.com/fleetdm/fleet/v4/server/fleet/AppConfig MDM fleet.MDM github.com/fleetdm/fleet/v4/server/fleet/MDM DeprecatedAppleBMDefaultTeam string github.com/fleetdm/fleet/v4/server/fleet/MDM AppleBusinessManager optjson.Slice[github.com/fleetdm/fleet/v4/server/fleet.MDMAppleABMAssignmentInfo] From 9d745323e9df8c56318f56beef1d13687039ae9d Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Fri, 4 Oct 2024 15:04:57 -0500 Subject: [PATCH 14/27] Added some testing of TestMDMAppleReconcileAppleProfiles --- server/service/apple_mdm.go | 3 + server/service/apple_mdm_test.go | 203 ++++++++++++++++++++++++++++++- 2 files changed, 202 insertions(+), 4 deletions(-) diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index d854a539092e..5f15c243a61a 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -3541,6 +3541,8 @@ func preprocessProfileContents( valid = false break } + case FleetVarHostEndUserEmailIDP: + // No extra validation needed for this variable default: // Error out if we find an unknown variable for _, hostUUID := range target.hostUUIDs { @@ -3653,6 +3655,7 @@ func preprocessProfileContents( profIdent: target.profIdent, hostUUIDs: []string{hostUUID}, } + profileContents[newProfUUID] = mobileconfig.Mobileconfig(hostContents) } } // Remove the parent target, since we will use host-specific targets diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go index aeabb3542b5c..41c29d01a675 100644 --- a/server/service/apple_mdm_test.go +++ b/server/service/apple_mdm_test.go @@ -17,6 +17,7 @@ import ( "math/big" "net/http" "net/http/httptest" + "net/url" "os" "strings" "sync" @@ -2209,6 +2210,7 @@ func TestMDMAppleReconcileAppleProfiles(t *testing.T) { cmdr := apple_mdm.NewMDMAppleCommander(mdmStorage, pusher) hostUUID, hostUUID2 := "ABC-DEF", "GHI-JKL" contents1 := []byte("test-content-1") + expectedContents1 := []byte("test-content-1") // used for Fleet variable substitution contents2 := []byte("test-content-2") contents4 := []byte("test-content-4") @@ -2267,10 +2269,10 @@ func TestMDMAppleReconcileAppleProfiles(t *testing.T) { mu.Unlock() require.NoError(t, err) - if !bytes.Equal(p7.Content, contents1) && !bytes.Equal(p7.Content, contents2) && + if !bytes.Equal(p7.Content, expectedContents1) && !bytes.Equal(p7.Content, contents2) && !bytes.Equal(p7.Content, contents4) { require.Failf(t, "profile contents don't match", "expected to contain %s, %s or %s but got %s", - contents1, contents2, contents4, p7.Content) + expectedContents1, contents2, contents4, p7.Content) } case "RemoveProfile": require.ElementsMatch(t, []string{hostUUID, hostUUID2}, id) @@ -2419,9 +2421,9 @@ func TestMDMAppleReconcileAppleProfiles(t *testing.T) { checkAndReset := func(t *testing.T, want bool, invoked *bool) { if want { - require.True(t, *invoked) + assert.True(t, *invoked) } else { - require.False(t, *invoked) + assert.False(t, *invoked) } *invoked = false } @@ -2530,6 +2532,199 @@ func TestMDMAppleReconcileAppleProfiles(t *testing.T) { checkAndReset(t, true, &ds.GetMDMAppleProfilesContentsFuncInvoked) checkAndReset(t, true, &ds.BulkUpsertMDMAppleHostProfilesFuncInvoked) }) + + // Zero profiles to remove + ds.ListMDMAppleProfilesToRemoveFunc = func(ctx context.Context) ([]*fleet.MDMAppleProfilePayload, error) { + return nil, nil + } + ds.BulkUpsertMDMAppleHostProfilesFunc = func(ctx context.Context, payload []*fleet.MDMAppleBulkUpsertHostProfilePayload) error { + if failedCall { + failedCheck(payload) + return nil + } + + // next call will be failed call, until reset + failedCall = true + + // first time it is called, it is to set the status to pending and all + // host profiles have a command uuid + cmdUUIDByProfileUUIDInstall := make(map[string]string) + cmdUUIDByProfileUUIDRemove := make(map[string]string) + copies := make([]*fleet.MDMAppleBulkUpsertHostProfilePayload, len(payload)) + for i, p := range payload { + if p.OperationType == fleet.MDMOperationTypeInstall { + existing, ok := cmdUUIDByProfileUUIDInstall[p.ProfileUUID] + if ok { + require.Equal(t, existing, p.CommandUUID) + } else { + cmdUUIDByProfileUUIDInstall[p.ProfileUUID] = p.CommandUUID + } + } else { + require.Equal(t, fleet.MDMOperationTypeRemove, p.OperationType) + existing, ok := cmdUUIDByProfileUUIDRemove[p.ProfileUUID] + if ok { + require.Equal(t, existing, p.CommandUUID) + } else { + cmdUUIDByProfileUUIDRemove[p.ProfileUUID] = p.CommandUUID + } + } + + // clear the command UUID (in a copy so that it does not affect the + // pointed-to struct) from the payload for the subsequent checks + copyp := *p + copyp.CommandUUID = "" + copies[i] = ©p + } + + require.ElementsMatch(t, []*fleet.MDMAppleBulkUpsertHostProfilePayload{ + { + ProfileUUID: p1, + ProfileIdentifier: "com.add.profile", + HostUUID: hostUUID, + OperationType: fleet.MDMOperationTypeInstall, + Status: &fleet.MDMDeliveryPending, + }, + { + ProfileUUID: p2, + ProfileIdentifier: "com.add.profile.two", + HostUUID: hostUUID, + OperationType: fleet.MDMOperationTypeInstall, + Status: &fleet.MDMDeliveryPending, + }, + { + ProfileUUID: p2, + ProfileIdentifier: "com.add.profile.two", + HostUUID: hostUUID2, + OperationType: fleet.MDMOperationTypeInstall, + Status: &fleet.MDMDeliveryPending, + }, + { + ProfileUUID: p4, + ProfileIdentifier: "com.add.profile.four", + HostUUID: hostUUID2, + OperationType: fleet.MDMOperationTypeInstall, + Status: &fleet.MDMDeliveryPending, + }, + }, copies) + return nil + } + + // Enable NDES + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + appCfg := &fleet.AppConfig{} + appCfg.ServerSettings.ServerURL = "https://test.example.com" + appCfg.MDM.EnabledAndConfigured = true + appCfg.Integrations.NDESSCEPProxy.Valid = true + return appCfg, nil + } + ctx = license.NewContext(ctx, &fleet.LicenseInfo{Tier: fleet.TierPremium}) + + t.Run("replace $FLEET_VAR_"+FleetVarNDESSCEPProxyURL, func(t *testing.T) { + var failedCount int + failedCall = false + failedCheck = func(payload []*fleet.MDMAppleBulkUpsertHostProfilePayload) { + failedCount++ + require.Len(t, payload, 0) + } + enqueueFailForOp = "" + newContents := "$FLEET_VAR_" + FleetVarNDESSCEPProxyURL + originalContents1 := contents1 + originalExpectedContents1 := expectedContents1 + contents1 = []byte(newContents) + expectedContents1 = []byte("https://test.example.com" + apple_mdm.SCEPProxyPath + url.QueryEscape(fmt.Sprintf("%s,%s", hostUUID, + p1))) + t.Cleanup(func() { + contents1 = originalContents1 + expectedContents1 = originalExpectedContents1 + }) + err := ReconcileAppleProfiles(ctx, ds, cmdr, kitlog.NewNopLogger()) + require.NoError(t, err) + require.Equal(t, 1, failedCount) + checkAndReset(t, true, &ds.ListMDMAppleProfilesToInstallFuncInvoked) + checkAndReset(t, true, &ds.ListMDMAppleProfilesToRemoveFuncInvoked) + checkAndReset(t, true, &ds.GetMDMAppleProfilesContentsFuncInvoked) + checkAndReset(t, true, &ds.BulkUpsertMDMAppleHostProfilesFuncInvoked) + }) + + t.Run("preprocessor fails on $FLEET_VAR_"+FleetVarHostEndUserEmailIDP, func(t *testing.T) { + var failedCount int + failedCall = false + failedCheck = func(payload []*fleet.MDMAppleBulkUpsertHostProfilePayload) { + failedCount++ + require.Len(t, payload, 0) + } + enqueueFailForOp = "" + newContents := "$FLEET_VAR_" + FleetVarHostEndUserEmailIDP + originalContents1 := contents1 + contents1 = []byte(newContents) + t.Cleanup(func() { + contents1 = originalContents1 + }) + ds.GetHostEmailsFunc = func(ctx context.Context, hostUUID string, source string) ([]string, error) { + return nil, errors.New("GetHostEmailsFuncError") + } + err := ReconcileAppleProfiles(ctx, ds, cmdr, kitlog.NewNopLogger()) + assert.ErrorContains(t, err, "GetHostEmailsFuncError") + checkAndReset(t, true, &ds.ListMDMAppleProfilesToInstallFuncInvoked) + checkAndReset(t, true, &ds.ListMDMAppleProfilesToRemoveFuncInvoked) + checkAndReset(t, true, &ds.GetMDMAppleProfilesContentsFuncInvoked) + checkAndReset(t, true, &ds.BulkUpsertMDMAppleHostProfilesFuncInvoked) + }) + + t.Run("bad $FLEET_VAR", func(t *testing.T) { + var failedCount int + failedCall = false + failedCheck = func(payload []*fleet.MDMAppleBulkUpsertHostProfilePayload) { + failedCount++ + require.Len(t, payload, 0) + } + enqueueFailForOp = "" + + // All profiles will have bad contents + badContents := "bad-content: $FLEET_VAR_BOZO" + originalContents1 := contents1 + originalContents2 := contents2 + originalContents4 := contents4 + contents1 = []byte(badContents) + contents2 = []byte(badContents) + contents4 = []byte(badContents) + t.Cleanup(func() { + contents1 = originalContents1 + contents2 = originalContents2 + contents4 = originalContents4 + }) + + profilesToInstall, err := ds.ListMDMAppleProfilesToInstallFunc(ctx) + hostUUIDs := make([]string, 0, len(profilesToInstall)) + for _, p := range profilesToInstall { + hostUUIDs = append(hostUUIDs, p.HostUUID) + } + + ds.UpdateOrDeleteHostMDMAppleProfileFunc = func(ctx context.Context, profile *fleet.HostMDMAppleProfile) error { + require.NotNil(t, profile) + require.NotNil(t, profile.Status) + assert.Equal(t, fleet.MDMDeliveryFailed, *profile.Status) + assert.Contains(t, profile.Detail, "FLEET_VAR_BOZO") + for i, hu := range hostUUIDs { + if hu == profile.HostUUID { + // remove element + hostUUIDs = append(hostUUIDs[:i], hostUUIDs[i+1:]...) + break + } + } + return nil + } + + err = ReconcileAppleProfiles(ctx, ds, cmdr, kitlog.NewNopLogger()) + require.NoError(t, err) + assert.Empty(t, hostUUIDs, "all host+profile combinations should be updated") + require.Equal(t, 1, failedCount) + checkAndReset(t, true, &ds.ListMDMAppleProfilesToInstallFuncInvoked) + checkAndReset(t, true, &ds.ListMDMAppleProfilesToRemoveFuncInvoked) + checkAndReset(t, true, &ds.GetMDMAppleProfilesContentsFuncInvoked) + checkAndReset(t, true, &ds.BulkUpsertMDMAppleHostProfilesFuncInvoked) + checkAndReset(t, true, &ds.UpdateOrDeleteHostMDMAppleProfileFuncInvoked) + }) } func TestAppleMDMFileVaultEscrowFunctions(t *testing.T) { From a5b3cf5d457406a89c929cadd506e6811aa1de52 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Fri, 4 Oct 2024 16:15:15 -0500 Subject: [PATCH 15/27] Completed tests for variable injection. --- ee/server/service/errors.go | 8 + server/datastore/mysql/hosts_test.go | 40 +++++ server/service/apple_mdm.go | 5 +- server/service/apple_mdm_test.go | 229 +++++++++++++++++++++++++++ 4 files changed, 281 insertions(+), 1 deletion(-) diff --git a/ee/server/service/errors.go b/ee/server/service/errors.go index 45f00558b7de..3a263c07a20c 100644 --- a/ee/server/service/errors.go +++ b/ee/server/service/errors.go @@ -26,6 +26,10 @@ func (e NDESInvalidError) Error() string { return e.msg } +func NewNDESInvalidError(msg string) NDESInvalidError { + return NDESInvalidError{msg: msg} +} + type NDESPasswordCacheFullError struct { msg string } @@ -33,3 +37,7 @@ type NDESPasswordCacheFullError struct { func (e NDESPasswordCacheFullError) Error() string { return e.msg } + +func NewNDESPasswordCacheFullError(msg string) NDESPasswordCacheFullError { + return NDESPasswordCacheFullError{msg: msg} +} diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index 2edc5ab39353..ed3e5ec3d70f 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -169,6 +169,7 @@ func TestHosts(t *testing.T) { {"HostsAddToTeamCleansUpTeamQueryResults", testHostsAddToTeamCleansUpTeamQueryResults}, {"UpdateHostIssues", testUpdateHostIssues}, {"ListUpcomingHostMaintenanceWindows", testListUpcomingHostMaintenanceWindows}, + {"GetHostEmails", testGetHostEmails}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { @@ -9654,3 +9655,42 @@ func testListUpcomingHostMaintenanceWindows(t *testing.T, ds *Datastore) { require.Equal(t, startTime.Round(time.Second), mW.StartsAt) require.Equal(t, timeZone, *mW.TimeZone) } + +func testGetHostEmails(t *testing.T, ds *Datastore) { + ctx := context.Background() + host, err := ds.NewHost(context.Background(), &fleet.Host{ + DetailUpdatedAt: time.Now(), + LabelUpdatedAt: time.Now(), + PolicyUpdatedAt: time.Now(), + SeenTime: time.Now(), + NodeKey: ptr.String("1"), + UUID: uuid.NewString(), + Hostname: "foo.local", + PrimaryIP: "192.168.1.1", + PrimaryMac: "30-65-EC-6F-C4-58", + }) + require.NoError(t, err) + + emails, err := ds.GetHostEmails(ctx, host.UUID, fleet.DeviceMappingMDMIdpAccounts) + require.NoError(t, err) + assert.Empty(t, emails) + + err = ds.ReplaceHostDeviceMapping(ctx, host.ID, []*fleet.HostDeviceMapping{ + { + HostID: host.ID, + Email: "foo@example.com", + Source: fleet.DeviceMappingMDMIdpAccounts, + }, + { + HostID: host.ID, + Email: "bar@example.com", + Source: fleet.DeviceMappingMDMIdpAccounts, + }, + }, fleet.DeviceMappingMDMIdpAccounts) + require.NoError(t, err) + + emails, err = ds.GetHostEmails(ctx, host.UUID, fleet.DeviceMappingMDMIdpAccounts) + require.NoError(t, err) + assert.ElementsMatch(t, []string{"foo@example.com", "bar@example.com"}, emails) + +} diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index 5f15c243a61a..bc582acf558c 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -70,6 +70,9 @@ var ( FleetVarHostEndUserEmailIDP)) ) +// Functions that can be overwritten in tests +var getNDESSCEPChallenge = eeservice.GetNDESSCEPChallenge + type getMDMAppleCommandResultsRequest struct { CommandUUID string `query:"command_uuid,optional"` } @@ -3583,7 +3586,7 @@ func preprocessProfileContents( switch fleetVar { case FleetVarNDESSCEPChallenge: // Insert the SCEP challenge into the profile contents - challenge, err := eeservice.GetNDESSCEPChallenge(ctx, appConfig.Integrations.NDESSCEPProxy.Value) + challenge, err := getNDESSCEPChallenge(ctx, appConfig.Integrations.NDESSCEPProxy.Value) if err != nil { detail := "" switch { diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go index 41c29d01a675..3185936d2ac9 100644 --- a/server/service/apple_mdm_test.go +++ b/server/service/apple_mdm_test.go @@ -25,6 +25,7 @@ import ( "testing" "time" + eeservice "github.com/fleetdm/fleet/v4/ee/server/service" "github.com/fleetdm/fleet/v4/pkg/optjson" "github.com/fleetdm/fleet/v4/server/authz" "github.com/fleetdm/fleet/v4/server/config" @@ -2727,6 +2728,234 @@ func TestMDMAppleReconcileAppleProfiles(t *testing.T) { }) } +func TestPreprocessProfileContents(t *testing.T) { + origGetNDESSCEPChallenge := getNDESSCEPChallenge + t.Cleanup(func() { + getNDESSCEPChallenge = origGetNDESSCEPChallenge + }) + + ctx := context.Background() + appCfg := &fleet.AppConfig{} + appCfg.ServerSettings.ServerURL = "https://test.example.com" + appCfg.MDM.EnabledAndConfigured = true + appCfg.Integrations.NDESSCEPProxy.Valid = true + ds := new(mock.Store) + + // No-op + err := preprocessProfileContents(ctx, appCfg, ds, nil, nil) + require.NoError(t, err) + + hostUUID := "host-1" + cmdUUID := "cmd-1" + var targets map[string]*cmdTarget + populateTargets := func() { + targets = map[string]*cmdTarget{ + "p1": {cmdUUID: cmdUUID, profIdent: "com.add.profile", hostUUIDs: []string{hostUUID}}, + } + } + populateTargets() + profileContents := map[string]mobileconfig.Mobileconfig{ + "p1": []byte("$FLEET_VAR_" + FleetVarNDESSCEPProxyURL), + } + + var updatedProfile *fleet.HostMDMAppleProfile + ds.UpdateOrDeleteHostMDMAppleProfileFunc = func(ctx context.Context, profile *fleet.HostMDMAppleProfile) error { + updatedProfile = profile + require.NotNil(t, updatedProfile.Status) + assert.Equal(t, fleet.MDMDeliveryFailed, *updatedProfile.Status) + assert.Equal(t, cmdUUID, updatedProfile.CommandUUID) + assert.Equal(t, hostUUID, updatedProfile.HostUUID) + assert.Equal(t, fleet.MDMOperationTypeInstall, updatedProfile.OperationType) + return nil + } + + // Can't use NDES SCEP proxy with free tier + ctx = license.NewContext(ctx, &fleet.LicenseInfo{Tier: fleet.TierFree}) + err = preprocessProfileContents(ctx, appCfg, ds, targets, profileContents) + require.NoError(t, err) + require.NotNil(t, updatedProfile) + assert.Contains(t, updatedProfile.Detail, "Premium license") + assert.Empty(t, targets) + + // Can't use NDES SCEP proxy without it being configured + ctx = license.NewContext(ctx, &fleet.LicenseInfo{Tier: fleet.TierPremium}) + appCfg.Integrations.NDESSCEPProxy.Valid = false + updatedProfile = nil + populateTargets() + err = preprocessProfileContents(ctx, appCfg, ds, targets, profileContents) + require.NoError(t, err) + require.NotNil(t, updatedProfile) + assert.Contains(t, updatedProfile.Detail, "not configured") + assert.Empty(t, targets) + + // Unknown variable + profileContents = map[string]mobileconfig.Mobileconfig{ + "p1": []byte("$FLEET_VAR_BOZO"), + } + appCfg.Integrations.NDESSCEPProxy.Valid = true + updatedProfile = nil + populateTargets() + err = preprocessProfileContents(ctx, appCfg, ds, targets, profileContents) + require.NoError(t, err) + require.NotNil(t, updatedProfile) + assert.Contains(t, updatedProfile.Detail, "FLEET_VAR_BOZO") + assert.Empty(t, targets) + + // Could not get NDES SCEP challenge + profileContents = map[string]mobileconfig.Mobileconfig{ + "p1": []byte("$FLEET_VAR_" + FleetVarNDESSCEPChallenge), + } + getNDESSCEPChallenge = func(ctx context.Context, proxy fleet.NDESSCEPProxyIntegration) (string, error) { + return "", eeservice.NewNDESInvalidError("NDES error") + } + updatedProfile = nil + populateTargets() + err = preprocessProfileContents(ctx, appCfg, ds, targets, profileContents) + require.NoError(t, err) + require.NotNil(t, updatedProfile) + assert.Contains(t, updatedProfile.Detail, "FLEET_VAR_"+FleetVarNDESSCEPChallenge) + assert.Contains(t, updatedProfile.Detail, "update credentials") + assert.Empty(t, targets) + + // Password cache full + getNDESSCEPChallenge = func(ctx context.Context, proxy fleet.NDESSCEPProxyIntegration) (string, error) { + return "", eeservice.NewNDESPasswordCacheFullError("NDES error") + } + updatedProfile = nil + populateTargets() + err = preprocessProfileContents(ctx, appCfg, ds, targets, profileContents) + require.NoError(t, err) + require.NotNil(t, updatedProfile) + assert.Contains(t, updatedProfile.Detail, "FLEET_VAR_"+FleetVarNDESSCEPChallenge) + assert.Contains(t, updatedProfile.Detail, "cached passwords") + assert.Empty(t, targets) + + // Other NDES challenge error + getNDESSCEPChallenge = func(ctx context.Context, proxy fleet.NDESSCEPProxyIntegration) (string, error) { + return "", errors.New("NDES error") + } + updatedProfile = nil + populateTargets() + err = preprocessProfileContents(ctx, appCfg, ds, targets, profileContents) + require.NoError(t, err) + require.NotNil(t, updatedProfile) + assert.Contains(t, updatedProfile.Detail, "FLEET_VAR_"+FleetVarNDESSCEPChallenge) + assert.NotContains(t, updatedProfile.Detail, "cached passwords") + assert.NotContains(t, updatedProfile.Detail, "update credentials") + assert.Empty(t, targets) + + // NDES challenge + challenge := "ndes-challenge" + getNDESSCEPChallenge = func(ctx context.Context, proxy fleet.NDESSCEPProxyIntegration) (string, error) { + return challenge, nil + } + updatedProfile = nil + populateTargets() + err = preprocessProfileContents(ctx, appCfg, ds, targets, profileContents) + require.NoError(t, err) + assert.Nil(t, updatedProfile) + require.NotEmpty(t, targets) + assert.Len(t, targets, 1) + for profUUID, target := range targets { + assert.NotEqual(t, profUUID, "p1") // new temporary UUID generated for specific host + assert.Equal(t, cmdUUID, target.cmdUUID) + assert.Equal(t, []string{hostUUID}, target.hostUUIDs) + assert.Equal(t, challenge, string(profileContents[profUUID])) + } + + // NDES SCEP proxy URL + profileContents = map[string]mobileconfig.Mobileconfig{ + "p1": []byte("$FLEET_VAR_" + FleetVarNDESSCEPProxyURL), + } + expectedURL := "https://test.example.com" + apple_mdm.SCEPProxyPath + url.QueryEscape(fmt.Sprintf("%s,%s", hostUUID, "p1")) + updatedProfile = nil + populateTargets() + err = preprocessProfileContents(ctx, appCfg, ds, targets, profileContents) + require.NoError(t, err) + assert.Nil(t, updatedProfile) + require.NotEmpty(t, targets) + assert.Len(t, targets, 1) + for profUUID, target := range targets { + assert.NotEqual(t, profUUID, "p1") // new temporary UUID generated for specific host + assert.Equal(t, cmdUUID, target.cmdUUID) + assert.Equal(t, []string{hostUUID}, target.hostUUIDs) + assert.Equal(t, expectedURL, string(profileContents[profUUID])) + } + + // No IdP email found + ds.GetHostEmailsFunc = func(ctx context.Context, hostUUID string, source string) ([]string, error) { + return nil, nil + } + profileContents = map[string]mobileconfig.Mobileconfig{ + "p1": []byte("$FLEET_VAR_" + FleetVarHostEndUserEmailIDP), + } + updatedProfile = nil + populateTargets() + err = preprocessProfileContents(ctx, appCfg, ds, targets, profileContents) + require.NoError(t, err) + require.NotNil(t, updatedProfile) + assert.Contains(t, updatedProfile.Detail, "FLEET_VAR_"+FleetVarHostEndUserEmailIDP) + assert.Contains(t, updatedProfile.Detail, "no IdP email") + assert.Empty(t, targets) + + // IdP email found + email := "user@example.com" + ds.GetHostEmailsFunc = func(ctx context.Context, hostUUID string, source string) ([]string, error) { + return []string{email}, nil + } + updatedProfile = nil + populateTargets() + err = preprocessProfileContents(ctx, appCfg, ds, targets, profileContents) + require.NoError(t, err) + assert.Nil(t, updatedProfile) + require.NotEmpty(t, targets) + assert.Len(t, targets, 1) + for profUUID, target := range targets { + assert.NotEqual(t, profUUID, "p1") // new temporary UUID generated for specific host + assert.Equal(t, cmdUUID, target.cmdUUID) + assert.Equal(t, []string{hostUUID}, target.hostUUIDs) + assert.Equal(t, email, string(profileContents[profUUID])) + } + + // multiple profiles, multiple hosts + populateTargets = func() { + targets = map[string]*cmdTarget{ + "p1": {cmdUUID: cmdUUID, profIdent: "com.add.profile", hostUUIDs: []string{hostUUID, "host-2"}}, // fails + "p2": {cmdUUID: cmdUUID, profIdent: "com.add.profile2", hostUUIDs: []string{hostUUID, "host-3"}}, // works + "p3": {cmdUUID: cmdUUID, profIdent: "com.add.profile2", hostUUIDs: []string{hostUUID, "host-4"}}, // no variables + } + } + populateTargets() + appCfg.Integrations.NDESSCEPProxy.Valid = false // NDES will fail + profileContents = map[string]mobileconfig.Mobileconfig{ + "p1": []byte("$FLEET_VAR_" + FleetVarNDESSCEPProxyURL), + "p2": []byte("$FLEET_VAR_" + FleetVarHostEndUserEmailIDP), + "p3": []byte("no variables"), + } + expectedHostsToFail := []string{hostUUID, "host-2", "host-3"} + ds.UpdateOrDeleteHostMDMAppleProfileFunc = func(ctx context.Context, profile *fleet.HostMDMAppleProfile) error { + updatedProfile = profile + require.NotNil(t, updatedProfile.Status) + assert.Equal(t, fleet.MDMDeliveryFailed, *updatedProfile.Status) + assert.Equal(t, cmdUUID, updatedProfile.CommandUUID) + assert.Contains(t, expectedHostsToFail, updatedProfile.HostUUID) + assert.Equal(t, fleet.MDMOperationTypeInstall, updatedProfile.OperationType) + return nil + } + err = preprocessProfileContents(ctx, appCfg, ds, targets, profileContents) + require.NoError(t, err) + require.NotEmpty(t, targets) + assert.Len(t, targets, 3) + assert.Nil(t, targets["p1"]) // error + assert.Nil(t, targets["p2"]) // renamed + assert.NotNil(t, targets["p3"]) // normal, no variables + for profUUID, target := range targets { + assert.Contains(t, [][]string{{hostUUID}, {"host-3"}, {hostUUID, "host-4"}}, target.hostUUIDs) + assert.Equal(t, cmdUUID, target.cmdUUID) + assert.Contains(t, []string{email, "no variables"}, string(profileContents[profUUID])) + } +} + func TestAppleMDMFileVaultEscrowFunctions(t *testing.T) { svc := Service{} From 899a8d7fee7bb196beb73e2d17f225c2dfb06023 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Mon, 7 Oct 2024 09:54:57 -0500 Subject: [PATCH 16/27] Hard delete password when NDES SCEP config is cleared. --- server/datastore/mysql/apple_mdm.go | 9 +++++++++ server/datastore/mysql/apple_mdm_test.go | 16 ++++++++++++++++ server/fleet/datastore.go | 3 +++ server/mock/datastore_mock.go | 12 ++++++++++++ server/service/appconfig.go | 7 +++++++ server/service/appconfig_test.go | 19 +++++++++++++++++-- server/service/apple_mdm_test.go | 4 ++-- 7 files changed, 66 insertions(+), 4 deletions(-) diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go index ad18242eb754..bd8d78172979 100644 --- a/server/datastore/mysql/apple_mdm.go +++ b/server/datastore/mysql/apple_mdm.go @@ -4833,6 +4833,15 @@ func (ds *Datastore) DeleteMDMConfigAssetsByName(ctx context.Context, assetNames }) } +func (ds *Datastore) HardDeleteMDMConfigAsset(ctx context.Context, assetName fleet.MDMAssetName) error { + stmt := ` +DELETE FROM mdm_config_assets +WHERE name = ?` + _, err := ds.writer(ctx).ExecContext(ctx, stmt, assetName) + // ctxerr.Wrap returns nil if err is nil + return ctxerr.Wrap(ctx, err, "hard delete mdm config asset") +} + func softDeleteMDMConfigAssetsByName(ctx context.Context, tx sqlx.ExtContext, assetNames []fleet.MDMAssetName) error { stmt := ` UPDATE diff --git a/server/datastore/mysql/apple_mdm_test.go b/server/datastore/mysql/apple_mdm_test.go index 3090bdb04d61..cfc9e50756cd 100644 --- a/server/datastore/mysql/apple_mdm_test.go +++ b/server/datastore/mysql/apple_mdm_test.go @@ -5802,6 +5802,22 @@ func testMDMConfigAsset(t *testing.T, ds *Datastore) { require.NotEmpty(t, got.DeletionUUID) require.NotEmpty(t, got.DeletedAt) } + + // Hard delete + err = ds.HardDeleteMDMConfigAsset(ctx, fleet.MDMAssetCACert) + require.NoError(t, err) + a, err = ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetCAKey}) + require.ErrorAs(t, err, &nfe) + require.Nil(t, a) + + var result bool + err = sqlx.GetContext(ctx, ds.reader(ctx), &result, "SELECT 1 FROM mdm_config_assets WHERE name = ?", fleet.MDMAssetCACert) + assert.ErrorIs(t, err, sql.ErrNoRows) + + // other (non-hard deleted asset still present) + err = sqlx.GetContext(ctx, ds.reader(ctx), &result, "SELECT 1 FROM mdm_config_assets WHERE name = ?", fleet.MDMAssetCAKey) + assert.NoError(t, err) + assert.True(t, result) } func testListIOSAndIPadOSToRefetch(t *testing.T, ds *Datastore) { diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index afcc57969635..e8ffef06fa6b 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -1353,6 +1353,9 @@ type Datastore interface { // DeleteMDMConfigAssetsByName soft deletes the given MDM config assets. DeleteMDMConfigAssetsByName(ctx context.Context, assetNames []MDMAssetName) error + // HardDeleteMDMConfigAsset permanently deletes the given MDM config asset. + HardDeleteMDMConfigAsset(ctx context.Context, assetName MDMAssetName) error + // ReplaceMDMConfigAssets replaces (soft delete if they exist + insert) `MDMConfigAsset`s in a // single transaction. Useful for "renew" flows where users are updating the assets with newly // generated ones. diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index 9723bc5d5b93..75c6b25e66bf 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -882,6 +882,8 @@ type GetAllMDMConfigAssetsHashesFunc func(ctx context.Context, assetNames []flee type DeleteMDMConfigAssetsByNameFunc func(ctx context.Context, assetNames []fleet.MDMAssetName) error +type HardDeleteMDMConfigAssetFunc func(ctx context.Context, assetName fleet.MDMAssetName) error + type ReplaceMDMConfigAssetsFunc func(ctx context.Context, assets []fleet.MDMConfigAsset) error type GetABMTokenByOrgNameFunc func(ctx context.Context, orgName string) (*fleet.ABMToken, error) @@ -2386,6 +2388,9 @@ type DataStore struct { DeleteMDMConfigAssetsByNameFunc DeleteMDMConfigAssetsByNameFunc DeleteMDMConfigAssetsByNameFuncInvoked bool + HardDeleteMDMConfigAssetFunc HardDeleteMDMConfigAssetFunc + HardDeleteMDMConfigAssetFuncInvoked bool + ReplaceMDMConfigAssetsFunc ReplaceMDMConfigAssetsFunc ReplaceMDMConfigAssetsFuncInvoked bool @@ -5721,6 +5726,13 @@ func (s *DataStore) DeleteMDMConfigAssetsByName(ctx context.Context, assetNames return s.DeleteMDMConfigAssetsByNameFunc(ctx, assetNames) } +func (s *DataStore) HardDeleteMDMConfigAsset(ctx context.Context, assetName fleet.MDMAssetName) error { + s.mu.Lock() + s.HardDeleteMDMConfigAssetFuncInvoked = true + s.mu.Unlock() + return s.HardDeleteMDMConfigAssetFunc(ctx, assetName) +} + func (s *DataStore) ReplaceMDMConfigAssets(ctx context.Context, assets []fleet.MDMConfigAsset) error { s.mu.Lock() s.ReplaceMDMConfigAssetsFuncInvoked = true diff --git a/server/service/appconfig.go b/server/service/appconfig.go index 98eaaa289095..5d5c8fb12c68 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -338,6 +338,7 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle // Validate NDES SCEP URLs if they changed. Validation is done in both dry run and normal mode. switch { case !license.IsPremium(): + invalid.Append("integrations.ndes_scep_proxy", ErrMissingLicense.Error()) appConfig.Integrations.NDESSCEPProxy.Valid = false case !newAppConfig.Integrations.NDESSCEPProxy.Set: // Nothing is set -- keep the old value @@ -345,6 +346,12 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle case !newAppConfig.Integrations.NDESSCEPProxy.Valid: // User is explicitly clearing this setting appConfig.Integrations.NDESSCEPProxy.Valid = false + // Delete stored password + if !applyOpts.DryRun { + if err := svc.ds.HardDeleteMDMConfigAsset(ctx, fleet.MDMAssetNDESPassword); err != nil { + return nil, ctxerr.Wrap(ctx, err, "delete NDES SCEP password") + } + } default: // User is updating the setting appConfig.Integrations.NDESSCEPProxy.Value.URL = fleet.Preprocess(newAppConfig.Integrations.NDESSCEPProxy.Value.URL) diff --git a/server/service/appconfig_test.go b/server/service/appconfig_test.go index d9556ae5517d..e03b3a289a3b 100644 --- a/server/service/appconfig_test.go +++ b/server/service/appconfig_test.go @@ -1451,8 +1451,8 @@ func TestModifyAppConfigForNDESSCEPProxy(t *testing.T) { // SCEP proxy not configured for free users ac, err := svc.ModifyAppConfig(ctx, []byte(jsonPayload), fleet.ApplySpecOptions{}) - require.NoError(t, err) - assert.False(t, ac.Integrations.NDESSCEPProxy.Valid) + assert.ErrorContains(t, err, ErrMissingLicense.Error()) + assert.ErrorContains(t, err, "integrations.ndes_scep_proxy") origValidateNDESSCEPURL := validateNDESSCEPURL origValidateNDESSCEPAdminURL := validateNDESSCEPAdminURL @@ -1567,6 +1567,20 @@ func TestModifyAppConfigForNDESSCEPProxy(t *testing.T) { } } ` + // First, dry run. + ac, err = svc.ModifyAppConfig(ctx, []byte(payload), fleet.ApplySpecOptions{DryRun: true}) + require.NoError(t, err) + assert.False(t, ac.Integrations.NDESSCEPProxy.Valid) + // Also check what was saved. + assert.False(t, appConfig.Integrations.NDESSCEPProxy.Valid) + assert.False(t, validateNDESSCEPURLCalled) + assert.False(t, validateNDESSCEPAdminURLCalled) + assert.False(t, ds.HardDeleteMDMConfigAssetFuncInvoked, "DB write should not happen in dry run") + + // Second, real run. + ds.HardDeleteMDMConfigAssetFunc = func(ctx context.Context, assetName fleet.MDMAssetName) error { + return nil + } ac, err = svc.ModifyAppConfig(ctx, []byte(payload), fleet.ApplySpecOptions{}) require.NoError(t, err) assert.False(t, ac.Integrations.NDESSCEPProxy.Valid) @@ -1574,4 +1588,5 @@ func TestModifyAppConfigForNDESSCEPProxy(t *testing.T) { assert.False(t, appConfig.Integrations.NDESSCEPProxy.Valid) assert.False(t, validateNDESSCEPURLCalled) assert.False(t, validateNDESSCEPAdminURLCalled) + assert.True(t, ds.HardDeleteMDMConfigAssetFuncInvoked) } diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go index 3185936d2ac9..2fd61da0c705 100644 --- a/server/service/apple_mdm_test.go +++ b/server/service/apple_mdm_test.go @@ -2695,7 +2695,7 @@ func TestMDMAppleReconcileAppleProfiles(t *testing.T) { contents4 = originalContents4 }) - profilesToInstall, err := ds.ListMDMAppleProfilesToInstallFunc(ctx) + profilesToInstall, _ := ds.ListMDMAppleProfilesToInstallFunc(ctx) hostUUIDs := make([]string, 0, len(profilesToInstall)) for _, p := range profilesToInstall { hostUUIDs = append(hostUUIDs, p.HostUUID) @@ -2716,7 +2716,7 @@ func TestMDMAppleReconcileAppleProfiles(t *testing.T) { return nil } - err = ReconcileAppleProfiles(ctx, ds, cmdr, kitlog.NewNopLogger()) + err := ReconcileAppleProfiles(ctx, ds, cmdr, kitlog.NewNopLogger()) require.NoError(t, err) assert.Empty(t, hostUUIDs, "all host+profile combinations should be updated") require.Equal(t, 1, failedCount) From b909f8c686f9b39a24d8dfab861cc7d2f498bf5c Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Mon, 7 Oct 2024 10:14:55 -0500 Subject: [PATCH 17/27] Updated profile preprocessor to retrieve NDES password. --- Makefile | 1 - server/service/apple_mdm.go | 17 ++++++++++++++++- server/service/apple_mdm_test.go | 12 ++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 0a4e4a2f124a..9e63214ebd9a 100644 --- a/Makefile +++ b/Makefile @@ -139,7 +139,6 @@ dump-test-schema: go run ./tools/dbutils ./server/datastore/mysql/schema.sql test-go: dump-test-schema generate-mock - cat --number ./server/service/appconfig_test.go go test -tags full,fts5,netgo ${GO_TEST_EXTRA_FLAGS_VAR} -parallel 8 -coverprofile=coverage.txt -covermode=atomic -coverpkg=github.com/fleetdm/fleet/v4/... ./cmd/... ./ee/... ./orbit/pkg/... ./orbit/cmd/orbit ./pkg/... ./server/... ./tools/... analyze-go: diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index bc582acf558c..0d693e5db97b 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -3516,6 +3516,9 @@ func preprocessProfileContents( return appConfig.Integrations.NDESSCEPProxy.Valid, nil } + // Copy of NDES SCEP config which will contain unencrypted password, if needed + var ndesConfig *fleet.NDESSCEPProxyIntegration + var addedTargets map[string]*cmdTarget for profUUID, target := range targets { contents, ok := profileContents[profUUID] @@ -3585,8 +3588,20 @@ func preprocessProfileContents( for fleetVar := range fleetVars { switch fleetVar { case FleetVarNDESSCEPChallenge: + if ndesConfig == nil { + // Retrieve the NDES admin password. This is done once per run. + configAssets, err := ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetNDESPassword}) + if err != nil { + return ctxerr.Wrap(ctx, err, "getting NDES password") + } + // Copy config struct by value + configWithPassword := appConfig.Integrations.NDESSCEPProxy.Value + configWithPassword.Password = string(configAssets[fleet.MDMAssetNDESPassword].Value) + // Store the config with the password for later use + ndesConfig = &configWithPassword + } // Insert the SCEP challenge into the profile contents - challenge, err := getNDESSCEPChallenge(ctx, appConfig.Integrations.NDESSCEPProxy.Value) + challenge, err := getNDESSCEPChallenge(ctx, *ndesConfig) if err != nil { detail := "" switch { diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go index 2fd61da0c705..6bcb63791fba 100644 --- a/server/service/apple_mdm_test.go +++ b/server/service/apple_mdm_test.go @@ -2801,11 +2801,20 @@ func TestPreprocessProfileContents(t *testing.T) { assert.Contains(t, updatedProfile.Detail, "FLEET_VAR_BOZO") assert.Empty(t, targets) + ndesPassword := "test-password" + ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, + assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + return map[fleet.MDMAssetName]fleet.MDMConfigAsset{ + fleet.MDMAssetNDESPassword: {Value: []byte(ndesPassword)}, + }, nil + } + // Could not get NDES SCEP challenge profileContents = map[string]mobileconfig.Mobileconfig{ "p1": []byte("$FLEET_VAR_" + FleetVarNDESSCEPChallenge), } getNDESSCEPChallenge = func(ctx context.Context, proxy fleet.NDESSCEPProxyIntegration) (string, error) { + assert.Equal(t, ndesPassword, proxy.Password) return "", eeservice.NewNDESInvalidError("NDES error") } updatedProfile = nil @@ -2819,6 +2828,7 @@ func TestPreprocessProfileContents(t *testing.T) { // Password cache full getNDESSCEPChallenge = func(ctx context.Context, proxy fleet.NDESSCEPProxyIntegration) (string, error) { + assert.Equal(t, ndesPassword, proxy.Password) return "", eeservice.NewNDESPasswordCacheFullError("NDES error") } updatedProfile = nil @@ -2832,6 +2842,7 @@ func TestPreprocessProfileContents(t *testing.T) { // Other NDES challenge error getNDESSCEPChallenge = func(ctx context.Context, proxy fleet.NDESSCEPProxyIntegration) (string, error) { + assert.Equal(t, ndesPassword, proxy.Password) return "", errors.New("NDES error") } updatedProfile = nil @@ -2847,6 +2858,7 @@ func TestPreprocessProfileContents(t *testing.T) { // NDES challenge challenge := "ndes-challenge" getNDESSCEPChallenge = func(ctx context.Context, proxy fleet.NDESSCEPProxyIntegration) (string, error) { + assert.Equal(t, ndesPassword, proxy.Password) return challenge, nil } updatedProfile = nil From 26c3cc29c2c1aea62ee4be675ef894f2acf78549 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Mon, 7 Oct 2024 16:30:48 -0500 Subject: [PATCH 18/27] Implemented pass-through SCEP proxy. --- cmd/fleet/serve.go | 2 +- ee/server/service/scep_proxy.go | 82 +++++++++++++++++++++++++---- server/datastore/mysql/apple_mdm.go | 23 ++++++++ server/fleet/datastore.go | 4 ++ server/mdm/scep/server/endpoint.go | 6 +-- server/mdm/scep/server/service.go | 22 ++++++++ server/mdm/scep/server/transport.go | 1 - server/mock/datastore_mock.go | 12 +++++ server/service/handler.go | 2 + 9 files changed, 140 insertions(+), 14 deletions(-) diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index 8af84d88a998..4f1b43a5f76c 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -1110,7 +1110,7 @@ the way that the Fleet server works. // SCEP proxy (for NDES, etc.) if license.IsPremium() { - if err = service.RegisterSCEPProxy(rootMux, logger); err != nil { + if err = service.RegisterSCEPProxy(rootMux, ds, logger); err != nil { initFatal(err, "setup SCEP proxy") } } diff --git a/ee/server/service/scep_proxy.go b/ee/server/service/scep_proxy.go index f54dcca75a5d..fcdbd9a15725 100644 --- a/ee/server/service/scep_proxy.go +++ b/ee/server/service/scep_proxy.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net/http" + "net/url" "regexp" "strings" @@ -20,35 +21,98 @@ import ( "golang.org/x/text/transform" ) -var _ scepserver.Service = (*scepProxyService)(nil) +var _ scepserver.ServiceWithIdentifier = (*scepProxyService)(nil) var challengeRegex = regexp.MustCompile(`(?i)The enrollment challenge password is: (?P\S*)`) const fullPasswordCache = "The password cache is full." type scepProxyService struct { + ds fleet.Datastore // info logging is implemented in the service middleware layer. debugLogger log.Logger } -func (svc *scepProxyService) GetCACaps(_ context.Context) ([]byte, error) { - return nil, errors.New("not implemented") +// GetCACaps returns a list of SCEP options which are supported by the server. +// It is a pass-through call to the SCEP server. +func (svc *scepProxyService) GetCACaps(ctx context.Context) ([]byte, error) { + appConfig, err := svc.ds.AppConfig(ctx) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "getting app config") + } + if !appConfig.Integrations.NDESSCEPProxy.Valid { + // Return error that implements kithttp.StatusCoder interface + return nil, ctxerr.Wrap(ctx, &scepserver.BadRequestError{Message: "SCEP proxy is not configured"}, "getting app config") + } + client, err := scepclient.New(appConfig.Integrations.NDESSCEPProxy.Value.URL, svc.debugLogger) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "creating SCEP client") + } + return client.GetCACaps(ctx) } -func (svc *scepProxyService) GetCACert(_ context.Context, _ string) ([]byte, int, error) { - return nil, 0, errors.New("not implemented") +// GetCACert returns the CA certificate(s) from SCEP server. +// It is a pass-through call to the SCEP server. +func (svc *scepProxyService) GetCACert(ctx context.Context, message string) ([]byte, int, error) { + appConfig, err := svc.ds.AppConfig(ctx) + if err != nil { + return nil, 0, ctxerr.Wrap(ctx, err, "getting app config") + } + if !appConfig.Integrations.NDESSCEPProxy.Valid { + // Return error that implements kithttp.StatusCoder interface + return nil, 0, ctxerr.Wrap(ctx, &scepserver.BadRequestError{Message: "SCEP proxy is not configured"}, "getting app config") + } + client, err := scepclient.New(appConfig.Integrations.NDESSCEPProxy.Value.URL, svc.debugLogger) + if err != nil { + return nil, 0, ctxerr.Wrap(ctx, err, "creating SCEP client") + } + return client.GetCACert(ctx, message) } -func (svc *scepProxyService) PKIOperation(_ context.Context, data []byte) ([]byte, error) { - return nil, errors.New("not implemented") +func (svc *scepProxyService) PKIOperation(ctx context.Context, data []byte, identifier string) ([]byte, error) { + appConfig, err := svc.ds.AppConfig(ctx) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "getting app config") + } + if !appConfig.Integrations.NDESSCEPProxy.Valid { + // Return error that implements kithttp.StatusCoder interface + return nil, ctxerr.Wrap(ctx, &scepserver.BadRequestError{Message: "SCEP proxy is not configured"}, "getting app config") + } + + // Validate the identifier. In the future, we will also use the identifier for tracking the certificate renewal. + parsedID, err := url.QueryUnescape(identifier) + if err != nil { + // Should never happen since the identifier comes in as a path variable + return nil, ctxerr.Wrap(ctx, err, "unescaping identifier in URL path") + } + parsedIDs := strings.Split(parsedID, ",") + if len(parsedIDs) != 2 || parsedIDs[0] == "" || parsedIDs[1] == "" { + // Return error that implements kithttp.StatusCoder interface + return nil, ctxerr.Wrap(ctx, &scepserver.BadRequestError{Message: "invalid identifier in URL path"}, "parsing identifier") + } + profile, err := svc.ds.GetHostMDMAppleProfile(ctx, parsedIDs[0], parsedIDs[1]) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "getting host MDM profile") + } + if profile == nil { + // Return error that implements kithttp.StatusCoder interface + return nil, ctxerr.Wrap(ctx, &scepserver.BadRequestError{Message: "unknown identifier in URL path"}, "getting host MDM profile") + } + + client, err := scepclient.New(appConfig.Integrations.NDESSCEPProxy.Value.URL, svc.debugLogger) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "creating SCEP client") + } + return client.PKIOperation(ctx, data) } func (svc *scepProxyService) GetNextCACert(ctx context.Context) ([]byte, error) { - return nil, errors.New("not implemented") + return nil, errors.New("GetNextCACert is not implemented for SCEP proxy") } // NewSCEPProxyService creates a new scep proxy service -func NewSCEPProxyService(logger log.Logger) scepserver.Service { +func NewSCEPProxyService(ds fleet.Datastore, logger log.Logger) scepserver.ServiceWithIdentifier { return &scepProxyService{ + ds: ds, debugLogger: logger, } } diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go index bd8d78172979..c6081fec95a7 100644 --- a/server/datastore/mysql/apple_mdm.go +++ b/server/datastore/mysql/apple_mdm.go @@ -455,6 +455,29 @@ WHERE return profiles, nil } +func (ds *Datastore) GetHostMDMAppleProfile(ctx context.Context, hostUUID string, profileUUID string) (*fleet.HostMDMAppleProfile, error) { + stmt := ` + SELECT + profile_uuid, + profile_name AS name, + profile_identifier AS identifier, + status, + COALESCE(operation_type, '') AS operation_type, + COALESCE(detail, '') AS detail + FROM + host_mdm_apple_profiles + WHERE + host_uuid = ? AND profile_uuid = ?` + var profile fleet.HostMDMAppleProfile + if err := sqlx.GetContext(ctx, ds.reader(ctx), &profile, stmt, hostUUID, profileUUID); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + return nil, err + } + return &profile, nil +} + func (ds *Datastore) NewMDMAppleEnrollmentProfile( ctx context.Context, payload fleet.MDMAppleEnrollmentProfilePayload, diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index e8ffef06fa6b..96fe5d082a06 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -1062,6 +1062,10 @@ type Datastore interface { // GetHostMDMAppleProfiles returns the MDM profile information for the specified host UUID. GetHostMDMAppleProfiles(ctx context.Context, hostUUID string) ([]HostMDMAppleProfile, error) + // GetHostMDMAppleProfile returns the MDM profile information for the specified host UUID and profile UUID. + // nil is returned if the profile is not found. + GetHostMDMAppleProfile(ctx context.Context, hostUUID string, profileUUID string) (*HostMDMAppleProfile, error) + CleanupDiskEncryptionKeysOnTeamChange(ctx context.Context, hostIDs []uint, newTeamID *uint) error // NewMDMAppleEnrollmentProfile creates and returns new enrollment profile. diff --git a/server/mdm/scep/server/endpoint.go b/server/mdm/scep/server/endpoint.go index 1d23b27ea6e5..8999006842e5 100644 --- a/server/mdm/scep/server/endpoint.go +++ b/server/mdm/scep/server/endpoint.go @@ -101,7 +101,7 @@ func MakeServerEndpoints(svc Service) *Endpoints { } } -func MakeServerEndpointsWithIdentifier(svc Service) *Endpoints { +func MakeServerEndpointsWithIdentifier(svc ServiceWithIdentifier) *Endpoints { e := MakeSCEPEndpointWithIdentifier(svc) return &Endpoints{ GetEndpoint: e, @@ -165,7 +165,7 @@ type SCEPRequest struct { func (r SCEPRequest) scepOperation() string { return r.Operation } -func MakeSCEPEndpointWithIdentifier(svc Service) endpoint.Endpoint { +func MakeSCEPEndpointWithIdentifier(svc ServiceWithIdentifier) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { req := request.(SCEPRequestWithIdentifier) resp := SCEPResponse{operation: req.Operation} @@ -175,7 +175,7 @@ func MakeSCEPEndpointWithIdentifier(svc Service) endpoint.Endpoint { case "GetCACert": resp.Data, resp.CACertNum, resp.Err = svc.GetCACert(ctx, string(req.Message)) case "PKIOperation": - resp.Data, resp.Err = svc.PKIOperation(ctx, req.Message) + resp.Data, resp.Err = svc.PKIOperation(ctx, req.Message, req.Identifier) default: return nil, &BadRequestError{Message: "operation not implemented"} } diff --git a/server/mdm/scep/server/service.go b/server/mdm/scep/server/service.go index d387f3f39cce..699fc6313323 100644 --- a/server/mdm/scep/server/service.go +++ b/server/mdm/scep/server/service.go @@ -32,6 +32,28 @@ type Service interface { GetNextCACert(ctx context.Context) ([]byte, error) } +// ServiceWithIdentifier is the interface for all supported SCEP server operations. +type ServiceWithIdentifier interface { + // GetCACaps returns a list of options + // which are supported by the server. + GetCACaps(ctx context.Context) ([]byte, error) + + // GetCACert returns CA certificate or + // a CA certificate chain with intermediates + // in a PKCS#7 Degenerate Certificates format + // message is an optional string for the CA + GetCACert(ctx context.Context, message string) ([]byte, int, error) + + // PKIOperation handles incoming SCEP messages such as PKCSReq and + // sends back a CertRep PKIMessag. + PKIOperation(ctx context.Context, msg []byte, identifier string) ([]byte, error) + + // GetNextCACert returns a replacement certificate or certificate chain + // when the old one expires. The response format is a PKCS#7 Degenerate + // Certificates type. + GetNextCACert(ctx context.Context) ([]byte, error) +} + type service struct { // The service certificate and key for SCEP exchanges. These are // quite likely the same as the CA keypair but may be its own SCEP diff --git a/server/mdm/scep/server/transport.go b/server/mdm/scep/server/transport.go index a55d06b35875..798caf5690ea 100644 --- a/server/mdm/scep/server/transport.go +++ b/server/mdm/scep/server/transport.go @@ -131,7 +131,6 @@ func decodeSCEPRequestWithIdentifier(_ context.Context, r *http.Request) (interf operation := r.URL.Query().Get("operation") identifier := mux.Vars(r)["identifier"] - // TODO: verify identifier if len(operation) == 0 { return nil, &BadRequestError{Message: "missing operation"} } diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index 75c6b25e66bf..dd766be6dfd4 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -728,6 +728,8 @@ type DeleteMDMAppleConfigProfileByTeamAndIdentifierFunc func(ctx context.Context type GetHostMDMAppleProfilesFunc func(ctx context.Context, hostUUID string) ([]fleet.HostMDMAppleProfile, error) +type GetHostMDMAppleProfileFunc func(ctx context.Context, hostUUID string, profileUUID string) (*fleet.HostMDMAppleProfile, error) + type CleanupDiskEncryptionKeysOnTeamChangeFunc func(ctx context.Context, hostIDs []uint, newTeamID *uint) error type NewMDMAppleEnrollmentProfileFunc func(ctx context.Context, enrollmentPayload fleet.MDMAppleEnrollmentProfilePayload) (*fleet.MDMAppleEnrollmentProfile, error) @@ -2157,6 +2159,9 @@ type DataStore struct { GetHostMDMAppleProfilesFunc GetHostMDMAppleProfilesFunc GetHostMDMAppleProfilesFuncInvoked bool + GetHostMDMAppleProfileFunc GetHostMDMAppleProfileFunc + GetHostMDMAppleProfileFuncInvoked bool + CleanupDiskEncryptionKeysOnTeamChangeFunc CleanupDiskEncryptionKeysOnTeamChangeFunc CleanupDiskEncryptionKeysOnTeamChangeFuncInvoked bool @@ -5187,6 +5192,13 @@ func (s *DataStore) GetHostMDMAppleProfiles(ctx context.Context, hostUUID string return s.GetHostMDMAppleProfilesFunc(ctx, hostUUID) } +func (s *DataStore) GetHostMDMAppleProfile(ctx context.Context, hostUUID string, profileUUID string) (*fleet.HostMDMAppleProfile, error) { + s.mu.Lock() + s.GetHostMDMAppleProfileFuncInvoked = true + s.mu.Unlock() + return s.GetHostMDMAppleProfileFunc(ctx, hostUUID, profileUUID) +} + func (s *DataStore) CleanupDiskEncryptionKeysOnTeamChange(ctx context.Context, hostIDs []uint, newTeamID *uint) error { s.mu.Lock() s.CleanupDiskEncryptionKeysOnTeamChangeFuncInvoked = true diff --git a/server/service/handler.go b/server/service/handler.go index 9cc9ca6a7b82..d981ce6ac02b 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -1126,9 +1126,11 @@ func registerSCEP( func RegisterSCEPProxy( rootMux *http.ServeMux, + ds fleet.Datastore, logger kitlog.Logger, ) error { scepService := eeservice.NewSCEPProxyService( + ds, kitlog.With(logger, "component", "scep-proxy-service"), ) scepLogger := kitlog.With(logger, "component", "http-scep-proxy") From b298bc68c692068ba3f78c4daf3d0c4edec9154f Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Tue, 8 Oct 2024 10:43:38 -0500 Subject: [PATCH 19/27] Added basic SCEP proxy tests. --- ee/server/service/scep_proxy.go | 37 +++-- server/mdm/scep/server/endpoint.go | 2 +- server/mdm/scep/server/service.go | 4 +- server/service/integration_mdm_test.go | 173 +++++++++++++++++++++- server/service/testdata/PKCSReq.der | Bin 0 -> 2461 bytes server/service/testdata/externalCA/ca.crt | 30 ++++ server/service/testdata/externalCA/ca.key | 54 +++++++ server/service/testdata/externalCA/ca.pem | 30 ++++ server/service/testing_utils.go | 9 ++ 9 files changed, 325 insertions(+), 14 deletions(-) create mode 100755 server/service/testdata/PKCSReq.der create mode 100644 server/service/testdata/externalCA/ca.crt create mode 100644 server/service/testdata/externalCA/ca.key create mode 100644 server/service/testdata/externalCA/ca.pem diff --git a/ee/server/service/scep_proxy.go b/ee/server/service/scep_proxy.go index fcdbd9a15725..4dfbc4bc9ea3 100644 --- a/ee/server/service/scep_proxy.go +++ b/ee/server/service/scep_proxy.go @@ -24,7 +24,10 @@ import ( var _ scepserver.ServiceWithIdentifier = (*scepProxyService)(nil) var challengeRegex = regexp.MustCompile(`(?i)The enrollment challenge password is: (?P\S*)`) -const fullPasswordCache = "The password cache is full." +const ( + fullPasswordCache = "The password cache is full." + MessageSCEPProxyNotConfigured = "SCEP proxy is not configured" +) type scepProxyService struct { ds fleet.Datastore @@ -41,13 +44,18 @@ func (svc *scepProxyService) GetCACaps(ctx context.Context) ([]byte, error) { } if !appConfig.Integrations.NDESSCEPProxy.Valid { // Return error that implements kithttp.StatusCoder interface - return nil, ctxerr.Wrap(ctx, &scepserver.BadRequestError{Message: "SCEP proxy is not configured"}, "getting app config") + return nil, &scepserver.BadRequestError{Message: MessageSCEPProxyNotConfigured} } client, err := scepclient.New(appConfig.Integrations.NDESSCEPProxy.Value.URL, svc.debugLogger) if err != nil { return nil, ctxerr.Wrap(ctx, err, "creating SCEP client") } - return client.GetCACaps(ctx) + res, err := client.GetCACaps(ctx) + if err != nil { + return res, ctxerr.Wrap(ctx, err, + fmt.Sprintf("Could not GetCACaps from SCEP server %s", appConfig.Integrations.NDESSCEPProxy.Value.URL)) + } + return res, nil } // GetCACert returns the CA certificate(s) from SCEP server. @@ -59,13 +67,18 @@ func (svc *scepProxyService) GetCACert(ctx context.Context, message string) ([]b } if !appConfig.Integrations.NDESSCEPProxy.Valid { // Return error that implements kithttp.StatusCoder interface - return nil, 0, ctxerr.Wrap(ctx, &scepserver.BadRequestError{Message: "SCEP proxy is not configured"}, "getting app config") + return nil, 0, &scepserver.BadRequestError{Message: MessageSCEPProxyNotConfigured} } client, err := scepclient.New(appConfig.Integrations.NDESSCEPProxy.Value.URL, svc.debugLogger) if err != nil { return nil, 0, ctxerr.Wrap(ctx, err, "creating SCEP client") } - return client.GetCACert(ctx, message) + res, num, err := client.GetCACert(ctx, message) + if err != nil { + return res, num, ctxerr.Wrap(ctx, err, + fmt.Sprintf("Could not GetCACert from SCEP server %s", appConfig.Integrations.NDESSCEPProxy.Value.URL)) + } + return res, num, nil } func (svc *scepProxyService) PKIOperation(ctx context.Context, data []byte, identifier string) ([]byte, error) { @@ -75,7 +88,7 @@ func (svc *scepProxyService) PKIOperation(ctx context.Context, data []byte, iden } if !appConfig.Integrations.NDESSCEPProxy.Valid { // Return error that implements kithttp.StatusCoder interface - return nil, ctxerr.Wrap(ctx, &scepserver.BadRequestError{Message: "SCEP proxy is not configured"}, "getting app config") + return nil, &scepserver.BadRequestError{Message: MessageSCEPProxyNotConfigured} } // Validate the identifier. In the future, we will also use the identifier for tracking the certificate renewal. @@ -87,7 +100,7 @@ func (svc *scepProxyService) PKIOperation(ctx context.Context, data []byte, iden parsedIDs := strings.Split(parsedID, ",") if len(parsedIDs) != 2 || parsedIDs[0] == "" || parsedIDs[1] == "" { // Return error that implements kithttp.StatusCoder interface - return nil, ctxerr.Wrap(ctx, &scepserver.BadRequestError{Message: "invalid identifier in URL path"}, "parsing identifier") + return nil, &scepserver.BadRequestError{Message: "invalid identifier in URL path"} } profile, err := svc.ds.GetHostMDMAppleProfile(ctx, parsedIDs[0], parsedIDs[1]) if err != nil { @@ -95,17 +108,23 @@ func (svc *scepProxyService) PKIOperation(ctx context.Context, data []byte, iden } if profile == nil { // Return error that implements kithttp.StatusCoder interface - return nil, ctxerr.Wrap(ctx, &scepserver.BadRequestError{Message: "unknown identifier in URL path"}, "getting host MDM profile") + return nil, &scepserver.BadRequestError{Message: "unknown identifier in URL path"} } client, err := scepclient.New(appConfig.Integrations.NDESSCEPProxy.Value.URL, svc.debugLogger) if err != nil { return nil, ctxerr.Wrap(ctx, err, "creating SCEP client") } - return client.PKIOperation(ctx, data) + res, err := client.PKIOperation(ctx, data) + if err != nil { + return res, ctxerr.Wrap(ctx, err, + fmt.Sprintf("Could not do PKIOperation on SCEP server %s", appConfig.Integrations.NDESSCEPProxy.Value.URL)) + } + return res, nil } func (svc *scepProxyService) GetNextCACert(ctx context.Context) ([]byte, error) { + // NDES on Windows Server 2022 does not support this, as advertised via GetCACaps return nil, errors.New("GetNextCACert is not implemented for SCEP proxy") } diff --git a/server/mdm/scep/server/endpoint.go b/server/mdm/scep/server/endpoint.go index 8999006842e5..58163fbce039 100644 --- a/server/mdm/scep/server/endpoint.go +++ b/server/mdm/scep/server/endpoint.go @@ -179,7 +179,7 @@ func MakeSCEPEndpointWithIdentifier(svc ServiceWithIdentifier) endpoint.Endpoint default: return nil, &BadRequestError{Message: "operation not implemented"} } - return resp, nil + return resp, resp.Err } } diff --git a/server/mdm/scep/server/service.go b/server/mdm/scep/server/service.go index 699fc6313323..e7bbbcb4c2b9 100644 --- a/server/mdm/scep/server/service.go +++ b/server/mdm/scep/server/service.go @@ -74,8 +74,10 @@ type service struct { debugLogger log.Logger } +const DefaultCACaps = "Renewal\nSHA-1\nSHA-256\nAES\nDES3\nSCEPStandard\nPOSTPKIOperation" + func (svc *service) GetCACaps(ctx context.Context) ([]byte, error) { - defaultCaps := []byte("Renewal\nSHA-1\nSHA-256\nAES\nDES3\nSCEPStandard\nPOSTPKIOperation") + defaultCaps := []byte(DefaultCACaps) return defaultCaps, nil } diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 6eee1327bb2b..bb9199baced0 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -31,6 +31,7 @@ import ( "testing" "time" + eeservice "github.com/fleetdm/fleet/v4/ee/server/service" "github.com/fleetdm/fleet/v4/pkg/file" "github.com/fleetdm/fleet/v4/pkg/fleetdbase" "github.com/fleetdm/fleet/v4/pkg/mdm/mdmtest" @@ -55,6 +56,9 @@ import ( "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm" "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push" nanomdm_pushsvc "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push/service" + "github.com/fleetdm/fleet/v4/server/mdm/scep/depot" + filedepot "github.com/fleetdm/fleet/v4/server/mdm/scep/depot/file" + scepserver "github.com/fleetdm/fleet/v4/server/mdm/scep/server" "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/service/mock" "github.com/fleetdm/fleet/v4/server/service/osquery_utils" @@ -63,10 +67,12 @@ import ( "github.com/fleetdm/fleet/v4/server/worker" kitlog "github.com/go-kit/log" "github.com/google/uuid" + "github.com/gorilla/mux" "github.com/groob/plist" "github.com/jmoiron/sqlx" micromdm "github.com/micromdm/micromdm/mdm/mdm" "github.com/smallstep/pkcs7" + "github.com/smallstep/scep" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -197,7 +203,7 @@ func (s *integrationMDMTestSuite) SetupSuite() { bootstrapPackageStore = s3.SetupTestBootstrapPackageStore(s.T(), "integration-tests", "") } - config := TestServerOpts{ + serverConfig := TestServerOpts{ License: &fleet.LicenseInfo{ Tier: fleet.TierPremium, }, @@ -283,7 +289,8 @@ func (s *integrationMDMTestSuite) SetupSuite() { } }, }, - APNSTopic: "com.apple.mgmt.External.10ac3ce5-4668-4e58-b69a-b2b5ce667589", + APNSTopic: "com.apple.mgmt.External.10ac3ce5-4668-4e58-b69a-b2b5ce667589", + EnableSCEPProxy: true, } // ensure all our tests support challenges with invalid XML characters @@ -292,7 +299,7 @@ func (s *integrationMDMTestSuite) SetupSuite() { {Name: fleet.MDMAssetSCEPChallenge, Value: []byte(s.scepChallenge)}, }) require.NoError(s.T(), err) - users, server := RunServerForTestsWithDS(s.T(), s.ds, &config) + users, server := RunServerForTestsWithDS(s.T(), s.ds, &serverConfig) s.server = server s.users = users s.token = s.getTestAdminToken() @@ -11423,3 +11430,163 @@ func (s *integrationMDMTestSuite) TestOTAEnrollment() { require.NotNil(t, hostByIdentifierResp.Host.TeamID) require.Equal(t, specResp.TeamIDsByName["newteam"], *hostByIdentifierResp.Host.TeamID) } + +func (s *integrationMDMTestSuite) TestSCEPProxy() { + t := s.T() + ctx := context.Background() + + data, err := os.ReadFile("./testdata/PKCSReq.der") + require.NoError(t, err) + message := base64.StdEncoding.EncodeToString(data) + + // NDES not configured + res := s.DoRawNoAuth("GET", apple_mdm.SCEPProxyPath+"1%2C1", nil, http.StatusBadRequest) + errBody, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Contains(t, string(errBody), "missing operation") + // Provide SCEP operation (GetCACaps) + res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+"1%2C1", nil, http.StatusBadRequest, nil, "operation", "GetCACaps") + errBody, err = io.ReadAll(res.Body) + require.NoError(t, err) + assert.Contains(t, string(errBody), eeservice.MessageSCEPProxyNotConfigured) + // Provide SCEP operation (GetCACerts) + res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+"1%2C1", nil, http.StatusBadRequest, nil, "operation", "GetCACert") + errBody, err = io.ReadAll(res.Body) + require.NoError(t, err) + assert.Contains(t, string(errBody), eeservice.MessageSCEPProxyNotConfigured) + // Provide SCEP operation (PKIOperation) + res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+"1%2C1", nil, http.StatusBadRequest, nil, "operation", "PKIOperation", + "message", message) + errBody, err = io.ReadAll(res.Body) + require.NoError(t, err) + assert.Contains(t, string(errBody), eeservice.MessageSCEPProxyNotConfigured) + // Provide SCEP operation (GetNextCACert) + res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+"1%2C1", nil, http.StatusBadRequest, nil, "operation", "GetNextCACert") + errBody, err = io.ReadAll(res.Body) + require.NoError(t, err) + assert.Contains(t, string(errBody), "not implemented") + + // Add an MDM profile + globalProfiles := [][]byte{ + mobileconfigForTest("N1", "I1"), + } + // add global profile + s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: globalProfiles}, http.StatusNoContent) + // Create a host and then enroll to MDM. + host, mdmDevice := createHostThenEnrollMDM(s.ds, s.server.URL, t) + setupPusher(s, t, mdmDevice) + // trigger a profile sync + s.awaitTriggerProfileSchedule(t) + profiles, err := s.ds.ListMDMAppleConfigProfiles(ctx, nil) + require.NoError(t, err) + require.Len(t, profiles, 1) + profileUUID := profiles[0].ProfileUUID + identifier := url.PathEscape(host.UUID + "," + profileUUID) + + // Configure a bad SCEP URL + appConf, err := s.ds.AppConfig(context.Background()) + require.NoError(s.T(), err) + appConf.Integrations.NDESSCEPProxy.Valid = true + appConf.Integrations.NDESSCEPProxy.Value.URL = "https://httpstat.us/410" + err = s.ds.SaveAppConfig(context.Background(), appConf) + + res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier, nil, http.StatusInternalServerError, nil, "operation", "GetCACaps") + errBody, err = io.ReadAll(res.Body) + require.NoError(t, err) + assert.Contains(t, string(errBody), "Could not GetCACaps from SCEP server") + res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier, nil, http.StatusInternalServerError, nil, "operation", "GetCACert") + errBody, err = io.ReadAll(res.Body) + require.NoError(t, err) + assert.Contains(t, string(errBody), "Could not GetCACert from SCEP server") + res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier, nil, http.StatusInternalServerError, nil, "operation", + "PKIOperation", "message", message) + errBody, err = io.ReadAll(res.Body) + require.NoError(t, err) + assert.Contains(t, string(errBody), "Could not do PKIOperation on SCEP server") + + // Spin up an "external" SCEP server, which Fleet server will proxy + newSCEPServer := func(t *testing.T, opts ...scepserver.ServiceOption) *httptest.Server { + var server *httptest.Server + teardown := func() { + if server != nil { + server.Close() + } + os.Remove("./testdata/externalCA/serial") + os.Remove("./testdata/externalCA/index.txt") + } + t.Cleanup(teardown) + + var err error + var certDepot depot.Depot // cert storage + certDepot, err = filedepot.NewFileDepot("./testdata/externalCA") + if err != nil { + t.Fatal(err) + } + certDepot = &noopCertDepot{certDepot} + crt, key, err := certDepot.CA([]byte{}) + if err != nil { + t.Fatal(err) + } + + var svc scepserver.Service // scep service + svc, err = scepserver.NewService(crt[0], key, scepserver.NopCSRSigner()) + if err != nil { + t.Fatal(err) + } + logger := kitlog.NewNopLogger() + e := scepserver.MakeServerEndpoints(svc) + scepHandler := scepserver.MakeHTTPHandler(e, svc, logger) + r := mux.NewRouter() + r.Handle("/scep", scepHandler) + server = httptest.NewServer(r) + return server + } + scepServer := newSCEPServer(t) + + appConf.Integrations.NDESSCEPProxy.Value.URL = scepServer.URL + "/scep" + err = s.ds.SaveAppConfig(context.Background(), appConf) + require.NoError(s.T(), err) + + // GetCACaps + res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier, nil, http.StatusOK, nil, "operation", "GetCACaps") + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Equal(t, scepserver.DefaultCACaps, string(body)) + + // GetCACert + res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier, nil, http.StatusOK, nil, "operation", "GetCACert") + body, err = io.ReadAll(res.Body) + require.NoError(t, err) + certs, err := x509.ParseCertificates(body) + require.NoError(t, err) + assert.Len(t, certs, 1) + + // PKIOperation + // Invalid identifier format + res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier+"%2Cbozo", nil, http.StatusBadRequest, nil, "operation", + "PKIOperation", "message", message) + errBody, err = io.ReadAll(res.Body) + require.NoError(t, err) + assert.Contains(t, string(errBody), "invalid identifier") + // Unknown host/profile + res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+"bozoHost%2CbozoProfile", nil, http.StatusBadRequest, nil, "operation", + "PKIOperation", "message", message) + errBody, err = io.ReadAll(res.Body) + require.NoError(t, err) + assert.Contains(t, string(errBody), "unknown identifier") + // "Good" request. Note, a cert is not returned here because the message is not a fully valid SCEP request. However, building a valid SCEP request is a bit involved, + // and this request is sufficient for testing our proxy functionality. + res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier, nil, http.StatusOK, nil, "operation", + "PKIOperation", "message", message) + body, err = io.ReadAll(res.Body) + require.NoError(t, err) + pkiMessage, err := scep.ParsePKIMessage(body, scep.WithCACerts(certs)) + require.NoError(t, err) + assert.Equal(t, scep.CertRep, pkiMessage.MessageType) +} + +type noopCertDepot struct{ depot.Depot } + +func (d *noopCertDepot) Put(_ string, _ *x509.Certificate) error { + return nil +} diff --git a/server/service/testdata/PKCSReq.der b/server/service/testdata/PKCSReq.der new file mode 100755 index 0000000000000000000000000000000000000000..71938c0696e353e1c9a33bcafa91747ec2608eb5 GIT binary patch literal 2461 zcma)-c{r4N8^`B)X3SW!HTFGPMD#pnj4jGCvW+cE8Ec{<2MJLco*0FqWRHf9LQbeq z)HJ1Z)Nl@xC5*%|l3l#-v7OO-z17uU=lScozrXvsf7kW9zxU^c5P>|bfCk-)z9bCb zKn5a^0wDr)5CC8yNC3;HDS`&&_)rjnIru#dU?3Q441yu^WYh05nkVbW;7yPq7VV0`;?b@LOkpudWFr#9;{s0kAJOvn`+6Bq5yFG!ro77`;csOD zfDeU1K$NY5gP{=r;2I-6cwbq-7t_?v_m>`@J6GCEv#URHtir{Ab>(EFy7WY@eO;ds z_3oOxUbU&V@@JExmR+MBDGl7{h8Lw)KFZYjvZvE*r9pK|PMQ5%e0jGy%Wp>AtND## zS)G}+&rUqsd$%&w2Og$rDdzM~?bb2WM57v)LUDp-w|DAm$<#B^rBj(eRpr@5ncIeq_g;oi>_cw`+Jl|Vc zWNjbrcI0?Z{$*(mNoG8^$=2mTZQ}g}$+`YKnQ?BIYK7A^-pElkkGY0wV<~dzgDwd{Plm~T7HAS&wXM!U8P-~6D@wzPAuCR$&Y6|_cB$-E8@ki(m{F(%-eya12C(h=-8aZ5& zh*R;fK8QBW8oW=bhN~Cvrv~Ws9rMX3@N9bcz_3l%CEC`}wRZ$@t4QA2SUyh0a?$bP zAVJ`Hw1Zwwh+tLuSn(_Mll^ZR#om>$0#+7WOWQg}(eh<-+yF(dBPT6~n0oa(?R&Gt&6 zmW`>5B|b1#JTqu7>bYX}QfZx%On<2DM2Shsa~cWSs?i;NMG;@$lATySkzrjWCz%-J zzvvtGmTgnwxLNzW^{TwBq%Hfw%_9uQ{P~@q$JC~q(~{D{xUrct@~u-laO*L^sH93l zCPgN+b<#k6baok!7D~8#!9RWMJWg-qY7m4oq+RM7F(>Z=OhipLR zt;%PIyH1yi<9(Cdr1xz*p&H(I_1Q05Z-Tg}m>!mT+ACy8`CxUx>|kE176Sp7c&M1? zHTXRP4Fcd#^Do51&kk4ylG;E9dXp%{f}(Q3ob0G_zSy(~iJx_&Fp5J4WDE3sDDwY}VJnl4&-@nve0PS%K7 z{NA_o3VvNZ%khM?2Y5p8-8nA08IXP5Hg{5F4+l++yw|yD+`YVik)BDpq#vYqw(K|ej=pm=g7>~#hw)o? z**z?3@D-2{7d}`dy1qUf-gGg&C(sFZ$9ZhRa8ZP@Ce}cJI1iYD=zI(xD4r1?fGA22j9F4M_bf$`wTcU>$=&gShyw zeNx=E5pO`BUlR-P@!`<-%FtWRbe z-1bdu-xDYP4sg-5l#TmbpLV7~Buq0QV*920xty3cbDY)tO?O22U51%)Zz`qmm{(`s zi=BU>RqZ$$G2dfylSM!37GH?=!E%!q{r|}GwkSOH@CMDUownG=9jY)Msl6^cY_;35 z$7<8ycx8$~qRYgMFm*O(*tstlffJem(?N_)%P-6BGO-m>!pD<}o2hBV>V}dvEE4me zZNE`QesoePze2<{{{>aI7ga%V3(}3vb_GaM!{Vd)rG|Ez@#kJCH51vcn~&#WWK>t01XR$3wY#|`5~JR7R2!1rEwEZG&2Vvg>PvBe6Z+GqXemaKgGaO z7zwK~Z(@ibYf1j^NUxyKnHAv~bpX=;p*RlKz^j|`2GIc0Bf+Kw$dbhSh?e@g76d&a zfncEnk#xztQPnXsg@5$N*Od>Tux!uasCL%*=jpzMyFRpuy*7w+Jylqzx{^3xlNhjW z7L&h{h7!0nOs#UWd@mI$c~zF>ZZ0F0MXferEcC@r()Pzzk}m^J~In)ze0m%bs3P)FT|pLMmK`+3F Date: Tue, 8 Oct 2024 11:19:31 -0500 Subject: [PATCH 20/27] Fix license fails in Free tier. And lint. --- server/service/appconfig.go | 84 +++++++++++++------------- server/service/appconfig_test.go | 2 +- server/service/integration_mdm_test.go | 5 +- 3 files changed, 47 insertions(+), 44 deletions(-) diff --git a/server/service/appconfig.go b/server/service/appconfig.go index 5d5c8fb12c68..30e9e54849ef 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -336,54 +336,56 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle } // Validate NDES SCEP URLs if they changed. Validation is done in both dry run and normal mode. - switch { - case !license.IsPremium(): + if newAppConfig.Integrations.NDESSCEPProxy.Set && !license.IsPremium() { invalid.Append("integrations.ndes_scep_proxy", ErrMissingLicense.Error()) appConfig.Integrations.NDESSCEPProxy.Valid = false - case !newAppConfig.Integrations.NDESSCEPProxy.Set: - // Nothing is set -- keep the old value - appConfig.Integrations.NDESSCEPProxy = oldAppConfig.Integrations.NDESSCEPProxy - case !newAppConfig.Integrations.NDESSCEPProxy.Valid: - // User is explicitly clearing this setting - appConfig.Integrations.NDESSCEPProxy.Valid = false - // Delete stored password - if !applyOpts.DryRun { - if err := svc.ds.HardDeleteMDMConfigAsset(ctx, fleet.MDMAssetNDESPassword); err != nil { - return nil, ctxerr.Wrap(ctx, err, "delete NDES SCEP password") - } - } - default: - // User is updating the setting - appConfig.Integrations.NDESSCEPProxy.Value.URL = fleet.Preprocess(newAppConfig.Integrations.NDESSCEPProxy.Value.URL) - appConfig.Integrations.NDESSCEPProxy.Value.AdminURL = fleet.Preprocess(newAppConfig.Integrations.NDESSCEPProxy.Value.AdminURL) - appConfig.Integrations.NDESSCEPProxy.Value.Username = fleet.Preprocess(newAppConfig.Integrations.NDESSCEPProxy.Value.Username) - // do not preprocess password - - validateAdminURL, validateSCEPURL := false, false - newSCEPProxy := appConfig.Integrations.NDESSCEPProxy.Value - if !oldAppConfig.Integrations.NDESSCEPProxy.Valid { - validateAdminURL, validateSCEPURL = true, true - } else { - oldSCEPProxy := oldAppConfig.Integrations.NDESSCEPProxy.Value - if newSCEPProxy.URL != oldSCEPProxy.URL { - validateSCEPURL = true + } else { + switch { + case !newAppConfig.Integrations.NDESSCEPProxy.Set: + // Nothing is set -- keep the old value + appConfig.Integrations.NDESSCEPProxy = oldAppConfig.Integrations.NDESSCEPProxy + case !newAppConfig.Integrations.NDESSCEPProxy.Valid: + // User is explicitly clearing this setting + appConfig.Integrations.NDESSCEPProxy.Valid = false + // Delete stored password + if !applyOpts.DryRun { + if err := svc.ds.HardDeleteMDMConfigAsset(ctx, fleet.MDMAssetNDESPassword); err != nil { + return nil, ctxerr.Wrap(ctx, err, "delete NDES SCEP password") + } } - if newSCEPProxy.AdminURL != oldSCEPProxy.AdminURL || - newSCEPProxy.Username != oldSCEPProxy.Username || - (newSCEPProxy.Password != "" && newSCEPProxy.Password != fleet.MaskedPassword) { - validateAdminURL = true + default: + // User is updating the setting + appConfig.Integrations.NDESSCEPProxy.Value.URL = fleet.Preprocess(newAppConfig.Integrations.NDESSCEPProxy.Value.URL) + appConfig.Integrations.NDESSCEPProxy.Value.AdminURL = fleet.Preprocess(newAppConfig.Integrations.NDESSCEPProxy.Value.AdminURL) + appConfig.Integrations.NDESSCEPProxy.Value.Username = fleet.Preprocess(newAppConfig.Integrations.NDESSCEPProxy.Value.Username) + // do not preprocess password + + validateAdminURL, validateSCEPURL := false, false + newSCEPProxy := appConfig.Integrations.NDESSCEPProxy.Value + if !oldAppConfig.Integrations.NDESSCEPProxy.Valid { + validateAdminURL, validateSCEPURL = true, true + } else { + oldSCEPProxy := oldAppConfig.Integrations.NDESSCEPProxy.Value + if newSCEPProxy.URL != oldSCEPProxy.URL { + validateSCEPURL = true + } + if newSCEPProxy.AdminURL != oldSCEPProxy.AdminURL || + newSCEPProxy.Username != oldSCEPProxy.Username || + (newSCEPProxy.Password != "" && newSCEPProxy.Password != fleet.MaskedPassword) { + validateAdminURL = true + } } - } - if validateAdminURL { - if err = validateNDESSCEPAdminURL(ctx, newSCEPProxy); err != nil { - invalid.Append("integrations.ndes_scep_proxy", err.Error()) + if validateAdminURL { + if err = validateNDESSCEPAdminURL(ctx, newSCEPProxy); err != nil { + invalid.Append("integrations.ndes_scep_proxy", err.Error()) + } } - } - if validateSCEPURL { - if err = validateNDESSCEPURL(ctx, newSCEPProxy, svc.logger); err != nil { - invalid.Append("integrations.ndes_scep_proxy.url", err.Error()) + if validateSCEPURL { + if err = validateNDESSCEPURL(ctx, newSCEPProxy, svc.logger); err != nil { + invalid.Append("integrations.ndes_scep_proxy.url", err.Error()) + } } } } diff --git a/server/service/appconfig_test.go b/server/service/appconfig_test.go index e03b3a289a3b..07db5acc5357 100644 --- a/server/service/appconfig_test.go +++ b/server/service/appconfig_test.go @@ -1450,7 +1450,7 @@ func TestModifyAppConfigForNDESSCEPProxy(t *testing.T) { ctx = viewer.NewContext(ctx, viewer.Viewer{User: admin}) // SCEP proxy not configured for free users - ac, err := svc.ModifyAppConfig(ctx, []byte(jsonPayload), fleet.ApplySpecOptions{}) + _, err := svc.ModifyAppConfig(ctx, []byte(jsonPayload), fleet.ApplySpecOptions{}) assert.ErrorContains(t, err, ErrMissingLicense.Error()) assert.ErrorContains(t, err, "integrations.ndes_scep_proxy") diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index bb9199baced0..205eacf38c57 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -11485,10 +11485,11 @@ func (s *integrationMDMTestSuite) TestSCEPProxy() { // Configure a bad SCEP URL appConf, err := s.ds.AppConfig(context.Background()) - require.NoError(s.T(), err) + require.NoError(t, err) appConf.Integrations.NDESSCEPProxy.Valid = true appConf.Integrations.NDESSCEPProxy.Value.URL = "https://httpstat.us/410" err = s.ds.SaveAppConfig(context.Background(), appConf) + require.NoError(t, err) res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier, nil, http.StatusInternalServerError, nil, "operation", "GetCACaps") errBody, err = io.ReadAll(res.Body) @@ -11545,7 +11546,7 @@ func (s *integrationMDMTestSuite) TestSCEPProxy() { appConf.Integrations.NDESSCEPProxy.Value.URL = scepServer.URL + "/scep" err = s.ds.SaveAppConfig(context.Background(), appConf) - require.NoError(s.T(), err) + require.NoError(t, err) // GetCACaps res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier, nil, http.StatusOK, nil, "operation", "GetCACaps") From 95a2cd627cc81d1eecac4c0dbaac117b558b8fd1 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Tue, 8 Oct 2024 12:10:20 -0500 Subject: [PATCH 21/27] Fix compile issue. --- changes/21955-ndes-scep-proxy | 1 + server/service/appconfig_test.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changes/21955-ndes-scep-proxy diff --git a/changes/21955-ndes-scep-proxy b/changes/21955-ndes-scep-proxy new file mode 100644 index 000000000000..0460f4c15655 --- /dev/null +++ b/changes/21955-ndes-scep-proxy @@ -0,0 +1 @@ +Added SCEP proxy for Windows NDES (Network Device Enrollment Service) AD CS server, which allows devices to request certificates. diff --git a/server/service/appconfig_test.go b/server/service/appconfig_test.go index 07db5acc5357..f514261e0707 100644 --- a/server/service/appconfig_test.go +++ b/server/service/appconfig_test.go @@ -1473,7 +1473,7 @@ func TestModifyAppConfigForNDESSCEPProxy(t *testing.T) { svc, ctx = newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}}) ctx = viewer.NewContext(ctx, viewer.Viewer{User: admin}) - ac, err = svc.ModifyAppConfig(ctx, []byte(jsonPayload), fleet.ApplySpecOptions{}) + ac, err := svc.ModifyAppConfig(ctx, []byte(jsonPayload), fleet.ApplySpecOptions{}) require.NoError(t, err) checkSCEPProxy := func() { require.NotNil(t, ac.Integrations.NDESSCEPProxy) From 57c1a98234fc7b2de25fa0997160fa10302d1af9 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Tue, 8 Oct 2024 14:54:10 -0500 Subject: [PATCH 22/27] Fix license check for NDES setting. --- server/service/appconfig.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/service/appconfig.go b/server/service/appconfig.go index 30e9e54849ef..70645872679e 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -336,7 +336,7 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle } // Validate NDES SCEP URLs if they changed. Validation is done in both dry run and normal mode. - if newAppConfig.Integrations.NDESSCEPProxy.Set && !license.IsPremium() { + if newAppConfig.Integrations.NDESSCEPProxy.Set && newAppConfig.Integrations.NDESSCEPProxy.Valid && !license.IsPremium() { invalid.Append("integrations.ndes_scep_proxy", ErrMissingLicense.Error()) appConfig.Integrations.NDESSCEPProxy.Valid = false } else { From 9fe4891d359bc0bf6f55c69b957d382c8ef41692 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Wed, 9 Oct 2024 10:57:46 -0500 Subject: [PATCH 23/27] Most (not all) changes from code review comments. --- ee/server/service/scep_proxy.go | 10 +-- server/mdm/scep/server/transport.go | 2 +- server/service/apple_mdm.go | 104 ++++++++++++++-------- server/service/apple_mdm_test.go | 129 +++++++++++++++++----------- 4 files changed, 151 insertions(+), 94 deletions(-) diff --git a/ee/server/service/scep_proxy.go b/ee/server/service/scep_proxy.go index 4dfbc4bc9ea3..53c47e419faf 100644 --- a/ee/server/service/scep_proxy.go +++ b/ee/server/service/scep_proxy.go @@ -52,8 +52,7 @@ func (svc *scepProxyService) GetCACaps(ctx context.Context) ([]byte, error) { } res, err := client.GetCACaps(ctx) if err != nil { - return res, ctxerr.Wrap(ctx, err, - fmt.Sprintf("Could not GetCACaps from SCEP server %s", appConfig.Integrations.NDESSCEPProxy.Value.URL)) + return res, ctxerr.Wrapf(ctx, err, "Could not GetCACaps from SCEP server %s", appConfig.Integrations.NDESSCEPProxy.Value.URL) } return res, nil } @@ -75,8 +74,7 @@ func (svc *scepProxyService) GetCACert(ctx context.Context, message string) ([]b } res, num, err := client.GetCACert(ctx, message) if err != nil { - return res, num, ctxerr.Wrap(ctx, err, - fmt.Sprintf("Could not GetCACert from SCEP server %s", appConfig.Integrations.NDESSCEPProxy.Value.URL)) + return res, num, ctxerr.Wrapf(ctx, err, "Could not GetCACert from SCEP server %s", appConfig.Integrations.NDESSCEPProxy.Value.URL) } return res, num, nil } @@ -117,8 +115,8 @@ func (svc *scepProxyService) PKIOperation(ctx context.Context, data []byte, iden } res, err := client.PKIOperation(ctx, data) if err != nil { - return res, ctxerr.Wrap(ctx, err, - fmt.Sprintf("Could not do PKIOperation on SCEP server %s", appConfig.Integrations.NDESSCEPProxy.Value.URL)) + return res, ctxerr.Wrapf(ctx, err, + "Could not do PKIOperation on SCEP server %s", appConfig.Integrations.NDESSCEPProxy.Value.URL) } return res, nil } diff --git a/server/mdm/scep/server/transport.go b/server/mdm/scep/server/transport.go index 798caf5690ea..d31e0a824c03 100644 --- a/server/mdm/scep/server/transport.go +++ b/server/mdm/scep/server/transport.go @@ -138,7 +138,7 @@ func decodeSCEPRequestWithIdentifier(_ context.Context, r *http.Request) (interf request := SCEPRequestWithIdentifier{ SCEPRequest: SCEPRequest{ Message: msg, - Operation: r.URL.Query().Get("operation"), + Operation: operation, }, Identifier: identifier, } diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index 0d693e5db97b..965e8f404e96 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -7,6 +7,7 @@ import ( "encoding/base64" "encoding/json" "encoding/pem" + "encoding/xml" "errors" "fmt" "io" @@ -66,10 +67,15 @@ var ( FleetVarNDESSCEPChallenge)) fleetVarNDESSCEPProxyURLRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%s})`, FleetVarNDESSCEPProxyURL, FleetVarNDESSCEPProxyURL)) - fleetVarHostEndUserEmailIDP = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%s})`, FleetVarHostEndUserEmailIDP, + fleetVarHostEndUserEmailIDPRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%s})`, FleetVarHostEndUserEmailIDP, FleetVarHostEndUserEmailIDP)) ) +type hostProfileUUID struct { + HostUUID string + ProfileUUID string +} + // Functions that can be overwritten in tests var getNDESSCEPChallenge = eeservice.GetNDESSCEPChallenge @@ -3276,6 +3282,9 @@ func ReconcileAppleProfiles( // command. hostProfilesToCleanup := []*fleet.MDMAppleProfilePayload{} + // Index host profiles to install by host and profile UUID, for easier bulk error processing + hostProfilesToInstallMap := make(map[hostProfileUUID]*fleet.MDMAppleBulkUpsertHostProfilePayload, len(toInstall)) + installTargets, removeTargets := make(map[string]*cmdTarget), make(map[string]*cmdTarget) for _, p := range toInstall { if pp, ok := profileIntersection.GetMatchingProfileInCurrentState(p); ok { @@ -3284,7 +3293,7 @@ func ReconcileAppleProfiles( // the same) we don't send another InstallProfile // command. if pp.Status != &fleet.MDMDeliveryFailed && bytes.Equal(pp.Checksum, p.Checksum) { - hostProfiles = append(hostProfiles, &fleet.MDMAppleBulkUpsertHostProfilePayload{ + hostProfile := &fleet.MDMAppleBulkUpsertHostProfilePayload{ ProfileUUID: p.ProfileUUID, HostUUID: p.HostUUID, ProfileIdentifier: p.ProfileIdentifier, @@ -3294,7 +3303,9 @@ func ReconcileAppleProfiles( Status: pp.Status, CommandUUID: pp.CommandUUID, Detail: pp.Detail, - }) + } + hostProfiles = append(hostProfiles, hostProfile) + hostProfilesToInstallMap[hostProfileUUID{HostUUID: p.HostUUID, ProfileUUID: p.ProfileUUID}] = hostProfile continue } } @@ -3310,7 +3321,7 @@ func ReconcileAppleProfiles( } target.hostUUIDs = append(target.hostUUIDs, p.HostUUID) - hostProfiles = append(hostProfiles, &fleet.MDMAppleBulkUpsertHostProfilePayload{ + hostProfile := &fleet.MDMAppleBulkUpsertHostProfilePayload{ ProfileUUID: p.ProfileUUID, HostUUID: p.HostUUID, OperationType: fleet.MDMOperationTypeInstall, @@ -3319,7 +3330,9 @@ func ReconcileAppleProfiles( ProfileIdentifier: p.ProfileIdentifier, ProfileName: p.ProfileName, Checksum: p.Checksum, - }) + } + hostProfiles = append(hostProfiles, hostProfile) + hostProfilesToInstallMap[hostProfileUUID{HostUUID: p.HostUUID, ProfileUUID: p.ProfileUUID}] = hostProfile } for _, p := range toRemove { @@ -3387,7 +3400,7 @@ func ReconcileAppleProfiles( } // Insert variables into profile contents - err = preprocessProfileContents(ctx, appConfig, ds, installTargets, profileContents) + err = preprocessProfileContents(ctx, appConfig, ds, installTargets, profileContents, hostProfilesToInstallMap) if err != nil { return err } @@ -3477,6 +3490,7 @@ func preprocessProfileContents( ds fleet.Datastore, targets map[string]*cmdTarget, profileContents map[string]mobileconfig.Mobileconfig, + hostProfilesToInstallMap map[hostProfileUUID]*fleet.MDMAppleBulkUpsertHostProfilePayload, ) error { // This method replaces Fleet variables ($FLEET_VAR_) in the profile contents, generating a unique profile for each host. @@ -3484,34 +3498,37 @@ func preprocessProfileContents( isNDESSCEPConfigured := func(profUUID string, target *cmdTarget) (bool, error) { if !license.IsPremium(ctx) { + profilesToUpdate := make([]*fleet.MDMAppleBulkUpsertHostProfilePayload, 0, len(target.hostUUIDs)) for _, hostUUID := range target.hostUUIDs { - err := ds.UpdateOrDeleteHostMDMAppleProfile(ctx, &fleet.HostMDMAppleProfile{ - CommandUUID: target.cmdUUID, - HostUUID: hostUUID, - Status: &fleet.MDMDeliveryFailed, - Detail: "NDES SCEP Proxy requires a Fleet Premium license.", - OperationType: fleet.MDMOperationTypeInstall, - }) - if err != nil { - return false, err + profile, ok := hostProfilesToInstallMap[hostProfileUUID{HostUUID: hostUUID, ProfileUUID: profUUID}] + if !ok { // Should never happen + continue } + profile.Status = &fleet.MDMDeliveryFailed + profile.Detail = "NDES SCEP Proxy requires a Fleet Premium license." + profilesToUpdate = append(profilesToUpdate, profile) + } + if err := ds.BulkUpsertMDMAppleHostProfiles(ctx, profilesToUpdate); err != nil { + return false, err } return false, nil } if !appConfig.Integrations.NDESSCEPProxy.Valid { + profilesToUpdate := make([]*fleet.MDMAppleBulkUpsertHostProfilePayload, 0, len(target.hostUUIDs)) for _, hostUUID := range target.hostUUIDs { - err := ds.UpdateOrDeleteHostMDMAppleProfile(ctx, &fleet.HostMDMAppleProfile{ - CommandUUID: target.cmdUUID, - HostUUID: hostUUID, - Status: &fleet.MDMDeliveryFailed, - Detail: "NDES SCEP Proxy is not configured. " + - "Please configure in Settings > Integrations > Mobile Device Management > Simple Certificate Enrollment Protocol.", - OperationType: fleet.MDMOperationTypeInstall, - }) - if err != nil { - return false, err + profile, ok := hostProfilesToInstallMap[hostProfileUUID{HostUUID: hostUUID, ProfileUUID: profUUID}] + if !ok { // Should never happen + continue } + profile.Status = &fleet.MDMDeliveryFailed + profile.Detail = "NDES SCEP Proxy is not configured. " + + "Please configure in Settings > Integrations > Mobile Device Management > Simple Certificate Enrollment Protocol." + profilesToUpdate = append(profilesToUpdate, profile) + } + if err := ds.BulkUpsertMDMAppleHostProfiles(ctx, profilesToUpdate); err != nil { + return false, err } + return false, nil } return appConfig.Integrations.NDESSCEPProxy.Valid, nil } @@ -3551,18 +3568,19 @@ func preprocessProfileContents( // No extra validation needed for this variable default: // Error out if we find an unknown variable + profilesToUpdate := make([]*fleet.MDMAppleBulkUpsertHostProfilePayload, 0, len(target.hostUUIDs)) for _, hostUUID := range target.hostUUIDs { - err := ds.UpdateOrDeleteHostMDMAppleProfile(ctx, &fleet.HostMDMAppleProfile{ - CommandUUID: target.cmdUUID, - HostUUID: hostUUID, - Status: &fleet.MDMDeliveryFailed, - Detail: fmt.Sprintf("Unknown Fleet variable $FLEET_VAR_%s found in profile. Please update or remove.", - fleetVar), - OperationType: fleet.MDMOperationTypeInstall, - }) - if err != nil { - return ctxerr.Wrap(ctx, err, "updating host MDM Apple profile for unknown variable") + profile, ok := hostProfilesToInstallMap[hostProfileUUID{HostUUID: hostUUID, ProfileUUID: profUUID}] + if !ok { // Should never happen + continue } + profile.Status = &fleet.MDMDeliveryFailed + profile.Detail = fmt.Sprintf("Unknown Fleet variable $FLEET_VAR_%s found in profile. Please update or remove.", + fleetVar) + profilesToUpdate = append(profilesToUpdate, profile) + } + if err := ds.BulkUpsertMDMAppleHostProfiles(ctx, profilesToUpdate); err != nil { + return ctxerr.Wrap(ctx, err, "updating host MDM Apple profiles for unknown variable") } valid = false break @@ -3631,12 +3649,13 @@ func preprocessProfileContents( failed = true break } - hostContents = fleetVarNDESSCEPChallengeRegexp.ReplaceAllString(hostContents, challenge) + + hostContents = replaceFleetVariable(fleetVarNDESSCEPChallengeRegexp, hostContents, challenge) case FleetVarNDESSCEPProxyURL: // Insert the SCEP URL into the profile contents proxyURL := fmt.Sprintf("%s%s%s", appConfig.ServerSettings.ServerURL, apple_mdm.SCEPProxyPath, url.QueryEscape(fmt.Sprintf("%s,%s", hostUUID, profUUID))) - hostContents = fleetVarNDESSCEPProxyURLRegexp.ReplaceAllString(hostContents, proxyURL) + hostContents = replaceFleetVariable(fleetVarNDESSCEPProxyURLRegexp, hostContents, proxyURL) case FleetVarHostEndUserEmailIDP: // Insert the end user email IDP into the profile contents emails, err := ds.GetHostEmails(ctx, hostUUID, fleet.DeviceMappingMDMIdpAccounts) @@ -3662,7 +3681,7 @@ func preprocessProfileContents( failed = true break } - hostContents = fleetVarHostEndUserEmailIDP.ReplaceAllString(hostContents, emails[0]) + hostContents = replaceFleetVariable(fleetVarHostEndUserEmailIDPRegexp, hostContents, emails[0]) default: // This was handled in the above switch statement, so we should never reach this case } @@ -3688,6 +3707,15 @@ func preprocessProfileContents( return nil } +func replaceFleetVariable(regExp *regexp.Regexp, contents string, replacement string) string { + // Escape XML characters + b := make([]byte, 0, len(replacement)) + buf := bytes.NewBuffer(b) + // error is always nil for Buffer.Write method, so we ignore it + _ = xml.EscapeText(buf, []byte(replacement)) + return regExp.ReplaceAllString(contents, buf.String()) +} + func findFleetVariables(contents string) map[string]interface{} { var result map[string]interface{} matches := profileVariableRegex.FindAllStringSubmatch(contents, -1) diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go index 6bcb63791fba..d23a4174f367 100644 --- a/server/service/apple_mdm_test.go +++ b/server/service/apple_mdm_test.go @@ -2675,9 +2675,22 @@ func TestMDMAppleReconcileAppleProfiles(t *testing.T) { t.Run("bad $FLEET_VAR", func(t *testing.T) { var failedCount int failedCall = false + var hostUUIDs []string failedCheck = func(payload []*fleet.MDMAppleBulkUpsertHostProfilePayload) { - failedCount++ - require.Len(t, payload, 0) + if len(payload) > 0 { + failedCount++ + } + for _, p := range payload { + assert.Equal(t, fleet.MDMDeliveryFailed, *p.Status) + assert.Contains(t, p.Detail, "FLEET_VAR_BOZO") + for i, hu := range hostUUIDs { + if hu == p.HostUUID { + // remove element + hostUUIDs = append(hostUUIDs[:i], hostUUIDs[i+1:]...) + break + } + } + } } enqueueFailForOp = "" @@ -2696,35 +2709,21 @@ func TestMDMAppleReconcileAppleProfiles(t *testing.T) { }) profilesToInstall, _ := ds.ListMDMAppleProfilesToInstallFunc(ctx) - hostUUIDs := make([]string, 0, len(profilesToInstall)) + hostUUIDs = make([]string, 0, len(profilesToInstall)) for _, p := range profilesToInstall { hostUUIDs = append(hostUUIDs, p.HostUUID) } - ds.UpdateOrDeleteHostMDMAppleProfileFunc = func(ctx context.Context, profile *fleet.HostMDMAppleProfile) error { - require.NotNil(t, profile) - require.NotNil(t, profile.Status) - assert.Equal(t, fleet.MDMDeliveryFailed, *profile.Status) - assert.Contains(t, profile.Detail, "FLEET_VAR_BOZO") - for i, hu := range hostUUIDs { - if hu == profile.HostUUID { - // remove element - hostUUIDs = append(hostUUIDs[:i], hostUUIDs[i+1:]...) - break - } - } - return nil - } - err := ReconcileAppleProfiles(ctx, ds, cmdr, kitlog.NewNopLogger()) require.NoError(t, err) assert.Empty(t, hostUUIDs, "all host+profile combinations should be updated") - require.Equal(t, 1, failedCount) + require.Equal(t, 3, failedCount, "number of profiles with bad content") checkAndReset(t, true, &ds.ListMDMAppleProfilesToInstallFuncInvoked) checkAndReset(t, true, &ds.ListMDMAppleProfilesToRemoveFuncInvoked) checkAndReset(t, true, &ds.GetMDMAppleProfilesContentsFuncInvoked) checkAndReset(t, true, &ds.BulkUpsertMDMAppleHostProfilesFuncInvoked) - checkAndReset(t, true, &ds.UpdateOrDeleteHostMDMAppleProfileFuncInvoked) + // Check that individual updates were not done (bulk update should be done) + checkAndReset(t, false, &ds.UpdateOrDeleteHostMDMAppleProfileFuncInvoked) }) } @@ -2742,7 +2741,7 @@ func TestPreprocessProfileContents(t *testing.T) { ds := new(mock.Store) // No-op - err := preprocessProfileContents(ctx, appCfg, ds, nil, nil) + err := preprocessProfileContents(ctx, appCfg, ds, nil, nil, nil) require.NoError(t, err) hostUUID := "host-1" @@ -2753,39 +2752,50 @@ func TestPreprocessProfileContents(t *testing.T) { "p1": {cmdUUID: cmdUUID, profIdent: "com.add.profile", hostUUIDs: []string{hostUUID}}, } } + hostProfilesToInstallMap := make(map[hostProfileUUID]*fleet.MDMAppleBulkUpsertHostProfilePayload, 1) + hostProfilesToInstallMap[hostProfileUUID{HostUUID: hostUUID, ProfileUUID: "p1"}] = &fleet.MDMAppleBulkUpsertHostProfilePayload{ + ProfileUUID: "p1", + ProfileIdentifier: "com.add.profile", + HostUUID: hostUUID, + OperationType: fleet.MDMOperationTypeInstall, + Status: &fleet.MDMDeliveryPending, + CommandUUID: cmdUUID, + } populateTargets() profileContents := map[string]mobileconfig.Mobileconfig{ "p1": []byte("$FLEET_VAR_" + FleetVarNDESSCEPProxyURL), } - var updatedProfile *fleet.HostMDMAppleProfile - ds.UpdateOrDeleteHostMDMAppleProfileFunc = func(ctx context.Context, profile *fleet.HostMDMAppleProfile) error { - updatedProfile = profile - require.NotNil(t, updatedProfile.Status) - assert.Equal(t, fleet.MDMDeliveryFailed, *updatedProfile.Status) - assert.Equal(t, cmdUUID, updatedProfile.CommandUUID) - assert.Equal(t, hostUUID, updatedProfile.HostUUID) - assert.Equal(t, fleet.MDMOperationTypeInstall, updatedProfile.OperationType) + var updatedPayload *fleet.MDMAppleBulkUpsertHostProfilePayload + ds.BulkUpsertMDMAppleHostProfilesFunc = func(ctx context.Context, payload []*fleet.MDMAppleBulkUpsertHostProfilePayload) error { + require.Len(t, payload, 1) + updatedPayload = payload[0] + for _, p := range payload { + require.NotNil(t, p.Status) + assert.Equal(t, fleet.MDMDeliveryFailed, *p.Status) + assert.Equal(t, cmdUUID, p.CommandUUID) + assert.Equal(t, hostUUID, p.HostUUID) + assert.Equal(t, fleet.MDMOperationTypeInstall, p.OperationType) + } return nil } - // Can't use NDES SCEP proxy with free tier ctx = license.NewContext(ctx, &fleet.LicenseInfo{Tier: fleet.TierFree}) - err = preprocessProfileContents(ctx, appCfg, ds, targets, profileContents) + err = preprocessProfileContents(ctx, appCfg, ds, targets, profileContents, hostProfilesToInstallMap) require.NoError(t, err) - require.NotNil(t, updatedProfile) - assert.Contains(t, updatedProfile.Detail, "Premium license") + require.NotNil(t, updatedPayload) + assert.Contains(t, updatedPayload.Detail, "Premium license") assert.Empty(t, targets) // Can't use NDES SCEP proxy without it being configured ctx = license.NewContext(ctx, &fleet.LicenseInfo{Tier: fleet.TierPremium}) appCfg.Integrations.NDESSCEPProxy.Valid = false - updatedProfile = nil + updatedPayload = nil populateTargets() - err = preprocessProfileContents(ctx, appCfg, ds, targets, profileContents) + err = preprocessProfileContents(ctx, appCfg, ds, targets, profileContents, hostProfilesToInstallMap) require.NoError(t, err) - require.NotNil(t, updatedProfile) - assert.Contains(t, updatedProfile.Detail, "not configured") + require.NotNil(t, updatedPayload) + assert.Contains(t, updatedPayload.Detail, "not configured") assert.Empty(t, targets) // Unknown variable @@ -2793,12 +2803,12 @@ func TestPreprocessProfileContents(t *testing.T) { "p1": []byte("$FLEET_VAR_BOZO"), } appCfg.Integrations.NDESSCEPProxy.Valid = true - updatedProfile = nil + updatedPayload = nil populateTargets() - err = preprocessProfileContents(ctx, appCfg, ds, targets, profileContents) + err = preprocessProfileContents(ctx, appCfg, ds, targets, profileContents, hostProfilesToInstallMap) require.NoError(t, err) - require.NotNil(t, updatedProfile) - assert.Contains(t, updatedProfile.Detail, "FLEET_VAR_BOZO") + require.NotNil(t, updatedPayload) + assert.Contains(t, updatedPayload.Detail, "FLEET_VAR_BOZO") assert.Empty(t, targets) ndesPassword := "test-password" @@ -2809,6 +2819,18 @@ func TestPreprocessProfileContents(t *testing.T) { }, nil } + ds.BulkUpsertMDMAppleHostProfilesFunc = nil + var updatedProfile *fleet.HostMDMAppleProfile + ds.UpdateOrDeleteHostMDMAppleProfileFunc = func(ctx context.Context, profile *fleet.HostMDMAppleProfile) error { + updatedProfile = profile + require.NotNil(t, updatedProfile.Status) + assert.Equal(t, fleet.MDMDeliveryFailed, *updatedProfile.Status) + assert.Equal(t, cmdUUID, updatedProfile.CommandUUID) + assert.Equal(t, hostUUID, updatedProfile.HostUUID) + assert.Equal(t, fleet.MDMOperationTypeInstall, updatedProfile.OperationType) + return nil + } + // Could not get NDES SCEP challenge profileContents = map[string]mobileconfig.Mobileconfig{ "p1": []byte("$FLEET_VAR_" + FleetVarNDESSCEPChallenge), @@ -2819,7 +2841,7 @@ func TestPreprocessProfileContents(t *testing.T) { } updatedProfile = nil populateTargets() - err = preprocessProfileContents(ctx, appCfg, ds, targets, profileContents) + err = preprocessProfileContents(ctx, appCfg, ds, targets, profileContents, hostProfilesToInstallMap) require.NoError(t, err) require.NotNil(t, updatedProfile) assert.Contains(t, updatedProfile.Detail, "FLEET_VAR_"+FleetVarNDESSCEPChallenge) @@ -2833,7 +2855,7 @@ func TestPreprocessProfileContents(t *testing.T) { } updatedProfile = nil populateTargets() - err = preprocessProfileContents(ctx, appCfg, ds, targets, profileContents) + err = preprocessProfileContents(ctx, appCfg, ds, targets, profileContents, hostProfilesToInstallMap) require.NoError(t, err) require.NotNil(t, updatedProfile) assert.Contains(t, updatedProfile.Detail, "FLEET_VAR_"+FleetVarNDESSCEPChallenge) @@ -2847,7 +2869,7 @@ func TestPreprocessProfileContents(t *testing.T) { } updatedProfile = nil populateTargets() - err = preprocessProfileContents(ctx, appCfg, ds, targets, profileContents) + err = preprocessProfileContents(ctx, appCfg, ds, targets, profileContents, hostProfilesToInstallMap) require.NoError(t, err) require.NotNil(t, updatedProfile) assert.Contains(t, updatedProfile.Detail, "FLEET_VAR_"+FleetVarNDESSCEPChallenge) @@ -2863,7 +2885,7 @@ func TestPreprocessProfileContents(t *testing.T) { } updatedProfile = nil populateTargets() - err = preprocessProfileContents(ctx, appCfg, ds, targets, profileContents) + err = preprocessProfileContents(ctx, appCfg, ds, targets, profileContents, hostProfilesToInstallMap) require.NoError(t, err) assert.Nil(t, updatedProfile) require.NotEmpty(t, targets) @@ -2882,7 +2904,7 @@ func TestPreprocessProfileContents(t *testing.T) { expectedURL := "https://test.example.com" + apple_mdm.SCEPProxyPath + url.QueryEscape(fmt.Sprintf("%s,%s", hostUUID, "p1")) updatedProfile = nil populateTargets() - err = preprocessProfileContents(ctx, appCfg, ds, targets, profileContents) + err = preprocessProfileContents(ctx, appCfg, ds, targets, profileContents, hostProfilesToInstallMap) require.NoError(t, err) assert.Nil(t, updatedProfile) require.NotEmpty(t, targets) @@ -2903,7 +2925,7 @@ func TestPreprocessProfileContents(t *testing.T) { } updatedProfile = nil populateTargets() - err = preprocessProfileContents(ctx, appCfg, ds, targets, profileContents) + err = preprocessProfileContents(ctx, appCfg, ds, targets, profileContents, hostProfilesToInstallMap) require.NoError(t, err) require.NotNil(t, updatedProfile) assert.Contains(t, updatedProfile.Detail, "FLEET_VAR_"+FleetVarHostEndUserEmailIDP) @@ -2917,7 +2939,7 @@ func TestPreprocessProfileContents(t *testing.T) { } updatedProfile = nil populateTargets() - err = preprocessProfileContents(ctx, appCfg, ds, targets, profileContents) + err = preprocessProfileContents(ctx, appCfg, ds, targets, profileContents, hostProfilesToInstallMap) require.NoError(t, err) assert.Nil(t, updatedProfile) require.NotEmpty(t, targets) @@ -2954,7 +2976,16 @@ func TestPreprocessProfileContents(t *testing.T) { assert.Equal(t, fleet.MDMOperationTypeInstall, updatedProfile.OperationType) return nil } - err = preprocessProfileContents(ctx, appCfg, ds, targets, profileContents) + ds.BulkUpsertMDMAppleHostProfilesFunc = func(ctx context.Context, payload []*fleet.MDMAppleBulkUpsertHostProfilePayload) error { + for _, p := range payload { + require.NotNil(t, p.Status) + assert.Equal(t, fleet.MDMDeliveryFailed, *p.Status) + assert.Equal(t, cmdUUID, p.CommandUUID) + assert.Equal(t, fleet.MDMOperationTypeInstall, p.OperationType) + } + return nil + } + err = preprocessProfileContents(ctx, appCfg, ds, targets, profileContents, nil) require.NoError(t, err) require.NotEmpty(t, targets) assert.Len(t, targets, 3) From 83f7f5005834558bff8bbd34930005383a17c376 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Wed, 9 Oct 2024 12:46:19 -0500 Subject: [PATCH 24/27] Using common transaction to update config and NDES password. --- cmd/fleet/serve.go | 8 ++-- cmd/fleetctl/testing_utils.go | 4 +- ee/server/service/mdm.go | 4 +- server/datastore/cached_mysql/cached_mysql.go | 6 ++- server/datastore/mysql/app_configs.go | 46 +++++++++---------- server/datastore/mysql/app_configs_test.go | 2 +- server/datastore/mysql/apple_mdm.go | 30 ++++++++---- server/datastore/mysql/apple_mdm_test.go | 16 +++---- server/datastore/mysql/nanomdm_storage.go | 5 +- server/datastore/mysql/testing_utils.go | 10 ++-- server/fleet/datastore.go | 16 +++++-- server/mdm/assets/assets.go | 6 +-- server/mock/datastore_mock.go | 19 ++++---- server/service/apple_mdm.go | 16 +++---- server/service/handler.go | 2 +- server/service/mdm.go | 8 ++-- tools/mdm/assets/main.go | 5 +- 17 files changed, 113 insertions(+), 90 deletions(-) diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index 4f1b43a5f76c..6afd5bd86303 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -501,7 +501,7 @@ the way that the Fleet server works. } checkMDMAssets := func(names []fleet.MDMAssetName) (bool, error) { - _, err = ds.GetAllMDMConfigAssetsByName(context.Background(), names) + _, err = ds.GetAllMDMConfigAssetsByName(context.Background(), names, nil) if err != nil { if fleet.IsNotFound(err) || errors.Is(err, mysql.ErrPartialResult) { return false, nil @@ -576,7 +576,7 @@ the way that the Fleet server works. } } - if err := ds.InsertMDMConfigAssets(context.Background(), args); err != nil { + if err := ds.InsertMDMConfigAssets(context.Background(), args, nil); err != nil { if mysql.IsDuplicate(err) { // we already checked for existing assets so we should never have a duplicate key error here; we'll add a debug log just in case level.Debug(logger).Log("msg", "unexpected duplicate key error inserting MDM APNs and SCEP assets") @@ -611,7 +611,7 @@ the way that the Fleet server works. } if len(toInsert) > 0 { - err := ds.InsertMDMConfigAssets(context.Background(), toInsert) + err := ds.InsertMDMConfigAssets(context.Background(), toInsert, nil) switch { case err != nil && mysql.IsDuplicate(err): // we already checked for existing assets so we should never have a duplicate key error here; we'll add a debug log just in case @@ -1082,7 +1082,7 @@ the way that the Fleet server works. err = ds.InsertMDMConfigAssets(context.Background(), []fleet.MDMConfigAsset{ {Name: fleet.MDMAssetSCEPChallenge, Value: []byte(scepChallenge)}, - }) + }, nil) if err != nil { // duplicate key errors mean that we already // have a value for those keys in the diff --git a/cmd/fleetctl/testing_utils.go b/cmd/fleetctl/testing_utils.go index 918a12b94e27..4229dea300b8 100644 --- a/cmd/fleetctl/testing_utils.go +++ b/cmd/fleetctl/testing_utils.go @@ -19,6 +19,7 @@ import ( "github.com/fleetdm/fleet/v4/server/mock" "github.com/fleetdm/fleet/v4/server/service" "github.com/fleetdm/fleet/v4/server/test" + "github.com/jmoiron/sqlx" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -136,7 +137,8 @@ func runServerWithMockedDS(t *testing.T, opts ...*service.TestServerOpts) (*http fleet.MDMAssetCAKey: "scepkey", }, nil } - ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, + _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { return map[fleet.MDMAssetName]fleet.MDMConfigAsset{ fleet.MDMAssetABMCert: {Name: fleet.MDMAssetABMCert, Value: certPEM}, fleet.MDMAssetABMKey: {Name: fleet.MDMAssetABMKey, Value: keyPEM}, diff --git a/ee/server/service/mdm.go b/ee/server/service/mdm.go index f6db29af5c9b..8e22df5b1b05 100644 --- a/ee/server/service/mdm.go +++ b/ee/server/service/mdm.go @@ -1163,7 +1163,7 @@ func (svc *Service) GetMDMManualEnrollmentProfile(ctx context.Context) ([]byte, assets, err := svc.ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{ fleet.MDMAssetSCEPChallenge, - }) + }, nil) if err != nil { return nil, fmt.Errorf("loading SCEP challenge from the database: %w", err) } @@ -1368,7 +1368,7 @@ func (svc *Service) decryptUploadedABMToken(ctx context.Context, token io.Reader pair, err := svc.ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{ fleet.MDMAssetABMCert, fleet.MDMAssetABMKey, - }) + }, nil) if err != nil { if fleet.IsNotFound(err) { return nil, nil, ctxerr.Wrap(ctx, &fleet.BadRequestError{ diff --git a/server/datastore/cached_mysql/cached_mysql.go b/server/datastore/cached_mysql/cached_mysql.go index 392199fab325..1292ed0dfde5 100644 --- a/server/datastore/cached_mysql/cached_mysql.go +++ b/server/datastore/cached_mysql/cached_mysql.go @@ -8,6 +8,7 @@ import ( "github.com/fleetdm/fleet/v4/server/contexts/ctxdb" "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/jmoiron/sqlx" "github.com/patrickmn/go-cache" ) @@ -403,7 +404,8 @@ func (ds *cachedMysql) ResultCountForQuery(ctx context.Context, queryID uint) (i return count, nil } -func (ds *cachedMysql) GetAllMDMConfigAssetsByName(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { +func (ds *cachedMysql) GetAllMDMConfigAssetsByName(ctx context.Context, assetNames []fleet.MDMAssetName, + queryerContext sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { // always reach the database to get the latest hashes latestHashes, err := ds.Datastore.GetAllMDMConfigAssetsHashes(ctx, assetNames) if err != nil { @@ -434,7 +436,7 @@ func (ds *cachedMysql) GetAllMDMConfigAssetsByName(ctx context.Context, assetNam } // fetch missing assets from the database - assetMap, err := ds.Datastore.GetAllMDMConfigAssetsByName(ctx, missingAssets) + assetMap, err := ds.Datastore.GetAllMDMConfigAssetsByName(ctx, missingAssets, queryerContext) if err != nil { return nil, err } diff --git a/server/datastore/mysql/app_configs.go b/server/datastore/mysql/app_configs.go index 7221ed0467ec..298d81738c5f 100644 --- a/server/datastore/mysql/app_configs.go +++ b/server/datastore/mysql/app_configs.go @@ -50,29 +50,29 @@ func appConfigDB(ctx context.Context, q sqlx.QueryerContext) (*fleet.AppConfig, } func (ds *Datastore) SaveAppConfig(ctx context.Context, info *fleet.AppConfig) error { - // Check if passwords need to be encrypted - if info.Integrations.NDESSCEPProxy.Valid { - if info.Integrations.NDESSCEPProxy.Set && - info.Integrations.NDESSCEPProxy.Value.Password != "" && - info.Integrations.NDESSCEPProxy.Value.Password != fleet.MaskedPassword { - err := ds.insertOrReplaceConfigAsset(ctx, fleet.MDMConfigAsset{ - Name: fleet.MDMAssetNDESPassword, - Value: []byte(info.Integrations.NDESSCEPProxy.Value.Password), - }) - if err != nil { - return ctxerr.Wrap(ctx, err, "processing NDES SCEP proxy password") + return ds.withTx(ctx, func(tx sqlx.ExtContext) error { + // Check if passwords need to be encrypted + if info.Integrations.NDESSCEPProxy.Valid { + if info.Integrations.NDESSCEPProxy.Set && + info.Integrations.NDESSCEPProxy.Value.Password != "" && + info.Integrations.NDESSCEPProxy.Value.Password != fleet.MaskedPassword { + err := ds.insertOrReplaceConfigAsset(ctx, tx, fleet.MDMConfigAsset{ + Name: fleet.MDMAssetNDESPassword, + Value: []byte(info.Integrations.NDESSCEPProxy.Value.Password), + }) + if err != nil { + return ctxerr.Wrap(ctx, err, "processing NDES SCEP proxy password") + } } + info.Integrations.NDESSCEPProxy.Value.Password = fleet.MaskedPassword } - info.Integrations.NDESSCEPProxy.Value.Password = fleet.MaskedPassword - } - configBytes, err := json.Marshal(info) - if err != nil { - return ctxerr.Wrap(ctx, err, "marshaling config") - } + configBytes, err := json.Marshal(info) + if err != nil { + return ctxerr.Wrap(ctx, err, "marshaling config") + } - return ds.withTx(ctx, func(tx sqlx.ExtContext) error { - _, err := tx.ExecContext(ctx, + _, err = tx.ExecContext(ctx, `INSERT INTO app_config_json(json_value) VALUES(?) ON DUPLICATE KEY UPDATE json_value = VALUES(json_value)`, configBytes, ) @@ -84,11 +84,11 @@ func (ds *Datastore) SaveAppConfig(ctx context.Context, info *fleet.AppConfig) e }) } -func (ds *Datastore) insertOrReplaceConfigAsset(ctx context.Context, asset fleet.MDMConfigAsset) error { - assets, err := ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{asset.Name}) +func (ds *Datastore) insertOrReplaceConfigAsset(ctx context.Context, tx sqlx.ExtContext, asset fleet.MDMConfigAsset) error { + assets, err := ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{asset.Name}, tx) if err != nil { if fleet.IsNotFound(err) { - return ds.InsertMDMConfigAssets(ctx, []fleet.MDMConfigAsset{asset}) + return ds.InsertMDMConfigAssets(ctx, []fleet.MDMConfigAsset{asset}, tx) } return ctxerr.Wrap(ctx, err, "get all mdm config assets by name") } @@ -102,7 +102,7 @@ func (ds *Datastore) insertOrReplaceConfigAsset(ctx context.Context, asset fleet return ctxerr.New(ctx, fmt.Sprintf("asset not found for name %s", asset.Name)) } if !bytes.Equal(currentAsset.Value, asset.Value) { - return ds.ReplaceMDMConfigAssets(ctx, []fleet.MDMConfigAsset{asset}) + return ds.ReplaceMDMConfigAssets(ctx, []fleet.MDMConfigAsset{asset}, tx) } // asset already exists and is the same, so not need to update return nil diff --git a/server/datastore/mysql/app_configs_test.go b/server/datastore/mysql/app_configs_test.go index d7daddd5ad40..0e8afb39ded7 100644 --- a/server/datastore/mysql/app_configs_test.go +++ b/server/datastore/mysql/app_configs_test.go @@ -576,7 +576,7 @@ func testNDESSCEPProxyPassword(t *testing.T, ds *Datastore) { checkProxyConfig() checkPassword := func() { - assets, err := ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetNDESPassword}) + assets, err := ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetNDESPassword}, nil) require.NoError(t, err) require.Len(t, assets, 1) assert.Equal(t, password, string(assets[fleet.MDMAssetNDESPassword].Value)) diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go index c6081fec95a7..1bf9259ea7b3 100644 --- a/server/datastore/mysql/apple_mdm.go +++ b/server/datastore/mysql/apple_mdm.go @@ -4754,17 +4754,21 @@ func decrypt(encrypted []byte, privateKey string) ([]byte, error) { return decrypted, nil } -func (ds *Datastore) InsertMDMConfigAssets(ctx context.Context, assets []fleet.MDMConfigAsset) error { - return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error { +func (ds *Datastore) InsertMDMConfigAssets(ctx context.Context, assets []fleet.MDMConfigAsset, tx sqlx.ExtContext) error { + insertFunc := func(tx sqlx.ExtContext) error { if err := insertMDMConfigAssets(ctx, tx, assets, ds.serverPrivateKey); err != nil { return ctxerr.Wrap(ctx, err, "insert mdm config assets") } - return nil - }) + } + if tx != nil { + return insertFunc(tx) + } + return ds.withRetryTxx(ctx, insertFunc) } -func (ds *Datastore) GetAllMDMConfigAssetsByName(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { +func (ds *Datastore) GetAllMDMConfigAssetsByName(ctx context.Context, assetNames []fleet.MDMAssetName, + queryerContext sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { if len(assetNames) == 0 { return nil, nil } @@ -4785,7 +4789,10 @@ WHERE } var res []fleet.MDMConfigAsset - if err := sqlx.SelectContext(ctx, ds.reader(ctx), &res, stmt, args...); err != nil { + if queryerContext == nil { + queryerContext = ds.reader(ctx) + } + if err := sqlx.SelectContext(ctx, queryerContext, &res, stmt, args...); err != nil { return nil, ctxerr.Wrap(ctx, err, "get mdm config assets by name") } @@ -4915,8 +4922,8 @@ VALUES return ctxerr.Wrap(ctx, err, "writing mdm config assets to db") } -func (ds *Datastore) ReplaceMDMConfigAssets(ctx context.Context, assets []fleet.MDMConfigAsset) error { - return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error { +func (ds *Datastore) ReplaceMDMConfigAssets(ctx context.Context, assets []fleet.MDMConfigAsset, tx sqlx.ExtContext) error { + replaceFunc := func(tx sqlx.ExtContext) error { var names []fleet.MDMAssetName for _, a := range assets { names = append(names, a.Name) @@ -4929,9 +4936,12 @@ func (ds *Datastore) ReplaceMDMConfigAssets(ctx context.Context, assets []fleet. if err := insertMDMConfigAssets(ctx, tx, assets, ds.serverPrivateKey); err != nil { return ctxerr.Wrap(ctx, err, "upsert mdm config assets insert") } - return nil - }) + } + if tx != nil { + return replaceFunc(tx) + } + return ds.withRetryTxx(ctx, replaceFunc) } // ListIOSAndIPadOSToRefetch returns the UUIDs of iPhones/iPads that should be refetched diff --git a/server/datastore/mysql/apple_mdm_test.go b/server/datastore/mysql/apple_mdm_test.go index cfc9e50756cd..3261b0b82ae1 100644 --- a/server/datastore/mysql/apple_mdm_test.go +++ b/server/datastore/mysql/apple_mdm_test.go @@ -5694,10 +5694,10 @@ func testMDMConfigAsset(t *testing.T, ds *Datastore) { for _, a := range assets { wantAssets[a.Name] = a } - err := ds.InsertMDMConfigAssets(ctx, assets) + err := ds.InsertMDMConfigAssets(ctx, assets, nil) require.NoError(t, err) - a, err := ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetCAKey}) + a, err := ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetCAKey}, nil) require.NoError(t, err) require.Equal(t, wantAssets, a) @@ -5709,7 +5709,7 @@ func testMDMConfigAsset(t *testing.T, ds *Datastore) { // try to fetch an asset that doesn't exist var nfe fleet.NotFoundError - a, err = ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetABMCert}) + a, err = ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetABMCert}, ds.writer(ctx)) require.ErrorAs(t, err, &nfe) require.Nil(t, a) @@ -5718,7 +5718,7 @@ func testMDMConfigAsset(t *testing.T, ds *Datastore) { require.Nil(t, h) // try to fetch a mix of assets that exist and doesn't exist - a, err = ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetABMCert}) + a, err = ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetABMCert}, nil) require.ErrorIs(t, err, ErrPartialResult) require.Len(t, a, 1) @@ -5745,10 +5745,10 @@ func testMDMConfigAsset(t *testing.T, ds *Datastore) { wantNewAssets[a.Name] = a } - err = ds.ReplaceMDMConfigAssets(ctx, newAssets) + err = ds.ReplaceMDMConfigAssets(ctx, newAssets, nil) require.NoError(t, err) - a, err = ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetCAKey}) + a, err = ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetCAKey}, ds.reader(ctx)) require.NoError(t, err) require.Equal(t, wantNewAssets, a) @@ -5763,7 +5763,7 @@ func testMDMConfigAsset(t *testing.T, ds *Datastore) { err = ds.DeleteMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetCAKey}) require.NoError(t, err) - a, err = ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetCAKey}) + a, err = ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetCAKey}, nil) require.ErrorAs(t, err, &nfe) require.Nil(t, a) @@ -5806,7 +5806,7 @@ func testMDMConfigAsset(t *testing.T, ds *Datastore) { // Hard delete err = ds.HardDeleteMDMConfigAsset(ctx, fleet.MDMAssetCACert) require.NoError(t, err) - a, err = ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetCAKey}) + a, err = ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetCAKey}, nil) require.ErrorAs(t, err, &nfe) require.Nil(t, a) diff --git a/server/datastore/mysql/nanomdm_storage.go b/server/datastore/mysql/nanomdm_storage.go index e032cab539ed..ed5f99352c3d 100644 --- a/server/datastore/mysql/nanomdm_storage.go +++ b/server/datastore/mysql/nanomdm_storage.go @@ -132,8 +132,9 @@ func (s *NanoMDMStorage) EnqueueDeviceWipeCommand(ctx context.Context, host *fle }, s.logger) } -func (s *NanoMDMStorage) GetAllMDMConfigAssetsByName(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { - return s.ds.GetAllMDMConfigAssetsByName(ctx, assetNames) +func (s *NanoMDMStorage) GetAllMDMConfigAssetsByName(ctx context.Context, assetNames []fleet.MDMAssetName, + queryerContext sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + return s.ds.GetAllMDMConfigAssetsByName(ctx, assetNames, queryerContext) } func (s *NanoMDMStorage) GetABMTokenByOrgName(ctx context.Context, orgName string) (*fleet.ABMToken, error) { diff --git a/server/datastore/mysql/testing_utils.go b/server/datastore/mysql/testing_utils.go index f3940eda31dc..051d508d19d2 100644 --- a/server/datastore/mysql/testing_utils.go +++ b/server/datastore/mysql/testing_utils.go @@ -706,7 +706,7 @@ func CreateABMKeyCertIfNotExists(t testing.TB, ds *Datastore) { var assets []fleet.MDMConfigAsset _, err = ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{ fleet.MDMAssetABMKey, - }) + }, nil) if err != nil { var nfe fleet.NotFoundError require.ErrorAs(t, err, &nfe) @@ -715,7 +715,7 @@ func CreateABMKeyCertIfNotExists(t testing.TB, ds *Datastore) { _, err = ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{ fleet.MDMAssetABMCert, - }) + }, nil) if err != nil { var nfe fleet.NotFoundError require.ErrorAs(t, err, &nfe) @@ -723,7 +723,7 @@ func CreateABMKeyCertIfNotExists(t testing.TB, ds *Datastore) { } if len(assets) != 0 { - err = ds.InsertMDMConfigAssets(context.Background(), assets) + err = ds.InsertMDMConfigAssets(context.Background(), assets, ds.writer(context.Background())) require.NoError(t, err) } } @@ -733,7 +733,7 @@ func CreateAndSetABMToken(t testing.TB, ds *Datastore, orgName string) *fleet.AB assets, err := ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{ fleet.MDMAssetABMKey, fleet.MDMAssetABMCert, - }) + }, nil) require.NoError(t, err) certPEM := assets[fleet.MDMAssetABMCert].Value @@ -791,7 +791,7 @@ func SetTestABMAssets(t testing.TB, ds *Datastore, orgName string) *fleet.ABMTok {Name: fleet.MDMAssetCAKey, Value: keyPEM}, } - err = ds.InsertMDMConfigAssets(context.Background(), assets) + err = ds.InsertMDMConfigAssets(context.Background(), assets, nil) require.NoError(t, err) tok, err := ds.InsertABMToken(context.Background(), &fleet.ABMToken{EncryptedToken: tokenBytes, OrganizationName: orgName}) diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 96fe5d082a06..3a21cae96c20 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -15,6 +15,7 @@ import ( "github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig" "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm" "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/storage" + "github.com/jmoiron/sqlx" "github.com/fleetdm/fleet/v4/server/mdm/nanodep/godep" ) @@ -1340,12 +1341,15 @@ type Datastore interface { GetMDMAppleOSUpdatesSettingsByHostSerial(ctx context.Context, hostSerial string) (*AppleOSUpdateSettings, error) // InsertMDMConfigAssets inserts MDM related config assets, such as SCEP and APNS certs and keys. - InsertMDMConfigAssets(ctx context.Context, assets []MDMConfigAsset) error + // tx is optional and can be used to pass an existing transaction. + InsertMDMConfigAssets(ctx context.Context, assets []MDMConfigAsset, tx sqlx.ExtContext) error // GetAllMDMConfigAssetsByName returns the requested config assets. // - // If it doesn't find all the assets requested, it returns a `mysql.ErrPartialResult` - GetAllMDMConfigAssetsByName(ctx context.Context, assetNames []MDMAssetName) (map[MDMAssetName]MDMConfigAsset, error) + // If it doesn't find all the assets requested, it returns a `mysql.ErrPartialResult` error. + // The queryerContext is optional and can be used to pass a transaction. + GetAllMDMConfigAssetsByName(ctx context.Context, assetNames []MDMAssetName, + queryerContext sqlx.QueryerContext) (map[MDMAssetName]MDMConfigAsset, error) // GetAllMDMConfigAssetsHashes behaves like // GetAllMDMConfigAssetsByName, but only returns a sha256 checksum of @@ -1363,7 +1367,8 @@ type Datastore interface { // ReplaceMDMConfigAssets replaces (soft delete if they exist + insert) `MDMConfigAsset`s in a // single transaction. Useful for "renew" flows where users are updating the assets with newly // generated ones. - ReplaceMDMConfigAssets(ctx context.Context, assets []MDMConfigAsset) error + // tx parameter is optional and can be used to pass an existing transaction. + ReplaceMDMConfigAssets(ctx context.Context, assets []MDMConfigAsset, tx sqlx.ExtContext) error // GetABMTokenByOrgName retrieves the Apple Business Manager token identified by // its unique name (the organization name). @@ -1747,7 +1752,8 @@ type MDMAppleStore interface { } type MDMAssetRetriever interface { - GetAllMDMConfigAssetsByName(ctx context.Context, assetNames []MDMAssetName) (map[MDMAssetName]MDMConfigAsset, error) + GetAllMDMConfigAssetsByName(ctx context.Context, assetNames []MDMAssetName, + queryerContext sqlx.QueryerContext) (map[MDMAssetName]MDMConfigAsset, error) GetABMTokenByOrgName(ctx context.Context, orgName string) (*ABMToken, error) } diff --git a/server/mdm/assets/assets.go b/server/mdm/assets/assets.go index af303b65ce47..218b3132fc61 100644 --- a/server/mdm/assets/assets.go +++ b/server/mdm/assets/assets.go @@ -26,7 +26,7 @@ func KeyPair(ctx context.Context, ds fleet.MDMAssetRetriever, certName, keyName assets, err := ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{ certName, keyName, - }) + }, nil) if err != nil { return nil, fmt.Errorf("loading %s, %s keypair from the database: %w", certName, keyName, err) } @@ -45,7 +45,7 @@ func KeyPair(ctx context.Context, ds fleet.MDMAssetRetriever, certName, keyName } func X509Cert(ctx context.Context, ds fleet.MDMAssetRetriever, certName fleet.MDMAssetName) (*x509.Certificate, error) { - assets, err := ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{certName}) + assets, err := ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{certName}, nil) if err != nil { return nil, fmt.Errorf("loading certificate %s from the database: %w", certName, err) } @@ -76,7 +76,7 @@ func ABMToken(ctx context.Context, ds fleet.MDMAssetRetriever, abmOrgName string assets, err := ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{ fleet.MDMAssetABMKey, fleet.MDMAssetABMCert, - }) + }, nil) if err != nil { return nil, fmt.Errorf("loading ABM assets from the database: %w", err) } diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index dd766be6dfd4..14c3ba040b4b 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -16,6 +16,7 @@ import ( "github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig" "github.com/fleetdm/fleet/v4/server/mdm/nanodep/godep" "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm" + "github.com/jmoiron/sqlx" ) var _ fleet.Datastore = (*DataStore)(nil) @@ -876,9 +877,9 @@ type MDMAppleSetPendingDeclarationsAsFunc func(ctx context.Context, hostUUID str type GetMDMAppleOSUpdatesSettingsByHostSerialFunc func(ctx context.Context, hostSerial string) (*fleet.AppleOSUpdateSettings, error) -type InsertMDMConfigAssetsFunc func(ctx context.Context, assets []fleet.MDMConfigAsset) error +type InsertMDMConfigAssetsFunc func(ctx context.Context, assets []fleet.MDMConfigAsset, tx sqlx.ExtContext) error -type GetAllMDMConfigAssetsByNameFunc func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) +type GetAllMDMConfigAssetsByNameFunc func(ctx context.Context, assetNames []fleet.MDMAssetName, queryerContext sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) type GetAllMDMConfigAssetsHashesFunc func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]string, error) @@ -886,7 +887,7 @@ type DeleteMDMConfigAssetsByNameFunc func(ctx context.Context, assetNames []flee type HardDeleteMDMConfigAssetFunc func(ctx context.Context, assetName fleet.MDMAssetName) error -type ReplaceMDMConfigAssetsFunc func(ctx context.Context, assets []fleet.MDMConfigAsset) error +type ReplaceMDMConfigAssetsFunc func(ctx context.Context, assets []fleet.MDMConfigAsset, tx sqlx.ExtContext) error type GetABMTokenByOrgNameFunc func(ctx context.Context, orgName string) (*fleet.ABMToken, error) @@ -5710,18 +5711,18 @@ func (s *DataStore) GetMDMAppleOSUpdatesSettingsByHostSerial(ctx context.Context return s.GetMDMAppleOSUpdatesSettingsByHostSerialFunc(ctx, hostSerial) } -func (s *DataStore) InsertMDMConfigAssets(ctx context.Context, assets []fleet.MDMConfigAsset) error { +func (s *DataStore) InsertMDMConfigAssets(ctx context.Context, assets []fleet.MDMConfigAsset, tx sqlx.ExtContext) error { s.mu.Lock() s.InsertMDMConfigAssetsFuncInvoked = true s.mu.Unlock() - return s.InsertMDMConfigAssetsFunc(ctx, assets) + return s.InsertMDMConfigAssetsFunc(ctx, assets, tx) } -func (s *DataStore) GetAllMDMConfigAssetsByName(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { +func (s *DataStore) GetAllMDMConfigAssetsByName(ctx context.Context, assetNames []fleet.MDMAssetName, queryerContext sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { s.mu.Lock() s.GetAllMDMConfigAssetsByNameFuncInvoked = true s.mu.Unlock() - return s.GetAllMDMConfigAssetsByNameFunc(ctx, assetNames) + return s.GetAllMDMConfigAssetsByNameFunc(ctx, assetNames, queryerContext) } func (s *DataStore) GetAllMDMConfigAssetsHashes(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]string, error) { @@ -5745,11 +5746,11 @@ func (s *DataStore) HardDeleteMDMConfigAsset(ctx context.Context, assetName flee return s.HardDeleteMDMConfigAssetFunc(ctx, assetName) } -func (s *DataStore) ReplaceMDMConfigAssets(ctx context.Context, assets []fleet.MDMConfigAsset) error { +func (s *DataStore) ReplaceMDMConfigAssets(ctx context.Context, assets []fleet.MDMConfigAsset, tx sqlx.ExtContext) error { s.mu.Lock() s.ReplaceMDMConfigAssetsFuncInvoked = true s.mu.Unlock() - return s.ReplaceMDMConfigAssetsFunc(ctx, assets) + return s.ReplaceMDMConfigAssetsFunc(ctx, assets, tx) } func (s *DataStore) GetABMTokenByOrgName(ctx context.Context, orgName string) (*fleet.ABMToken, error) { diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index 965e8f404e96..4dfc5ce72c64 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -1404,7 +1404,7 @@ func (svc *Service) GetMDMAppleEnrollmentProfileByToken(ctx context.Context, tok assets, err := svc.ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{ fleet.MDMAssetSCEPChallenge, - }) + }, nil) if err != nil { return nil, fmt.Errorf("loading SCEP challenge from the database: %w", err) } @@ -1512,7 +1512,7 @@ func (svc *Service) getAppleSoftwareUpdateRequiredForDEPEnrollment(m fleet.MDMAp func (svc *Service) mdmPushCertTopic(ctx context.Context) (string, error) { assets, err := svc.ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{ fleet.MDMAssetAPNSCert, - }) + }, nil) if err != nil { return "", ctxerr.Wrap(ctx, err, "loading SCEP keypair from the database") } @@ -3226,7 +3226,7 @@ func ReconcileAppleProfiles( assets, err := ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{ fleet.MDMAssetCACert, - }) + }, nil) if err != nil { return ctxerr.Wrap(ctx, err, "getting Apple SCEP") } @@ -3608,7 +3608,7 @@ func preprocessProfileContents( case FleetVarNDESSCEPChallenge: if ndesConfig == nil { // Retrieve the NDES admin password. This is done once per run. - configAssets, err := ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetNDESPassword}) + configAssets, err := ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetNDESPassword}, nil) if err != nil { return ctxerr.Wrap(ctx, err, "getting NDES password") } @@ -3820,7 +3820,7 @@ func RenewSCEPCertificates( assets, err := ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{ fleet.MDMAssetSCEPChallenge, - }) + }, nil) if err != nil { return ctxerr.Wrap(ctx, err, "loading SCEP challenge from the database") } @@ -4189,7 +4189,7 @@ func (svc *Service) GenerateABMKeyPair(ctx context.Context) (*fleet.MDMAppleDEPK assets, err := svc.ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{ fleet.MDMAssetABMCert, fleet.MDMAssetABMKey, - }) + }, nil) if err != nil { // allow not found errors as it means that we're generating the // keypair for the first time @@ -4209,7 +4209,7 @@ func (svc *Service) GenerateABMKeyPair(ctx context.Context) (*fleet.MDMAppleDEPK err = svc.ds.InsertMDMConfigAssets(ctx, []fleet.MDMConfigAsset{ {Name: fleet.MDMAssetABMCert, Value: publicKeyPEM}, {Name: fleet.MDMAssetABMKey, Value: privateKeyPEM}, - }) + }, nil) if err != nil { return nil, ctxerr.Wrap(ctx, err, "saving ABM keypair in database") } @@ -4606,7 +4606,7 @@ func (svc *Service) MDMAppleProcessOTAEnrollment( assets, err := svc.ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{ fleet.MDMAssetSCEPChallenge, - }) + }, nil) if err != nil { return nil, fmt.Errorf("loading SCEP challenge from the database: %w", err) } diff --git a/server/service/handler.go b/server/service/handler.go index d981ce6ac02b..b21ba317be24 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -1102,7 +1102,7 @@ func registerSCEP( scep_depot.WithValidityDays(scepConfig.AppleSCEPSignerValidityDays), scep_depot.WithAllowRenewalDays(scepConfig.AppleSCEPSignerAllowRenewalDays), )) - assets, err := mdmStorage.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{fleet.MDMAssetSCEPChallenge}) + assets, err := mdmStorage.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{fleet.MDMAssetSCEPChallenge}, nil) if err != nil { return fmt.Errorf("retrieving SCEP challenge: %w", err) } diff --git a/server/service/mdm.go b/server/service/mdm.go index 7a06c015cd30..7ce235935f34 100644 --- a/server/service/mdm.go +++ b/server/service/mdm.go @@ -2286,7 +2286,7 @@ func (svc *Service) GetMDMAppleCSR(ctx context.Context) ([]byte, error) { fleet.MDMAssetCACert, fleet.MDMAssetCAKey, fleet.MDMAssetAPNSKey, - }) + }, nil) if err != nil { // allow not found errors as it means we're generating the assets for // the first time. @@ -2321,7 +2321,7 @@ func (svc *Service) GetMDMAppleCSR(ctx context.Context) ([]byte, error) { }) } - if err := svc.ds.InsertMDMConfigAssets(ctx, assets); err != nil { + if err := svc.ds.InsertMDMConfigAssets(ctx, assets, nil); err != nil { return nil, ctxerr.Wrap(ctx, err, "inserting mdm config assets") } } else { @@ -2465,7 +2465,7 @@ func (svc *Service) UploadMDMAppleAPNSCert(ctx context.Context, cert io.ReadSeek return err } - assets, err := svc.ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetAPNSKey}) + assets, err := svc.ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetAPNSKey}, nil) if err != nil { if fleet.IsNotFound(err) { return ctxerr.Wrap(ctx, &fleet.BadRequestError{ @@ -2489,7 +2489,7 @@ func (svc *Service) UploadMDMAppleAPNSCert(ctx context.Context, cert io.ReadSeek } err = svc.ds.InsertMDMConfigAssets(ctx, []fleet.MDMConfigAsset{ {Name: fleet.MDMAssetAPNSCert, Value: certBytes}, - }) + }, nil) if err != nil { return ctxerr.Wrap(ctx, err, "writing apns cert to db") } diff --git a/tools/mdm/assets/main.go b/tools/mdm/assets/main.go index d91a246f30c7..9a2e8ed58b42 100644 --- a/tools/mdm/assets/main.go +++ b/tools/mdm/assets/main.go @@ -142,7 +142,8 @@ func main() { log.Fatalf("invalid asset name %s", flagImportName) } - err := ds.ReplaceMDMConfigAssets(ctx, []fleet.MDMConfigAsset{{Name: fleet.MDMAssetName(flagImportName), Value: []byte(flagImportValue)}}) + err := ds.ReplaceMDMConfigAssets(ctx, + []fleet.MDMConfigAsset{{Name: fleet.MDMAssetName(flagImportName), Value: []byte(flagImportValue)}}, nil) if err != nil { log.Fatal("writing asset to db: ", err) } @@ -192,7 +193,7 @@ func main() { names = []fleet.MDMAssetName{fleet.MDMAssetName(flagExportName)} } - assets, err := ds.GetAllMDMConfigAssetsByName(ctx, names) + assets, err := ds.GetAllMDMConfigAssetsByName(ctx, names, nil) if err != nil && !errors.Is(err, mysql.ErrPartialResult) { log.Fatal("retrieving assets from db:", err) } From 0a6c167b1b82e816e38163f5b128af30a71738d7 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Wed, 9 Oct 2024 13:00:47 -0500 Subject: [PATCH 25/27] Updated mdm_mock --- server/mock/mdm/datastore_mdm_mock.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/mock/mdm/datastore_mdm_mock.go b/server/mock/mdm/datastore_mdm_mock.go index 78141b25253f..5e3f19e6cbe2 100644 --- a/server/mock/mdm/datastore_mdm_mock.go +++ b/server/mock/mdm/datastore_mdm_mock.go @@ -10,6 +10,7 @@ import ( "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm" + "github.com/jmoiron/sqlx" ) var _ fleet.MDMAppleStore = (*MDMAppleStore)(nil) @@ -54,7 +55,7 @@ type RetrieveMigrationCheckinsFunc func(p0 context.Context, p1 chan<- interface{ type RetrieveTokenUpdateTallyFunc func(ctx context.Context, id string) (int, error) -type GetAllMDMConfigAssetsByNameFunc func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) +type GetAllMDMConfigAssetsByNameFunc func(ctx context.Context, assetNames []fleet.MDMAssetName, queryerContext sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) type GetABMTokenByOrgNameFunc func(ctx context.Context, orgName string) (*fleet.ABMToken, error) @@ -278,11 +279,11 @@ func (fs *MDMAppleStore) RetrieveTokenUpdateTally(ctx context.Context, id string return fs.RetrieveTokenUpdateTallyFunc(ctx, id) } -func (fs *MDMAppleStore) GetAllMDMConfigAssetsByName(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { +func (fs *MDMAppleStore) GetAllMDMConfigAssetsByName(ctx context.Context, assetNames []fleet.MDMAssetName, queryerContext sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { fs.mu.Lock() fs.GetAllMDMConfigAssetsByNameFuncInvoked = true fs.mu.Unlock() - return fs.GetAllMDMConfigAssetsByNameFunc(ctx, assetNames) + return fs.GetAllMDMConfigAssetsByNameFunc(ctx, assetNames, queryerContext) } func (fs *MDMAppleStore) GetABMTokenByOrgName(ctx context.Context, orgName string) (*fleet.ABMToken, error) { From 13850b0294d1e8f7c4734267a10ceebc1ec06cbc Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Wed, 9 Oct 2024 13:18:10 -0500 Subject: [PATCH 26/27] Fixed lint/compile issues in tests. --- cmd/fleet/serve_test.go | 4 +++- .../gitops_enterprise_integration_test.go | 4 ++-- cmd/fleetctl/gitops_integration_test.go | 2 +- cmd/fleetctl/gitops_test.go | 4 +++- ee/server/service/mdm_external_test.go | 4 +++- .../datastore/cached_mysql/cached_mysql_test.go | 16 +++++++++------- server/mdm/apple/commander_test.go | 4 +++- server/mdm/assets/assets_test.go | 16 +++++++++++----- server/mdm/crypto/scep_test.go | 4 +++- server/service/apple_mdm_test.go | 7 +++++-- server/service/hosts_test.go | 10 +++++++--- server/service/integration_mdm_lifecycle_test.go | 2 +- server/service/mdm_test.go | 9 ++++++--- 13 files changed, 57 insertions(+), 29 deletions(-) diff --git a/cmd/fleet/serve_test.go b/cmd/fleet/serve_test.go index e472566f3e93..0d147f698603 100644 --- a/cmd/fleet/serve_test.go +++ b/cmd/fleet/serve_test.go @@ -30,6 +30,7 @@ import ( "github.com/go-kit/log" kitlog "github.com/go-kit/log" "github.com/go-kit/log/level" + "github.com/jmoiron/sqlx" "github.com/smallstep/pkcs7" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -1077,7 +1078,8 @@ func TestVerifyDiskEncryptionKeysJob(t *testing.T) { fleet.MDMAssetCACert: {Value: testCertPEM}, fleet.MDMAssetCAKey: {Value: testKeyPEM}, } - ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, + _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { return assets, nil } ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { diff --git a/cmd/fleetctl/gitops_enterprise_integration_test.go b/cmd/fleetctl/gitops_enterprise_integration_test.go index 235d89dca82a..1128fbf26879 100644 --- a/cmd/fleetctl/gitops_enterprise_integration_test.go +++ b/cmd/fleetctl/gitops_enterprise_integration_test.go @@ -59,7 +59,7 @@ func (s *enterpriseIntegrationGitopsTestSuite) SetupSuite() { {Name: fleet.MDMAssetAPNSKey, Value: testKeyPEM}, {Name: fleet.MDMAssetCACert, Value: testCertPEM}, {Name: fleet.MDMAssetCAKey, Value: testKeyPEM}, - }) + }, nil) require.NoError(s.T(), err) mdmStorage, err := s.ds.NewMDMAppleMDMStorage() @@ -83,7 +83,7 @@ func (s *enterpriseIntegrationGitopsTestSuite) SetupSuite() { } err = s.ds.InsertMDMConfigAssets(context.Background(), []fleet.MDMConfigAsset{ {Name: fleet.MDMAssetSCEPChallenge, Value: []byte("scepchallenge")}, - }) + }, nil) require.NoError(s.T(), err) users, server := service.RunServerForTestsWithDS(s.T(), s.ds, &serverConfig) s.T().Setenv("FLEET_SERVER_ADDRESS", server.URL) // fleetctl always uses this env var in tests diff --git a/cmd/fleetctl/gitops_integration_test.go b/cmd/fleetctl/gitops_integration_test.go index 3117edc13aa2..78d88c7c5f7b 100644 --- a/cmd/fleetctl/gitops_integration_test.go +++ b/cmd/fleetctl/gitops_integration_test.go @@ -71,7 +71,7 @@ func (s *integrationGitopsTestSuite) SetupSuite() { } err = s.ds.InsertMDMConfigAssets(context.Background(), []fleet.MDMConfigAsset{ {Name: fleet.MDMAssetSCEPChallenge, Value: []byte("scepchallenge")}, - }) + }, nil) require.NoError(s.T(), err) users, server := service.RunServerForTestsWithDS(s.T(), s.ds, &serverConfig) s.T().Setenv("FLEET_SERVER_ADDRESS", server.URL) // fleetctl always uses this env var in tests diff --git a/cmd/fleetctl/gitops_test.go b/cmd/fleetctl/gitops_test.go index 295172612d74..b6119885b3a3 100644 --- a/cmd/fleetctl/gitops_test.go +++ b/cmd/fleetctl/gitops_test.go @@ -27,6 +27,7 @@ import ( "github.com/fleetdm/fleet/v4/server/service" "github.com/fleetdm/fleet/v4/server/test" "github.com/google/uuid" + "github.com/jmoiron/sqlx" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -1682,7 +1683,8 @@ func TestGitOpsFullGlobalAndTeam(t *testing.T) { scepCert := tokenpki.PEMCertificate(crt.Raw) scepKey := tokenpki.PEMRSAPrivateKey(key) - ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, + _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { return map[fleet.MDMAssetName]fleet.MDMConfigAsset{ fleet.MDMAssetCACert: {Value: scepCert}, fleet.MDMAssetCAKey: {Value: scepKey}, diff --git a/ee/server/service/mdm_external_test.go b/ee/server/service/mdm_external_test.go index 760d046c9c5b..9522a87e5198 100644 --- a/ee/server/service/mdm_external_test.go +++ b/ee/server/service/mdm_external_test.go @@ -30,6 +30,7 @@ import ( "github.com/fleetdm/fleet/v4/server/worker" kitlog "github.com/go-kit/log" "github.com/google/uuid" + "github.com/jmoiron/sqlx" "github.com/stretchr/testify/require" ) @@ -234,7 +235,8 @@ func TestGetOrCreatePreassignTeam(t *testing.T) { require.NoError(t, err) certPEM, keyPEM, tokenBytes, err := mysql.GenerateTestABMAssets(t) require.NoError(t, err) - ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, + _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { return map[fleet.MDMAssetName]fleet.MDMConfigAsset{ fleet.MDMAssetABMCert: {Name: fleet.MDMAssetABMCert, Value: certPEM}, fleet.MDMAssetABMKey: {Name: fleet.MDMAssetABMKey, Value: keyPEM}, diff --git a/server/datastore/cached_mysql/cached_mysql_test.go b/server/datastore/cached_mysql/cached_mysql_test.go index 004f3360bfb2..6fa01758dba2 100644 --- a/server/datastore/cached_mysql/cached_mysql_test.go +++ b/server/datastore/cached_mysql/cached_mysql_test.go @@ -12,6 +12,7 @@ import ( "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mock" "github.com/fleetdm/fleet/v4/server/ptr" + "github.com/jmoiron/sqlx" "github.com/stretchr/testify/require" ) @@ -772,7 +773,8 @@ func TestGetAllMDMConfigAssetsByName(t *testing.T) { return result, nil } - mockedDS.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + mockedDS.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, + _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { result := map[fleet.MDMAssetName]fleet.MDMConfigAsset{} for _, n := range assetNames { result[n] = assetMap[n] @@ -781,7 +783,7 @@ func TestGetAllMDMConfigAssetsByName(t *testing.T) { } // returns cached assets if hashes match - result, err := ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{"asset1", "asset2"}) + result, err := ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{"asset1", "asset2"}, nil) require.NoError(t, err) require.True(t, mockedDS.GetAllMDMConfigAssetsByNameFuncInvoked) @@ -792,7 +794,7 @@ func TestGetAllMDMConfigAssetsByName(t *testing.T) { require.Equal(t, assetMap["asset2"], result["asset2"]) require.NotContains(t, result, "asset3") - result, err = ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{"asset1", "asset2"}) + result, err = ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{"asset1", "asset2"}, nil) require.NoError(t, err) require.False(t, mockedDS.GetAllMDMConfigAssetsByNameFuncInvoked) require.True(t, mockedDS.GetAllMDMConfigAssetsHashesFuncInvoked) @@ -805,7 +807,7 @@ func TestGetAllMDMConfigAssetsByName(t *testing.T) { mockedDS.GetAllMDMConfigAssetsHashesFuncInvoked = false // fetches missing assets from the db - result, err = ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{"asset1", "asset2", "asset3"}) + result, err = ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{"asset1", "asset2", "asset3"}, nil) require.NoError(t, err) require.Equal(t, assetMap["asset1"], result["asset1"]) require.Equal(t, assetMap["asset2"], result["asset2"]) @@ -818,7 +820,7 @@ func TestGetAllMDMConfigAssetsByName(t *testing.T) { // fetches updated assets from the db assetHashes["asset1"] = "newhash" assetMap["asset1"] = fleet.MDMConfigAsset{Name: "asset1", Value: []byte("newvalue")} - result, err = ds.GetAllMDMConfigAssetsByName(context.Background(), assetNames) + result, err = ds.GetAllMDMConfigAssetsByName(context.Background(), assetNames, nil) require.NoError(t, err) require.Equal(t, assetMap["asset1"], result["asset1"]) require.Equal(t, assetMap["asset2"], result["asset2"]) @@ -833,7 +835,7 @@ func TestGetAllMDMConfigAssetsByName(t *testing.T) { return nil, errors.New("error fetching assets") } - _, err = ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{"not exists"}) + _, err = ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{"not exists"}, nil) require.Error(t, err) require.Equal(t, "error fetching assets", err.Error()) @@ -842,7 +844,7 @@ func TestGetAllMDMConfigAssetsByName(t *testing.T) { return nil, errors.New("error fetching hashes") } - _, err = ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{"not exists"}) + _, err = ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{"not exists"}, nil) require.Error(t, err) require.Equal(t, "error fetching hashes", err.Error()) } diff --git a/server/mdm/apple/commander_test.go b/server/mdm/apple/commander_test.go index 29138179f40b..6f63d1054003 100644 --- a/server/mdm/apple/commander_test.go +++ b/server/mdm/apple/commander_test.go @@ -18,6 +18,7 @@ import ( svcmock "github.com/fleetdm/fleet/v4/server/service/mock" "github.com/google/uuid" "github.com/groob/plist" + "github.com/jmoiron/sqlx" micromdm "github.com/micromdm/micromdm/mdm/mdm" "github.com/smallstep/pkcs7" "github.com/stretchr/testify/require" @@ -75,7 +76,8 @@ func TestMDMAppleCommander(t *testing.T) { mdmStorage.IsPushCertStaleFunc = func(ctx context.Context, topic string, staleToken string) (bool, error) { return false, nil } - mdmStorage.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + mdmStorage.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, + _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { certPEM, err := os.ReadFile("../../service/testdata/server.pem") require.NoError(t, err) keyPEM, err := os.ReadFile("../../service/testdata/server.key") diff --git a/server/mdm/assets/assets_test.go b/server/mdm/assets/assets_test.go index b6484fd4b003..9e12ea074fda 100644 --- a/server/mdm/assets/assets_test.go +++ b/server/mdm/assets/assets_test.go @@ -18,6 +18,7 @@ import ( "github.com/fleetdm/fleet/v4/server/fleet" nanodep_client "github.com/fleetdm/fleet/v4/server/mdm/nanodep/client" "github.com/fleetdm/fleet/v4/server/mock" + "github.com/jmoiron/sqlx" "github.com/smallstep/pkcs7" "github.com/stretchr/testify/require" ) @@ -70,7 +71,8 @@ func TestCAKeyPair(t *testing.T) { fleet.MDMAssetCAKey: {Value: keyPEM}, } - ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, + _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { require.ElementsMatch(t, []fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetCAKey}, assetNames) return assets, nil } @@ -92,7 +94,8 @@ func TestAPNSKeyPair(t *testing.T) { fleet.MDMAssetAPNSCert: {Value: certPEM}, fleet.MDMAssetAPNSKey: {Value: keyPEM}, } - ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, + _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { require.ElementsMatch(t, []fleet.MDMAssetName{fleet.MDMAssetAPNSCert, fleet.MDMAssetAPNSKey}, assetNames) return assets, nil } @@ -112,7 +115,8 @@ func TestX509Cert(t *testing.T) { assets := map[fleet.MDMAssetName]fleet.MDMConfigAsset{ fleet.MDMAssetAPNSCert: {Value: certPEM}, } - ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, + _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { require.ElementsMatch(t, []fleet.MDMAssetName{fleet.MDMAssetAPNSCert}, assetNames) return assets, nil } @@ -133,7 +137,8 @@ func TestAPNSTopic(t *testing.T) { assets := map[fleet.MDMAssetName]fleet.MDMConfigAsset{ fleet.MDMAssetAPNSCert: {Value: certPEM}, } - ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, + _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { require.ElementsMatch(t, []fleet.MDMAssetName{fleet.MDMAssetAPNSCert}, assetNames) return assets, nil } @@ -188,7 +193,8 @@ func TestABMToken(t *testing.T) { fleet.MDMAssetABMCert: {Value: certPEM}, fleet.MDMAssetABMKey: {Value: keyPEM}, } - ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, + _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { require.ElementsMatch(t, []fleet.MDMAssetName{ fleet.MDMAssetABMCert, fleet.MDMAssetABMKey, diff --git a/server/mdm/crypto/scep_test.go b/server/mdm/crypto/scep_test.go index 179864b9d3ff..b59bc62e8247 100644 --- a/server/mdm/crypto/scep_test.go +++ b/server/mdm/crypto/scep_test.go @@ -15,6 +15,7 @@ import ( "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mock" + "github.com/jmoiron/sqlx" "github.com/stretchr/testify/require" ) @@ -76,7 +77,8 @@ func TestVerify(t *testing.T) { for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { - ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, + _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { if tt.rootCert == nil { return nil, errors.New("test error") } diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go index d23a4174f367..e49355314f98 100644 --- a/server/service/apple_mdm_test.go +++ b/server/service/apple_mdm_test.go @@ -50,6 +50,7 @@ import ( kitlog "github.com/go-kit/log" "github.com/google/uuid" "github.com/groob/plist" + "github.com/jmoiron/sqlx" micromdm "github.com/micromdm/micromdm/mdm/mdm" "github.com/smallstep/pkcs7" "github.com/stretchr/testify/assert" @@ -214,7 +215,8 @@ func setupAppleMDMService(t *testing.T, license *fleet.LicenseInfo) (fleet.Servi require.NoError(t, err) certPEM := tokenpki.PEMCertificate(crt.Raw) keyPEM := tokenpki.PEMRSAPrivateKey(key) - ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, + _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { return map[fleet.MDMAssetName]fleet.MDMConfigAsset{ fleet.MDMAssetAPNSCert: {Value: apnsCert}, fleet.MDMAssetAPNSKey: {Value: apnsKey}, @@ -2199,7 +2201,8 @@ func TestMDMAppleReconcileAppleProfiles(t *testing.T) { AppleSCEPCert: "./testdata/server.pem", AppleSCEPKey: "./testdata/server.key", } - ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, + _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { _, pemCert, pemKey, err := mdmConfig.AppleSCEP() require.NoError(t, err) return map[fleet.MDMAssetName]fleet.MDMConfigAsset{ diff --git a/server/service/hosts_test.go b/server/service/hosts_test.go index 59e1cc9413d7..8ae126df9698 100644 --- a/server/service/hosts_test.go +++ b/server/service/hosts_test.go @@ -26,6 +26,7 @@ import ( "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/test" kitlog "github.com/go-kit/log" + "github.com/jmoiron/sqlx" "github.com/smallstep/pkcs7" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -1281,7 +1282,8 @@ func TestHostEncryptionKey(t *testing.T) { return nil } - ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, + _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { return map[fleet.MDMAssetName]fleet.MDMConfigAsset{ fleet.MDMAssetCACert: {Name: fleet.MDMAssetCACert, Value: testCertPEM}, fleet.MDMAssetCAKey: {Name: fleet.MDMAssetCAKey, Value: testKeyPEM}, @@ -1333,7 +1335,8 @@ func TestHostEncryptionKey(t *testing.T) { ds.GetHostDiskEncryptionKeyFunc = func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) { return nil, keyErr } - ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, + _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { return map[fleet.MDMAssetName]fleet.MDMConfigAsset{ fleet.MDMAssetCACert: {Name: fleet.MDMAssetCACert, Value: testCertPEM}, fleet.MDMAssetCAKey: {Name: fleet.MDMAssetCAKey, Value: testKeyPEM}, @@ -1393,7 +1396,8 @@ func TestHostEncryptionKey(t *testing.T) { ) error { return nil } - ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, + _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { return map[fleet.MDMAssetName]fleet.MDMConfigAsset{ fleet.MDMAssetCACert: {Name: fleet.MDMAssetCACert, Value: testCertPEM}, fleet.MDMAssetCAKey: {Name: fleet.MDMAssetCAKey, Value: testKeyPEM}, diff --git a/server/service/integration_mdm_lifecycle_test.go b/server/service/integration_mdm_lifecycle_test.go index 27c1458c4075..c52810d36aae 100644 --- a/server/service/integration_mdm_lifecycle_test.go +++ b/server/service/integration_mdm_lifecycle_test.go @@ -752,7 +752,7 @@ func (s *integrationMDMTestSuite) TestLifecycleSCEPCertExpiration() { assets, err := s.ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{ fleet.MDMAssetCACert, - }) + }, nil) require.NoError(t, err) require.True(t, rootCA.AppendCertsFromPEM(assets[fleet.MDMAssetCACert].Value)) diff --git a/server/service/mdm_test.go b/server/service/mdm_test.go index 3a626af0277d..46c44a0b5204 100644 --- a/server/service/mdm_test.go +++ b/server/service/mdm_test.go @@ -19,6 +19,7 @@ import ( nanodep_client "github.com/fleetdm/fleet/v4/server/mdm/nanodep/client" nanodep_mock "github.com/fleetdm/fleet/v4/server/mock/nanodep" + "github.com/jmoiron/sqlx" "github.com/fleetdm/fleet/v4/server/authz" "github.com/fleetdm/fleet/v4/server/config" @@ -46,7 +47,8 @@ func TestGetMDMApple(t *testing.T) { keyPEM, err := os.ReadFile("testdata/server.key") require.NoError(t, err) - ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, + _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { return map[fleet.MDMAssetName]fleet.MDMConfigAsset{ fleet.MDMAssetAPNSCert: {Name: fleet.MDMAssetAPNSCert, Value: certPEM}, fleet.MDMAssetAPNSKey: {Name: fleet.MDMAssetAPNSKey, Value: keyPEM}, @@ -105,11 +107,12 @@ func TestMDMAppleAuthorization(t *testing.T) { }, nil } - ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, + _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { return map[fleet.MDMAssetName]fleet.MDMConfigAsset{}, nil } - ds.InsertMDMConfigAssetsFunc = func(ctx context.Context, assets []fleet.MDMConfigAsset) error { return nil } + ds.InsertMDMConfigAssetsFunc = func(ctx context.Context, assets []fleet.MDMConfigAsset, _ sqlx.ExtContext) error { return nil } ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { return &fleet.AppConfig{OrgInfo: fleet.OrgInfo{OrgName: "Nurv"}}, nil From 59d7b643ed948279afc64e4cab63431d9bd7bf2d Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Wed, 9 Oct 2024 13:30:04 -0500 Subject: [PATCH 27/27] Fixed lint/compile issues in tests. --- ee/server/service/mdm_test.go | 7 ++++-- .../cached_mysql/cached_mysql_test.go | 3 ++- server/mdm/crypto/sign_test.go | 4 +++- server/service/apple_mdm_test.go | 11 +++++---- .../service/integration_mdm_profiles_test.go | 2 +- server/service/integration_mdm_test.go | 24 +++++++++++-------- server/service/software_installers_test.go | 4 +++- server/service/vpp_test.go | 4 +++- 8 files changed, 38 insertions(+), 21 deletions(-) diff --git a/ee/server/service/mdm_test.go b/ee/server/service/mdm_test.go index df7e5afb50e4..21cc3d21d939 100644 --- a/ee/server/service/mdm_test.go +++ b/ee/server/service/mdm_test.go @@ -11,13 +11,15 @@ import ( "github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig" "github.com/fleetdm/fleet/v4/server/mock" "github.com/fleetdm/fleet/v4/server/ptr" + "github.com/jmoiron/sqlx" "github.com/stretchr/testify/require" ) func setup(t *testing.T) (*mock.Store, *Service) { ds := new(mock.Store) - ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, + _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { return map[fleet.MDMAssetName]fleet.MDMConfigAsset{ fleet.MDMAssetCACert: {Value: []byte(testCert)}, fleet.MDMAssetCAKey: {Value: []byte(testKey)}, @@ -38,7 +40,8 @@ func TestMDMAppleEnableFileVaultAndEscrow(t *testing.T) { t.Run("fails if SCEP is not configured", func(t *testing.T) { ds := new(mock.Store) svc := &Service{ds: ds} - ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, + _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { return nil, nil } err := svc.MDMAppleEnableFileVaultAndEscrow(ctx, nil) diff --git a/server/datastore/cached_mysql/cached_mysql_test.go b/server/datastore/cached_mysql/cached_mysql_test.go index 6fa01758dba2..bb41f514c6af 100644 --- a/server/datastore/cached_mysql/cached_mysql_test.go +++ b/server/datastore/cached_mysql/cached_mysql_test.go @@ -831,7 +831,8 @@ func TestGetAllMDMConfigAssetsByName(t *testing.T) { mockedDS.GetAllMDMConfigAssetsHashesFuncInvoked = false // passes errors fetching assets from downstream - mockedDS.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + mockedDS.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, + _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { return nil, errors.New("error fetching assets") } diff --git a/server/mdm/crypto/sign_test.go b/server/mdm/crypto/sign_test.go index 7a46eb71ba2f..bf8935550cc3 100644 --- a/server/mdm/crypto/sign_test.go +++ b/server/mdm/crypto/sign_test.go @@ -7,6 +7,7 @@ import ( "github.com/fleetdm/fleet/v4/server/fleet" mdmmock "github.com/fleetdm/fleet/v4/server/mock/mdm" + "github.com/jmoiron/sqlx" "github.com/stretchr/testify/require" ) @@ -43,7 +44,8 @@ func TestSign(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, + _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { require.ElementsMatch(t, []fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetCAKey}, assetNames) return map[fleet.MDMAssetName]fleet.MDMConfigAsset{ fleet.MDMAssetCACert: {Value: tc.cert}, diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go index e49355314f98..d93d1c45c34a 100644 --- a/server/service/apple_mdm_test.go +++ b/server/service/apple_mdm_test.go @@ -2309,7 +2309,8 @@ func TestMDMAppleReconcileAppleProfiles(t *testing.T) { mdmStorage.IsPushCertStaleFunc = func(ctx context.Context, topic string, staleToken string) (bool, error) { return false, nil } - mdmStorage.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + mdmStorage.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, + _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { certPEM, err := os.ReadFile("./testdata/server.pem") require.NoError(t, err) keyPEM, err := os.ReadFile("./testdata/server.key") @@ -2816,7 +2817,7 @@ func TestPreprocessProfileContents(t *testing.T) { ndesPassword := "test-password" ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, - assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + assetNames []fleet.MDMAssetName, _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { return map[fleet.MDMAssetName]fleet.MDMConfigAsset{ fleet.MDMAssetNDESPassword: {Value: []byte(ndesPassword)}, }, nil @@ -3505,7 +3506,8 @@ func setupTest(t *testing.T) (context.Context, kitlog.Logger, *mock.Store, *conf _, pemCert, pemKey, err := mdmConfig.AppleSCEP() require.NoError(t, err) - ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, + _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { return map[fleet.MDMAssetName]fleet.MDMConfigAsset{ fleet.MDMAssetCACert: {Value: pemCert}, fleet.MDMAssetCAKey: {Value: pemKey}, @@ -3513,7 +3515,8 @@ func setupTest(t *testing.T) (context.Context, kitlog.Logger, *mock.Store, *conf fleet.MDMAssetAPNSCert: {Value: apnsCert}, }, nil } - mdmStorage.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + mdmStorage.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, + _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { return map[fleet.MDMAssetName]fleet.MDMConfigAsset{ fleet.MDMAssetCACert: {Value: pemCert}, fleet.MDMAssetCAKey: {Value: pemKey}, diff --git a/server/service/integration_mdm_profiles_test.go b/server/service/integration_mdm_profiles_test.go index 1566fae3b114..2b74bdcde9f6 100644 --- a/server/service/integration_mdm_profiles_test.go +++ b/server/service/integration_mdm_profiles_test.go @@ -42,7 +42,7 @@ func (s *integrationMDMTestSuite) signedProfilesMatch(want, got [][]byte) { assets, err := s.ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{ fleet.MDMAssetCACert, - }) + }, nil) require.NoError(t, err) require.True(t, rootCA.AppendCertsFromPEM(assets[fleet.MDMAssetCACert].Value)) diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 205eacf38c57..25e9664a7819 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -297,7 +297,7 @@ func (s *integrationMDMTestSuite) SetupSuite() { s.scepChallenge = "scepcha/>