From fc4106c688a17d5397fb99484d56953071b46a3e Mon Sep 17 00:00:00 2001 From: Jonathan Katz <44128041+jkatz01@users.noreply.github.com> Date: Mon, 5 Jan 2026 16:30:31 -0500 Subject: [PATCH] Cloudfront signing for in-house apps (#37650) **Related issue:** Resolves #33756 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. ## Testing - [x] Added/updated automated tests - [ ] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [x] QA'd all new/changed functionality manually --- changes/35565-ipa-cloudfront-signing | 1 + ee/server/service/in_house_apps.go | 31 ++++++++--- ee/server/service/software_installers.go | 2 +- ee/server/service/software_installers_test.go | 23 ++++++++ server/datastore/failing/common_store.go | 2 +- .../filesystem/software_installer.go | 2 +- .../filesystem/software_title_icons.go | 2 +- .../filesystem/software_title_icons_test.go | 2 +- server/datastore/s3/common_file_store.go | 6 +- server/fleet/apple_mdm.go | 4 +- server/fleet/in_house_apps.go | 6 ++ server/fleet/software_installer.go | 3 +- server/fleet/software_title_icons.go | 4 +- server/mock/mdm/bootstrap_package_store.go | 6 +- .../mock/software/software_installer_store.go | 6 +- server/service/integration_install_test.go | 55 +++++++++++++++++-- .../service/integration_vpp_install_test.go | 31 +++++++++++ server/worker/apple_mdm.go | 2 +- server/worker/apple_mdm_test.go | 6 +- 19 files changed, 159 insertions(+), 35 deletions(-) create mode 100644 changes/35565-ipa-cloudfront-signing diff --git a/changes/35565-ipa-cloudfront-signing b/changes/35565-ipa-cloudfront-signing new file mode 100644 index 0000000000..66fe23b123 --- /dev/null +++ b/changes/35565-ipa-cloudfront-signing @@ -0,0 +1 @@ +* Added support for in-house apps to use Cloudfront signed URLs in manifest if Cloudfront is configured. diff --git a/ee/server/service/in_house_apps.go b/ee/server/service/in_house_apps.go index d569636f03..c240023b1c 100644 --- a/ee/server/service/in_house_apps.go +++ b/ee/server/service/in_house_apps.go @@ -10,7 +10,9 @@ import ( "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/contexts/viewer" "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig" "github.com/fleetdm/fleet/v4/server/ptr" + "github.com/go-kit/log/level" ) func (svc *Service) updateInHouseAppInstaller(ctx context.Context, payload *fleet.UpdateSoftwareInstallerPayload, vc viewer.Viewer, teamName *string, software *fleet.SoftwareTitle) (*fleet.SoftwareInstaller, error) { @@ -165,18 +167,29 @@ func (svc *Service) GetInHouseAppManifest(ctx context.Context, titleID uint, tea return nil, ctxerr.Wrap(ctx, err, "get in house app manifest: get app config") } - var tid uint - if teamID != nil { - tid = *teamID - } - downloadUrl := fmt.Sprintf("%s/api/latest/fleet/software/titles/%d/in_house_app?team_id=%d", appConfig.ServerSettings.ServerURL, titleID, tid) - meta, err := svc.ds.GetInHouseAppMetadataByTeamAndTitleID(ctx, teamID, titleID) if err != nil { return nil, ctxerr.Wrap(ctx, err, "get in house app manifest: get in house app metadata") } - tmpl := template.Must(template.New("").Parse(` + downloadURL := fmt.Sprintf("%s/api/latest/fleet/software/titles/%d/in_house_app?team_id=%d", appConfig.ServerSettings.ServerURL, titleID, ptr.ValOrZero(teamID)) + + if svc.config.S3.SoftwareInstallersCloudFrontSigner != nil { + signedURL, err := svc.softwareInstallStore.Sign(ctx, meta.StorageID, fleet.InHouseAppSignedURLExpiry) + if err != nil { + // We log the error and continue to send the Fleet server URL for the in-house app + level.Error(svc.logger).Log("msg", "error signing in-house app URL; check CloudFront configuration", "err", err) + } else { + downloadURL = signedURL + } + } + + // Escape & characters in case of using CloudFront signed URL + var funcMap = map[string]any{ + "xml": mobileconfig.XMLEscapeString, + } + + tmpl := template.Must(template.New("").Funcs(funcMap).Parse(` items @@ -188,7 +201,7 @@ func (svc *Service) GetInHouseAppManifest(ctx context.Context, titleID uint, tea kind software-package url - {{ .URL }} + {{ .URL | xml }} kind @@ -222,7 +235,7 @@ func (svc *Service) GetInHouseAppManifest(ctx context.Context, titleID uint, tea Version string Name string URL string - }{meta.BundleIdentifier, meta.Version, meta.SoftwareTitle, downloadUrl}) + }{meta.BundleIdentifier, meta.Version, meta.SoftwareTitle, downloadURL}) if err != nil { return nil, ctxerr.Wrap(ctx, err, "rendering app manifest") diff --git a/ee/server/service/software_installers.go b/ee/server/service/software_installers.go index 8aab6c4f4e..de8e02bbc6 100644 --- a/ee/server/service/software_installers.go +++ b/ee/server/service/software_installers.go @@ -1033,7 +1033,7 @@ func (svc *Service) getSoftwareInstallURL(ctx context.Context, installerID uint) // If CloudFront is misconfigured, the server and Orbit clients will experience a greater load since they'll be doing throw-away work. // Get the signed URL - signedURL, err := svc.softwareInstallStore.Sign(ctx, meta.StorageID) + signedURL, err := svc.softwareInstallStore.Sign(ctx, meta.StorageID, fleet.SoftwareInstallerSignedURLExpiry) if err != nil { return nil, ctxerr.Wrap(ctx, err, "signing software installer URL") } diff --git a/ee/server/service/software_installers_test.go b/ee/server/service/software_installers_test.go index f0749938a8..aecf816298 100644 --- a/ee/server/service/software_installers_test.go +++ b/ee/server/service/software_installers_test.go @@ -2,6 +2,8 @@ package service import ( "context" + "crypto/rand" + "crypto/rsa" "crypto/sha256" "encoding/hex" "encoding/json" @@ -14,7 +16,9 @@ import ( ma "github.com/fleetdm/fleet/v4/ee/maintained-apps" "github.com/fleetdm/fleet/v4/server/authz" + "github.com/fleetdm/fleet/v4/server/config" "github.com/fleetdm/fleet/v4/server/contexts/viewer" + "github.com/fleetdm/fleet/v4/server/datastore/s3" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm" "github.com/fleetdm/fleet/v4/server/mock" @@ -357,6 +361,7 @@ func TestGetInHouseAppManifest(t *testing.T) { BundleIdentifier: "com.foo.bar", Version: "1.2.3", SoftwareTitle: "test in-house app", + StorageID: "123storageid", }, nil } @@ -411,6 +416,24 @@ func TestGetInHouseAppManifest(t *testing.T) { assert.Error(t, err) assert.True(t, fleet.IsNotFound(err)) + // Set up a new S3 store to test CloudFront signing + signer, _ := rsa.GenerateKey(rand.Reader, 2048) + svc.config.S3.SoftwareInstallersCloudFrontSigner = signer + signerURL := "https://example.cloudfront.net" + + s3Config := config.S3Config{ + SoftwareInstallersCloudFrontURL: signerURL, + SoftwareInstallersCloudFrontURLSigningPublicKeyID: "ABC123XYZ", + SoftwareInstallersCloudFrontSigner: signer, + } + s3Store, err := s3.NewTestSoftwareInstallerStore(s3Config) + require.NoError(t, err) + svc.softwareInstallStore = s3Store + + manifest, err = svc.GetInHouseAppManifest(ctx, 1, nil) + require.NoError(t, err) + require.Contains(t, string(manifest), signerURL) + } func checkAuthErr(t *testing.T, shouldFail bool, err error) { diff --git a/server/datastore/failing/common_store.go b/server/datastore/failing/common_store.go index 6e8052361c..580aa75e9f 100644 --- a/server/datastore/failing/common_store.go +++ b/server/datastore/failing/common_store.go @@ -30,6 +30,6 @@ func (c commonFailingStore) Cleanup(ctx context.Context, usedIconIDs []string, r return 0, nil } -func (c commonFailingStore) Sign(_ context.Context, _ string) (string, error) { +func (c commonFailingStore) Sign(_ context.Context, _ string, _ time.Duration) (string, error) { return "", fmt.Errorf("%s store not properly configured", c.Entity) } diff --git a/server/datastore/filesystem/software_installer.go b/server/datastore/filesystem/software_installer.go index 25a90a25ae..26c7a6fc51 100644 --- a/server/datastore/filesystem/software_installer.go +++ b/server/datastore/filesystem/software_installer.go @@ -133,7 +133,7 @@ func (i *SoftwareInstallerStore) Cleanup(ctx context.Context, usedInstallerIDs [ return count, ctxerr.Wrap(ctx, errors.Join(errs...), "delete unused software installers") } -func (i *SoftwareInstallerStore) Sign(ctx context.Context, _ string) (string, error) { +func (i *SoftwareInstallerStore) Sign(ctx context.Context, _ string, _ time.Duration) (string, error) { return "", ctxerr.New(ctx, "signing not supported for software installers in filesystem store") } diff --git a/server/datastore/filesystem/software_title_icons.go b/server/datastore/filesystem/software_title_icons.go index 4898cb06d9..a399f0bbc0 100644 --- a/server/datastore/filesystem/software_title_icons.go +++ b/server/datastore/filesystem/software_title_icons.go @@ -130,7 +130,7 @@ func (s *SoftwareTitleIconStore) Cleanup(ctx context.Context, usedIconIDs []stri return count, ctxerr.Wrap(ctx, errors.Join(errs...), "delete unused software title icons") } -func (s *SoftwareTitleIconStore) Sign(ctx context.Context, _ string) (string, error) { +func (s *SoftwareTitleIconStore) Sign(ctx context.Context, _ string, _ time.Duration) (string, error) { return "", ctxerr.New(ctx, "signing not supported for software title icons in filesystem store") } diff --git a/server/datastore/filesystem/software_title_icons_test.go b/server/datastore/filesystem/software_title_icons_test.go index 446c8f5052..981e8982f3 100644 --- a/server/datastore/filesystem/software_title_icons_test.go +++ b/server/datastore/filesystem/software_title_icons_test.go @@ -91,7 +91,7 @@ func TestSoftwareTitleIconStore(t *testing.T) { require.Equal(t, 1, n) assertIconsOnDisk(t, dir, []string{id0}) - _, err = store.Sign(ctx, id0) + _, err = store.Sign(ctx, id0, fleet.SoftwareTitleIconSignedURLExpiry) require.Error(t, err) require.Contains(t, err.Error(), "signing not supported for software title icons in filesystem store") } diff --git a/server/datastore/s3/common_file_store.go b/server/datastore/s3/common_file_store.go index 3969a98584..8a7b2a1eed 100644 --- a/server/datastore/s3/common_file_store.go +++ b/server/datastore/s3/common_file_store.go @@ -20,8 +20,6 @@ import ( "golang.org/x/sync/errgroup" ) -const signedURLExpiresIn = 6 * time.Hour - // commonFileStore implements the common Get, Put, Exists, Sign and Cleanup // operations typical for storage of files in the SoftwareInstallers S3 bucket // configuration. It is used by the SoftwareInstallerStore and the @@ -193,7 +191,7 @@ func (s *commonFileStore) Cleanup(ctx context.Context, usedFileIDs []string, rem return int(deleted.Load()), ctxerr.Wrapf(ctx, err, "deleting %s in S3 store", s.fileLabel) } -func (s *commonFileStore) Sign(ctx context.Context, fileID string) (string, error) { +func (s *commonFileStore) Sign(ctx context.Context, fileID string, expiresIn time.Duration) (string, error) { if s.cloudFrontConfig == nil { return "", ctxerr.Wrapf(ctx, fleet.ErrNotConfigured, "signing %s URL in S3 store", s.fileLabel) } @@ -202,7 +200,7 @@ func (s *commonFileStore) Sign(ctx context.Context, fileID string) (string, erro return "", ctxerr.Wrapf(ctx, err, "building URL for %s with ID %s in S3 store", s.fileLabel, fileID) } signer := sign.NewURLSigner(s.cloudFrontConfig.SigningPublicKeyID, s.cloudFrontConfig.Signer) - signedURL, err := signer.Sign(urlToAccess, time.Now().Add(signedURLExpiresIn)) + signedURL, err := signer.Sign(urlToAccess, time.Now().Add(expiresIn)) if err != nil { return "", ctxerr.Wrapf(ctx, err, "signing %s URL %s in S3 store", s.fileLabel, urlToAccess) } diff --git a/server/fleet/apple_mdm.go b/server/fleet/apple_mdm.go index 559bdbe4ae..755c1d5bc9 100644 --- a/server/fleet/apple_mdm.go +++ b/server/fleet/apple_mdm.go @@ -987,6 +987,8 @@ type MDMAppleDDMActivation struct { Type string `json:"Type"` // "com.apple.activation.simple" } +const BootstrapPackageSignedURLExpiry = 6 * time.Hour + // MDMBootstrapPackageStore is the interface to store and retrieve bootstrap // package files. Fleet supports storing to the database and to an S3 bucket. type MDMBootstrapPackageStore interface { @@ -994,7 +996,7 @@ type MDMBootstrapPackageStore interface { Put(ctx context.Context, packageID string, content io.ReadSeeker) error Exists(ctx context.Context, packageID string) (bool, error) Cleanup(ctx context.Context, usedPackageIDs []string, removeCreatedBefore time.Time) (int, error) - Sign(ctx context.Context, fileID string) (string, error) + Sign(ctx context.Context, fileID string, expiresIn time.Duration) (string, error) } // MDMAppleMachineInfo is a [device's information][1] sent as part of an MDM enrollment profile request diff --git a/server/fleet/in_house_apps.go b/server/fleet/in_house_apps.go index 0ccc18c56a..832d031b8f 100644 --- a/server/fleet/in_house_apps.go +++ b/server/fleet/in_house_apps.go @@ -1,5 +1,11 @@ package fleet +import ( + "time" +) + +const InHouseAppSignedURLExpiry = 5 * time.Minute + type InHouseAppPayload struct { TeamID *uint Title string // app name diff --git a/server/fleet/software_installer.go b/server/fleet/software_installer.go index 912e7b6417..54e3f207be 100644 --- a/server/fleet/software_installer.go +++ b/server/fleet/software_installer.go @@ -18,6 +18,7 @@ import ( // MaxSoftwareInstallerSize is the maximum size allowed for software // installers. This is enforced by the endpoints that upload installers. const MaxSoftwareInstallerSize = 3000 * units.MiB +const SoftwareInstallerSignedURLExpiry = 6 * time.Hour // SoftwareInstallerStore is the interface to store and retrieve software // installer files. Fleet supports storing to the local filesystem and to an @@ -27,7 +28,7 @@ type SoftwareInstallerStore interface { Put(ctx context.Context, installerID string, content io.ReadSeeker) error Exists(ctx context.Context, installerID string) (bool, error) Cleanup(ctx context.Context, usedInstallerIDs []string, removeCreatedBefore time.Time) (int, error) - Sign(ctx context.Context, fileID string) (string, error) + Sign(ctx context.Context, fileID string, expiresIn time.Duration) (string, error) } // SoftwareInstallDetails contains all of the information diff --git a/server/fleet/software_title_icons.go b/server/fleet/software_title_icons.go index cbb652b2cb..0200c68598 100644 --- a/server/fleet/software_title_icons.go +++ b/server/fleet/software_title_icons.go @@ -10,6 +10,8 @@ import ( var SoftwareTitleIconURLRegex = regexp.MustCompile(`fleet/software/titles/\d+/icon\?team_id=\d+`) +const SoftwareTitleIconSignedURLExpiry = 6 * time.Hour + type UploadSoftwareTitleIconPayload struct { TitleID uint TeamID uint @@ -42,7 +44,7 @@ type SoftwareTitleIconStore interface { Get(ctx context.Context, iconID string) (io.ReadCloser, int64, error) Exists(ctx context.Context, iconID string) (bool, error) Cleanup(ctx context.Context, usedIconIDs []string, removeCreatedBefore time.Time) (int, error) - Sign(ctx context.Context, iconID string) (string, error) + Sign(ctx context.Context, iconID string, expiresIn time.Duration) (string, error) } type DetailsForSoftwareIconActivity struct { diff --git a/server/mock/mdm/bootstrap_package_store.go b/server/mock/mdm/bootstrap_package_store.go index 3588b0c077..d9c0500c82 100644 --- a/server/mock/mdm/bootstrap_package_store.go +++ b/server/mock/mdm/bootstrap_package_store.go @@ -21,7 +21,7 @@ type ExistsFunc func(ctx context.Context, packageID string) (bool, error) type CleanupFunc func(ctx context.Context, usedPackageIDs []string, removeCreatedBefore time.Time) (int, error) -type SignFunc func(ctx context.Context, fileID string) (string, error) +type SignFunc func(ctx context.Context, fileID string, expiresIn time.Duration) (string, error) type MDMBootstrapPackageStore struct { GetFunc GetFunc @@ -70,9 +70,9 @@ func (s *MDMBootstrapPackageStore) Cleanup(ctx context.Context, usedPackageIDs [ return s.CleanupFunc(ctx, usedPackageIDs, removeCreatedBefore) } -func (s *MDMBootstrapPackageStore) Sign(ctx context.Context, fileID string) (string, error) { +func (s *MDMBootstrapPackageStore) Sign(ctx context.Context, fileID string, expiresIn time.Duration) (string, error) { s.mu.Lock() s.SignFuncInvoked = true s.mu.Unlock() - return s.SignFunc(ctx, fileID) + return s.SignFunc(ctx, fileID, expiresIn) } diff --git a/server/mock/software/software_installer_store.go b/server/mock/software/software_installer_store.go index 9901de56c1..87870cf79e 100644 --- a/server/mock/software/software_installer_store.go +++ b/server/mock/software/software_installer_store.go @@ -21,7 +21,7 @@ type ExistsFunc func(ctx context.Context, installerID string) (bool, error) type CleanupFunc func(ctx context.Context, usedInstallerIDs []string, removeCreatedBefore time.Time) (int, error) -type SignFunc func(ctx context.Context, fileID string) (string, error) +type SignFunc func(ctx context.Context, fileID string, expiresIn time.Duration) (string, error) type SoftwareInstallerStore struct { GetFunc GetFunc @@ -70,9 +70,9 @@ func (s *SoftwareInstallerStore) Cleanup(ctx context.Context, usedInstallerIDs [ return s.CleanupFunc(ctx, usedInstallerIDs, removeCreatedBefore) } -func (s *SoftwareInstallerStore) Sign(ctx context.Context, fileID string) (string, error) { +func (s *SoftwareInstallerStore) Sign(ctx context.Context, fileID string, expiresIn time.Duration) (string, error) { s.mu.Lock() s.SignFuncInvoked = true s.mu.Unlock() - return s.SignFunc(ctx, fileID) + return s.SignFunc(ctx, fileID, expiresIn) } diff --git a/server/service/integration_install_test.go b/server/service/integration_install_test.go index b77f341636..e298a77770 100644 --- a/server/service/integration_install_test.go +++ b/server/service/integration_install_test.go @@ -12,12 +12,14 @@ import ( "path/filepath" "strings" "testing" + "time" "github.com/fleetdm/fleet/v4/server/config" "github.com/fleetdm/fleet/v4/server/datastore/mysql" "github.com/fleetdm/fleet/v4/server/datastore/s3" "github.com/fleetdm/fleet/v4/server/fleet" software_mock "github.com/fleetdm/fleet/v4/server/mock/software" + "github.com/fleetdm/fleet/v4/server/ptr" "github.com/go-kit/log" kitlog "github.com/go-kit/log" "github.com/jmoiron/sqlx" @@ -105,7 +107,7 @@ func (s *integrationInstallTestSuite) TestSoftwareInstallerSignedURL() { myInstallerID = installerID return nil } - s.softwareInstallStore.SignFunc = func(ctx context.Context, fileID string) (string, error) { + s.softwareInstallStore.SignFunc = func(ctx context.Context, fileID string, expiresIn time.Duration) (string, error) { return "https://example.com/signed", nil } @@ -166,7 +168,7 @@ func (s *integrationInstallTestSuite) TestSoftwareInstallerSignedURL() { require.Equal(t, filename, orbitSoftwareResp.SoftwareInstallerURL.Filename) // Error in signing -- we simply don't return the URL - s.softwareInstallStore.SignFunc = func(ctx context.Context, fileID string) (string, error) { + s.softwareInstallStore.SignFunc = func(ctx context.Context, fileID string, expiresIn time.Duration) (string, error) { return "", errors.New("error signing") } orbitSoftwareResp = orbitGetSoftwareInstallResponse{} @@ -187,8 +189,8 @@ func (s *integrationInstallTestSuite) TestSoftwareInstallerSignedURL() { } s3Store, err := s3.NewTestSoftwareInstallerStore(s3Config) require.NoError(t, err) - s.softwareInstallStore.SignFunc = func(ctx context.Context, fileID string) (string, error) { - return s3Store.Sign(ctx, fileID) + s.softwareInstallStore.SignFunc = func(ctx context.Context, fileID string, expiresIn time.Duration) (string, error) { + return s3Store.Sign(ctx, fileID, fleet.SoftwareInstallerSignedURLExpiry) } s.DoJSON("POST", "/api/fleet/orbit/software_install/details", orbitGetSoftwareInstallRequest{ InstallUUID: installUUID, @@ -215,3 +217,48 @@ func getLatestSoftwareInstallExecID(t *testing.T, ds *mysql.Datastore, hostID ui }) return installUUID } + +func (s *integrationInstallTestSuite) TestGetInHouseAppManifestSignedURL() { + // Test that the signed URL is used if cloudfrontsigner is configured + t := s.T() + teamID := ptr.Uint(0) + + signURL := `https://example.cloudfront.net/software-installers/storage_id?Expires=1766462733&Signature=some_signature&Key-Pair-Id=ABC123XYZ` + + // Set up mocks + var myInstallerID string + s.softwareInstallStore.ExistsFunc = func(ctx context.Context, installerID string) (bool, error) { + return installerID == myInstallerID, nil + } + s.softwareInstallStore.PutFunc = func(ctx context.Context, installerID string, content io.ReadSeeker) error { + myInstallerID = installerID + return nil + } + s.softwareInstallStore.SignFunc = func(ctx context.Context, fileID string, expiresIn time.Duration) (string, error) { + return signURL, nil + } + + s.uploadSoftwareInstaller(t, &fleet.UploadSoftwareInstallerPayload{Filename: "ipa_test.ipa"}, http.StatusOK, "") + + var titleResp listSoftwareTitlesResponse + s.DoJSON("GET", "/api/latest/fleet/software/titles", listSoftwareTitlesRequest{ + SoftwareTitleListOptions: fleet.SoftwareTitleListOptions{Platform: "ios"}, + }, http.StatusOK, &titleResp, "team_id", "0") + require.Len(t, titleResp.SoftwareTitles, 1) + require.Equal(t, "ipa_test", titleResp.SoftwareTitles[0].Name) + titleID := titleResp.SoftwareTitles[0].ID + + readManifest := func(res *http.Response) []byte { + buf, err := io.ReadAll(res.Body) + require.NoError(t, err) + res.Body.Close() + return buf + } + res := s.DoRawNoAuth("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d/in_house_app/manifest?team_id=%d", titleID, *teamID), + jsonMustMarshal(t, getInHouseAppManifestRequest{TitleID: titleID, TeamID: teamID}), http.StatusOK) + + manifest := readManifest(res) + require.NotNil(t, manifest) + escapedURL := `https://example.cloudfront.net/software-installers/storage_id?Expires=1766462733&Signature=some_signature&Key-Pair-Id=ABC123XYZ` + require.Contains(t, string(manifest), escapedURL) +} diff --git a/server/service/integration_vpp_install_test.go b/server/service/integration_vpp_install_test.go index 33aa270c00..a0c98d4979 100644 --- a/server/service/integration_vpp_install_test.go +++ b/server/service/integration_vpp_install_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/base64" "fmt" + "io" "net/http" "net/http/httptest" "path/filepath" @@ -1719,6 +1720,36 @@ func (s *integrationMDMTestSuite) TestInHouseAppSelfInstall() { s.DoRawWithHeaders("POST", fmt.Sprintf("/api/v1/fleet/device/%s/software/install/%d", iosHost.UUID, titleID), nil, http.StatusAccepted, headers) } +func (s *integrationMDMTestSuite) TestGetInHouseAppManifestUnsignedURL() { + // Test that the Fleet URL is used if cloudfrontsigner is nil + t := s.T() + s.setSkipWorkerJobs(t) + teamID := ptr.Uint(0) + + s.uploadSoftwareInstaller(t, &fleet.UploadSoftwareInstallerPayload{Filename: "ipa_test.ipa"}, http.StatusOK, "") + + var titleResp listSoftwareTitlesResponse + s.DoJSON("GET", "/api/latest/fleet/software/titles", listSoftwareTitlesRequest{ + SoftwareTitleListOptions: fleet.SoftwareTitleListOptions{Platform: "ios"}, + }, http.StatusOK, &titleResp, "team_id", "0") + require.Len(t, titleResp.SoftwareTitles, 1) + require.Equal(t, "ipa_test", titleResp.SoftwareTitles[0].Name) + titleID := titleResp.SoftwareTitles[0].ID + + readManifest := func(res *http.Response) []byte { + buf, err := io.ReadAll(res.Body) + require.NoError(t, err) + res.Body.Close() + return buf + } + res := s.DoRawNoAuth("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d/in_house_app/manifest?team_id=%d", titleID, *teamID), + jsonMustMarshal(t, getInHouseAppManifestRequest{TitleID: titleID, TeamID: teamID}), http.StatusOK) + + manifest := readManifest(res) + require.NotNil(t, manifest) + require.Contains(t, string(manifest), fmt.Sprintf("/%d/in_house_app?team_id=%d", titleID, *teamID)) +} + func (s *integrationMDMTestSuite) addHostIdentityCertificate(hostUUID string, certSerial uint64) { t := s.T() s.setSkipWorkerJobs(t) diff --git a/server/worker/apple_mdm.go b/server/worker/apple_mdm.go index e4a930b176..3098e1f4d6 100644 --- a/server/worker/apple_mdm.go +++ b/server/worker/apple_mdm.go @@ -627,7 +627,7 @@ func (a *AppleMDM) getSignedURL(ctx context.Context, meta *fleet.MDMAppleBootstr var url string if a.BootstrapPackageStore != nil { pkgID := hex.EncodeToString(meta.Sha256) - signedURL, err := a.BootstrapPackageStore.Sign(ctx, pkgID) + signedURL, err := a.BootstrapPackageStore.Sign(ctx, pkgID, fleet.BootstrapPackageSignedURLExpiry) switch { case errors.Is(err, fleet.ErrNotConfigured): // no CDN configured, fall back to the MDM URL diff --git a/server/worker/apple_mdm_test.go b/server/worker/apple_mdm_test.go index 797ff9df9d..4f225d75fc 100644 --- a/server/worker/apple_mdm_test.go +++ b/server/worker/apple_mdm_test.go @@ -1267,14 +1267,14 @@ func TestGetSignedURL(t *testing.T) { // Signer not configured mockStore := &mock.MDMBootstrapPackageStore{} a.BootstrapPackageStore = mockStore - mockStore.SignFunc = func(ctx context.Context, fileID string) (string, error) { + mockStore.SignFunc = func(ctx context.Context, fileID string, expiresIn time.Duration) (string, error) { return "bozo", fleet.ErrNotConfigured } assert.Empty(t, a.getSignedURL(ctx, meta)) assert.Empty(t, buf.String()) // Test happy path - mockStore.SignFunc = func(ctx context.Context, fileID string) (string, error) { + mockStore.SignFunc = func(ctx context.Context, fileID string, expiresIn time.Duration) (string, error) { return "signed", nil } mockStore.ExistsFunc = func(ctx context.Context, packageID string) (bool, error) { @@ -1289,7 +1289,7 @@ func TestGetSignedURL(t *testing.T) { mockStore.ExistsFuncInvoked = false // Test error -- sign failed - mockStore.SignFunc = func(ctx context.Context, fileID string) (string, error) { + mockStore.SignFunc = func(ctx context.Context, fileID string, expiresIn time.Duration) (string, error) { return "", errors.New("test error") } assert.Empty(t, a.getSignedURL(ctx, meta))