mirror of
https://github.com/fleetdm/fleet
synced 2026-05-06 06:48:54 +00:00
For #26933. # Checklist for submitter If some of the following don't apply, delete the relevant line. <!-- Note that API documentation changes are now addressed by the product design team. --> - [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/Committing-Changes.md#changes-files) for more information. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [x] Added/updated automated tests - [x] A detailed QA plan exists on the associated ticket (if it isn't there, work with the product group's QA engineer to add it) - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Rachael Shaw <r@rachael.wtf>
269 lines
8.6 KiB
Go
269 lines
8.6 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"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/ptr"
|
|
)
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
// List Software Titles
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type listSoftwareTitlesRequest struct {
|
|
fleet.SoftwareTitleListOptions
|
|
}
|
|
|
|
type listSoftwareTitlesResponse struct {
|
|
Meta *fleet.PaginationMetadata `json:"meta"`
|
|
Count int `json:"count"`
|
|
CountsUpdatedAt *time.Time `json:"counts_updated_at"`
|
|
SoftwareTitles []fleet.SoftwareTitleListResult `json:"software_titles"`
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r listSoftwareTitlesResponse) Error() error { return r.Err }
|
|
|
|
func listSoftwareTitlesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*listSoftwareTitlesRequest)
|
|
titles, count, meta, err := svc.ListSoftwareTitles(ctx, req.SoftwareTitleListOptions)
|
|
if err != nil {
|
|
return listSoftwareTitlesResponse{Err: err}, nil
|
|
}
|
|
|
|
var latest time.Time
|
|
for _, sw := range titles {
|
|
if sw.CountsUpdatedAt != nil && !sw.CountsUpdatedAt.IsZero() && sw.CountsUpdatedAt.After(latest) {
|
|
latest = *sw.CountsUpdatedAt
|
|
}
|
|
// we dont want to include the InstallDuringSetup field in the response
|
|
// for software titles list.
|
|
if sw.SoftwarePackage != nil {
|
|
sw.SoftwarePackage.InstallDuringSetup = nil
|
|
} else if sw.AppStoreApp != nil {
|
|
sw.AppStoreApp.InstallDuringSetup = nil
|
|
}
|
|
}
|
|
if len(titles) == 0 {
|
|
titles = []fleet.SoftwareTitleListResult{}
|
|
}
|
|
listResp := listSoftwareTitlesResponse{
|
|
SoftwareTitles: titles,
|
|
Count: count,
|
|
Meta: meta,
|
|
}
|
|
if !latest.IsZero() {
|
|
listResp.CountsUpdatedAt = &latest
|
|
}
|
|
|
|
return listResp, nil
|
|
}
|
|
|
|
func (svc *Service) ListSoftwareTitles(
|
|
ctx context.Context,
|
|
opt fleet.SoftwareTitleListOptions,
|
|
) ([]fleet.SoftwareTitleListResult, int, *fleet.PaginationMetadata, error) {
|
|
if err := svc.authz.Authorize(ctx, &fleet.AuthzSoftwareInventory{
|
|
TeamID: opt.TeamID,
|
|
}, fleet.ActionRead); err != nil {
|
|
return nil, 0, nil, err
|
|
}
|
|
|
|
lic, err := svc.License(ctx)
|
|
if err != nil {
|
|
return nil, 0, nil, ctxerr.Wrap(ctx, err, "get license")
|
|
}
|
|
|
|
if opt.TeamID != nil && *opt.TeamID != 0 && !lic.IsPremium() {
|
|
return nil, 0, nil, fleet.ErrMissingLicense
|
|
}
|
|
|
|
if !lic.IsPremium() && (opt.MaximumCVSS > 0 || opt.MinimumCVSS > 0 || opt.KnownExploit) {
|
|
return nil, 0, nil, fleet.ErrMissingLicense
|
|
}
|
|
|
|
// always include metadata for software titles
|
|
opt.ListOptions.IncludeMetadata = true
|
|
// cursor-based pagination is not supported for software titles
|
|
opt.ListOptions.After = ""
|
|
|
|
vc, ok := viewer.FromContext(ctx)
|
|
if !ok {
|
|
return nil, 0, nil, fleet.ErrNoContext
|
|
}
|
|
|
|
titles, count, meta, err := svc.ds.ListSoftwareTitles(ctx, opt, fleet.TeamFilter{
|
|
User: vc.User,
|
|
IncludeObserver: true,
|
|
TeamID: opt.TeamID,
|
|
})
|
|
if err != nil {
|
|
return nil, 0, nil, err
|
|
}
|
|
|
|
return titles, count, meta, nil
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
// Get a Software Title
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type getSoftwareTitleRequest struct {
|
|
ID uint `url:"id"`
|
|
TeamID *uint `query:"team_id,optional"`
|
|
}
|
|
|
|
type getSoftwareTitleResponse struct {
|
|
SoftwareTitle *fleet.SoftwareTitle `json:"software_title,omitempty"`
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r getSoftwareTitleResponse) Error() error { return r.Err }
|
|
|
|
func getSoftwareTitleEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*getSoftwareTitleRequest)
|
|
|
|
software, err := svc.SoftwareTitleByID(ctx, req.ID, req.TeamID)
|
|
if err != nil {
|
|
return getSoftwareTitleResponse{Err: err}, nil
|
|
}
|
|
|
|
return getSoftwareTitleResponse{SoftwareTitle: software}, nil
|
|
}
|
|
|
|
func (svc *Service) SoftwareTitleByID(ctx context.Context, id uint, teamID *uint) (*fleet.SoftwareTitle, error) {
|
|
if err := svc.authz.Authorize(ctx, &fleet.Host{TeamID: teamID}, fleet.ActionList); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if teamID != nil && *teamID != 0 {
|
|
// This auth check ensures we return 403 if the user doesn't have access to the team
|
|
if err := svc.authz.Authorize(ctx, &fleet.AuthzSoftwareInventory{TeamID: teamID}, fleet.ActionRead); err != nil {
|
|
return nil, err
|
|
}
|
|
exists, err := svc.ds.TeamExists(ctx, *teamID)
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "checking if team exists")
|
|
} else if !exists {
|
|
return nil, fleet.NewInvalidArgumentError("team_id", fmt.Sprintf("team %d does not exist", *teamID)).
|
|
WithStatus(http.StatusNotFound)
|
|
}
|
|
}
|
|
|
|
vc, ok := viewer.FromContext(ctx)
|
|
if !ok {
|
|
return nil, fleet.ErrNoContext
|
|
}
|
|
|
|
// get software by id including team_id data from software_title_host_counts
|
|
software, err := svc.ds.SoftwareTitleByID(ctx, id, teamID, fleet.TeamFilter{
|
|
User: vc.User,
|
|
IncludeObserver: true,
|
|
})
|
|
if err != nil {
|
|
if fleet.IsNotFound(err) && teamID == nil {
|
|
// here we use a global admin as filter because we want to check if the software exists
|
|
filter := fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}
|
|
_, err = svc.ds.SoftwareTitleByID(ctx, id, nil, filter)
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "checked using a global admin")
|
|
}
|
|
|
|
return nil, fleet.NewPermissionError("Error: You don't have permission to view specified software. It is installed on hosts that belong to team you don't have permissions to view.")
|
|
}
|
|
return nil, ctxerr.Wrap(ctx, err, "getting software title by id")
|
|
}
|
|
|
|
license, err := svc.License(ctx)
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "get license")
|
|
}
|
|
if license.IsPremium() {
|
|
// add software installer data if needed
|
|
if software.SoftwareInstallersCount > 0 {
|
|
meta, err := svc.ds.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, teamID, id, true)
|
|
if err != nil && !fleet.IsNotFound(err) {
|
|
return nil, ctxerr.Wrap(ctx, err, "get software installer metadata")
|
|
}
|
|
if meta != nil {
|
|
summary, err := svc.ds.GetSummaryHostSoftwareInstalls(ctx, meta.InstallerID)
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "get software installer status summary")
|
|
}
|
|
meta.Status = summary
|
|
}
|
|
software.SoftwarePackage = meta
|
|
}
|
|
|
|
// add VPP app data if needed
|
|
if software.VPPAppsCount > 0 {
|
|
meta, err := svc.ds.GetVPPAppMetadataByTeamAndTitleID(ctx, teamID, id)
|
|
if err != nil && !fleet.IsNotFound(err) {
|
|
return nil, ctxerr.Wrap(ctx, err, "get VPP app metadata")
|
|
}
|
|
if meta != nil {
|
|
summary, err := svc.ds.GetSummaryHostVPPAppInstalls(ctx, teamID, meta.VPPAppID)
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "get VPP app status summary")
|
|
}
|
|
meta.Status = summary
|
|
}
|
|
software.AppStoreApp = meta
|
|
}
|
|
}
|
|
|
|
return software, nil
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
// Update a software title's name
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type updateSoftwareNameRequest struct {
|
|
ID uint `url:"id"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
type updateSoftwareNameResponse struct {
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r updateSoftwareNameResponse) Error() error { return r.Err }
|
|
func (r updateSoftwareNameResponse) Status() int { return http.StatusResetContent }
|
|
|
|
func updateSoftwareNameEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*updateSoftwareNameRequest)
|
|
return updateSoftwareNameResponse{Err: svc.UpdateSoftwareName(ctx, req.ID, req.Name)}, nil
|
|
}
|
|
|
|
func (svc *Service) UpdateSoftwareName(ctx context.Context, titleID uint, name string) error {
|
|
if err := svc.authz.Authorize(ctx, &fleet.AuthzSoftwareInventory{}, fleet.ActionWrite); err != nil {
|
|
return err
|
|
}
|
|
vc, ok := viewer.FromContext(ctx)
|
|
if !ok {
|
|
return fleet.ErrNoContext
|
|
}
|
|
|
|
// get software by id including team_id data from software_title_host_counts
|
|
software, err := svc.ds.SoftwareTitleByID(ctx, titleID, nil, fleet.TeamFilter{
|
|
User: vc.User,
|
|
})
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "getting software title by id")
|
|
}
|
|
if software.BundleIdentifier == nil || *software.BundleIdentifier == "" {
|
|
return fleet.NewInvalidArgumentError("id", "only titles with a bundle ID can have their name modified")
|
|
}
|
|
if name == "" {
|
|
return fleet.NewInvalidArgumentError("name", "cannot be empty")
|
|
}
|
|
|
|
return svc.ds.UpdateSoftwareTitleName(ctx, titleID, name)
|
|
}
|