diff --git a/ee/server/service/software_installers.go b/ee/server/service/software_installers.go index bb8e51234c..5e72681862 100644 --- a/ee/server/service/software_installers.go +++ b/ee/server/service/software_installers.go @@ -72,6 +72,7 @@ func (svc *Service) UploadSoftwareInstaller(ctx context.Context, payload *fleet. SoftwarePackage: payload.Filename, TeamName: teamName, TeamID: payload.TeamID, + SelfService: payload.SelfService, }); err != nil { return ctxerr.Wrap(ctx, err, "creating activity for added software") } @@ -116,6 +117,7 @@ func (svc *Service) DeleteSoftwareInstaller(ctx context.Context, titleID uint, t SoftwarePackage: meta.Name, TeamName: teamName, TeamID: meta.TeamID, + SelfService: meta.SelfService, }); err != nil { return ctxerr.Wrap(ctx, err, "creating activity for deleted software") } @@ -453,6 +455,7 @@ func (svc *Service) BatchSetSoftwareInstallers(ctx context.Context, tmName strin PreInstallQuery: p.PreInstallQuery, PostInstallScript: p.PostInstallScript, InstallerFile: bytes.NewReader(bodyBytes), + SelfService: p.SelfService, } // set the filename before adding metadata, as it is used as fallback diff --git a/server/datastore/mysql/software_installers.go b/server/datastore/mysql/software_installers.go index 8eef7a7374..3cc7d36d2b 100644 --- a/server/datastore/mysql/software_installers.go +++ b/server/datastore/mysql/software_installers.go @@ -104,8 +104,9 @@ INSERT INTO software_installers ( install_script_content_id, pre_install_query, post_install_script_content_id, - platform -) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + platform, + self_service +) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` args := []interface{}{ payload.TeamID, @@ -118,6 +119,7 @@ INSERT INTO software_installers ( payload.PreInstallQuery, postInstallScriptID, payload.Platform, + payload.SelfService, } res, err := ds.writer(ctx).ExecContext(ctx, stmt, args...) @@ -204,6 +206,7 @@ SELECT si.pre_install_query, si.post_install_script_content_id, si.uploaded_at, + si.self_service, COALESCE(st.name, '') AS software_title %s FROM @@ -299,7 +302,8 @@ SELECT h.team_id AS host_team_id, hsi.user_id AS user_id, hsi.post_install_script_exit_code, - hsi.install_script_exit_code + hsi.install_script_exit_code, + hsi.self_service FROM host_software_installs hsi JOIN hosts h ON h.id = hsi.host_id diff --git a/server/fleet/activities.go b/server/fleet/activities.go index 5646802e46..4b05ad1951 100644 --- a/server/fleet/activities.go +++ b/server/fleet/activities.go @@ -1455,6 +1455,7 @@ type ActivityTypeAddedSoftware struct { SoftwarePackage string `json:"software_package"` TeamName *string `json:"team_name"` TeamID *uint `json:"team_id"` + SelfService bool `json:"self_service"` } func (a ActivityTypeAddedSoftware) ActivityName() string { @@ -1477,6 +1478,7 @@ func (a ActivityTypeAddedSoftware) Documentation() (string, string, string) { } type ActivityTypeDeletedSoftware struct { + SelfService bool `json:"self_service"` SoftwareTitle string `json:"software_title"` SoftwarePackage string `json:"software_package"` TeamName *string `json:"team_name"` diff --git a/server/fleet/scripts.go b/server/fleet/scripts.go index 70ccb510d3..6f803f28c1 100644 --- a/server/fleet/scripts.go +++ b/server/fleet/scripts.go @@ -363,6 +363,7 @@ type SoftwareInstallerPayload struct { PreInstallQuery string `json:"pre_install_query"` InstallScript string `json:"install_script"` PostInstallScript string `json:"post_install_script"` + SelfService bool `json:"self_service"` } type HostLockWipeStatus struct { diff --git a/server/fleet/software_installer.go b/server/fleet/software_installer.go index 068e539fc6..6f6073e2fd 100644 --- a/server/fleet/software_installer.go +++ b/server/fleet/software_installer.go @@ -95,6 +95,9 @@ type SoftwareInstaller struct { Status *SoftwareInstallerStatusSummary `json:"status,omitempty" db:"-"` // SoftwareTitle is the title of the software pointed installed by this installer. SoftwareTitle string `json:"-" db:"software_title"` + // SelfService indicates that the software can be installed by the + // end user without admin intervention + SelfService bool `json:"-" db:"self_service"` } // AuthzType implements authz.AuthzTyper. @@ -175,6 +178,9 @@ type HostSoftwareInstallerResult struct { InstallScriptExitCode *int `json:"-" db:"install_script_exit_code"` // PostInstallScriptExitCode is used internally to determine the output displayed to the user. PostInstallScriptExitCode *int `json:"-" db:"post_install_script_exit_code"` + // SelfService indicates that the installation was queued by the + // end user and not an administrator + SelfService bool `json:"self_service" db:"self_service"` } const ( @@ -252,6 +258,7 @@ type UploadSoftwareInstallerPayload struct { Version string Source string Platform string + SelfService bool } // DownloadSoftwareInstallerPayload is the payload for downloading a software installer. diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index e5a7aca8b7..4f1d1a112b 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -8970,7 +8970,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndD s.uploadSoftwareInstaller(payload, http.StatusOK, "") // check activity - s.lastActivityOfTypeMatches(fleet.ActivityTypeAddedSoftware{}.ActivityName(), `{"software_title": "ruby", "software_package": "ruby.deb", "team_name": null, "team_id": null}`, 0) + s.lastActivityOfTypeMatches(fleet.ActivityTypeAddedSoftware{}.ActivityName(), `{"software_title": "ruby", "software_package": "ruby.deb", "team_name": null, "team_id": null, "self_service": false}`, 0) // check the software installer _, titleID := checkSoftwareInstaller(t, payload) @@ -9005,11 +9005,12 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndD PostInstallScript: "another post install script", Filename: "ruby.deb", // additional fields below are pre-populated so we can re-use the payload later for the test assertions - Title: "ruby", - Version: "1:2.5.1", - Source: "deb_packages", - StorageID: "df06d9ce9e2090d9cb2e8cd1f4d7754a803dc452bf93e3204e3acd3b95508628", - Platform: "linux", + Title: "ruby", + Version: "1:2.5.1", + Source: "deb_packages", + StorageID: "df06d9ce9e2090d9cb2e8cd1f4d7754a803dc452bf93e3204e3acd3b95508628", + Platform: "linux", + SelfService: true, } s.uploadSoftwareInstaller(payload, http.StatusOK, "") @@ -9017,7 +9018,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndD installerID, titleID := checkSoftwareInstaller(t, payload) // check activity - s.lastActivityOfTypeMatches(fleet.ActivityTypeAddedSoftware{}.ActivityName(), fmt.Sprintf(`{"software_title": "ruby", "software_package": "ruby.deb", "team_name": "%s", "team_id": %d}`, createTeamResp.Team.Name, createTeamResp.Team.ID), 0) + s.lastActivityOfTypeMatches(fleet.ActivityTypeAddedSoftware{}.ActivityName(), fmt.Sprintf(`{"software_title": "ruby", "software_package": "ruby.deb", "team_name": "%s", "team_id": %d, "self_service": true}`, createTeamResp.Team.Name, createTeamResp.Team.ID), 0) // upload again fails s.uploadSoftwareInstaller(payload, http.StatusConflict, "already exists") @@ -9057,7 +9058,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndD s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/software/%d/package", titleID), nil, http.StatusNoContent, "team_id", fmt.Sprintf("%d", *payload.TeamID)) // check activity - s.lastActivityOfTypeMatches(fleet.ActivityTypeDeletedSoftware{}.ActivityName(), fmt.Sprintf(`{"software_title": "ruby", "software_package": "ruby.deb", "team_name": "%s", "team_id": %d}`, createTeamResp.Team.Name, createTeamResp.Team.ID), 0) + s.lastActivityOfTypeMatches(fleet.ActivityTypeDeletedSoftware{}.ActivityName(), fmt.Sprintf(`{"software_title": "ruby", "software_package": "ruby.deb", "team_name": "%s", "team_id": %d, "self_service": true}`, createTeamResp.Team.Name, createTeamResp.Team.ID), 0) }) } @@ -9714,6 +9715,9 @@ func (s *integrationEnterpriseTestSuite) uploadSoftwareInstaller(payload *fleet. require.NoError(t, w.WriteField("install_script", payload.InstallScript)) require.NoError(t, w.WriteField("pre_install_query", payload.PreInstallQuery)) require.NoError(t, w.WriteField("post_install_script", payload.PostInstallScript)) + if payload.SelfService { + require.NoError(t, w.WriteField("self_service", "true")) + } w.Close() diff --git a/server/service/software_installers.go b/server/service/software_installers.go index 9a51b7f5f4..b56473c13b 100644 --- a/server/service/software_installers.go +++ b/server/service/software_installers.go @@ -20,6 +20,7 @@ type uploadSoftwareInstallerRequest struct { InstallScript string PreInstallQuery string PostInstallScript string + SelfService bool } type uploadSoftwareInstallerResponse struct { @@ -79,6 +80,15 @@ func (uploadSoftwareInstallerRequest) DecodeRequest(ctx context.Context, r *http decoded.PostInstallScript = val[0] } + val, ok = r.MultipartForm.Value["self_service"] + if ok && len(val) > 0 && val[0] != "" { + parsed, err := strconv.ParseBool(val[0]) + if err != nil { + return nil, &fleet.BadRequestError{Message: fmt.Sprintf("failed to decode self_service bool in multipart form: %s", err.Error())} + } + decoded.SelfService = parsed + } + return &decoded, nil } @@ -99,6 +109,7 @@ func uploadSoftwareInstallerEndpoint(ctx context.Context, request interface{}, s PostInstallScript: req.PostInstallScript, InstallerFile: ff, Filename: req.File.Filename, + SelfService: req.SelfService, } if err := svc.UploadSoftwareInstaller(ctx, payload); err != nil {