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.SoftwareTitle `json:"software_titles,omitempty"` Err error `json:"error,omitempty"` } func (r listSoftwareTitlesResponse) error() error { return r.Err } func listSoftwareTitlesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (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.IsZero() && sw.CountsUpdatedAt.After(latest) { latest = sw.CountsUpdatedAt } } 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.SoftwareTitle, int, *fleet.PaginationMetadata, error) { if err := svc.authz.Authorize(ctx, &fleet.AuthzSoftwareInventory{ TeamID: opt.TeamID, }, fleet.ActionRead); err != nil { return nil, 0, nil, err } if opt.TeamID != nil && *opt.TeamID != 0 { lic, err := svc.License(ctx) if err != nil { return nil, 0, nil, ctxerr.Wrap(ctx, err, "get license") } if !lic.IsPremium() { 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) (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 { // 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") } return software, nil }