fleet/server/service/software.go
Gabriel Hernandez 119d1df76f
add permission check to software titles/versions endpoints (#16561)
relates to #16052

This adds a team permission check the `GET software/titles/:id`
endpoint. If the user should not be able to get the software title if it
is not on a host that is on the same team as the user (e.g. software
title 1 is on host 1, which is on team 1. A user who is only on team 2
should get a 403 response)

The UI is also updated to show the access denied error page when the we
receive a 403 response for the software title

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [x] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#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 tests
- [x] Manual QA for all new/changed functionality

---------

Co-authored-by: Roberto Dip <dip.jesusr@gmail.com>
Co-authored-by: Roberto Dip <me@roperzh.com>
2024-02-15 17:22:27 -03:00

213 lines
6.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service
import (
"context"
"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
/////////////////////////////////////////////////////////////////////////////////
type listSoftwareRequest struct {
fleet.SoftwareListOptions
}
// DEPRECATED: listSoftwareResponse is the response struct for the deprecated
// listSoftwareEndpoint. It differs from listSoftwareVersionsResponse in that
// the latter includes a count of the total number of software items.
type listSoftwareResponse struct {
CountsUpdatedAt *time.Time `json:"counts_updated_at"`
Software []fleet.Software `json:"software,omitempty"`
Err error `json:"error,omitempty"`
}
func (r listSoftwareResponse) error() error { return r.Err }
// DEPRECATED: use listSoftwareVersionsEndpoint instead
func listSoftwareEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*listSoftwareRequest)
resp, _, err := svc.ListSoftware(ctx, req.SoftwareListOptions)
if err != nil {
return listSoftwareResponse{Err: err}, nil
}
// calculate the latest counts_updated_at
var latest time.Time
for _, sw := range resp {
if !sw.CountsUpdatedAt.IsZero() && sw.CountsUpdatedAt.After(latest) {
latest = sw.CountsUpdatedAt
}
}
listResp := listSoftwareResponse{Software: resp}
if !latest.IsZero() {
listResp.CountsUpdatedAt = &latest
}
return listResp, nil
}
type listSoftwareVersionsResponse struct {
Count int `json:"count"`
CountsUpdatedAt *time.Time `json:"counts_updated_at"`
Software []fleet.Software `json:"software,omitempty"`
Meta *fleet.PaginationMetadata `json:"meta"`
Err error `json:"error,omitempty"`
}
func (r listSoftwareVersionsResponse) error() error { return r.Err }
func listSoftwareVersionsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*listSoftwareRequest)
// always include pagination for new software versions endpoint (not included by default in
// legacy endpoint for backwards compatibility)
req.SoftwareListOptions.ListOptions.IncludeMetadata = true
resp, meta, err := svc.ListSoftware(ctx, req.SoftwareListOptions)
if err != nil {
return listSoftwareVersionsResponse{Err: err}, nil
}
// calculate the latest counts_updated_at
var latest time.Time
for _, sw := range resp {
if !sw.CountsUpdatedAt.IsZero() && sw.CountsUpdatedAt.After(latest) {
latest = sw.CountsUpdatedAt
}
}
listResp := listSoftwareVersionsResponse{Software: resp, Meta: meta}
if !latest.IsZero() {
listResp.CountsUpdatedAt = &latest
}
c, err := svc.CountSoftware(ctx, req.SoftwareListOptions)
if err != nil {
return listSoftwareVersionsResponse{Err: err}, nil
}
listResp.Count = c
return listResp, nil
}
func (svc *Service) ListSoftware(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, *fleet.PaginationMetadata, error) {
if err := svc.authz.Authorize(ctx, &fleet.AuthzSoftwareInventory{
TeamID: opt.TeamID,
}, fleet.ActionRead); err != nil {
return nil, nil, err
}
// default sort order to hosts_count descending
if opt.ListOptions.OrderKey == "" {
opt.ListOptions.OrderKey = "hosts_count"
opt.ListOptions.OrderDirection = fleet.OrderDescending
}
opt.WithHostCounts = true
softwares, meta, err := svc.ds.ListSoftware(ctx, opt)
if err != nil {
return nil, nil, err
}
return softwares, meta, nil
}
/////////////////////////////////////////////////////////////////////////////////
// Get Software
/////////////////////////////////////////////////////////////////////////////////
type getSoftwareRequest struct {
ID uint `url:"id"`
}
type getSoftwareResponse struct {
Software *fleet.Software `json:"software,omitempty"`
Err error `json:"error,omitempty"`
}
func (r getSoftwareResponse) error() error { return r.Err }
func getSoftwareEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*getSoftwareRequest)
software, err := svc.SoftwareByID(ctx, req.ID, false)
if err != nil {
return getSoftwareResponse{Err: err}, nil
}
return getSoftwareResponse{Software: software}, nil
}
func (svc *Service) SoftwareByID(ctx context.Context, id uint, includeCVEScores bool) (*fleet.Software, error) {
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
return nil, err
}
vc, ok := viewer.FromContext(ctx)
if !ok {
return nil, fleet.ErrNoContext
}
software, err := svc.ds.SoftwareByID(ctx, id, includeCVEScores, &fleet.TeamFilter{
User: vc.User,
IncludeObserver: true,
})
if err != nil {
if fleet.IsNotFound(err) {
// here we use a global admin as filter because we want
// to check if the software version exists
filter := fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}
if _, err = svc.ds.SoftwareByID(ctx, id, includeCVEScores, &filter); err != nil {
return nil, ctxerr.Wrap(ctx, err, "checked using a global admin")
}
return nil, fleet.NewPermissionError("Error: You dont have permission to view specified software. It is installed on hosts that belong to team you dont have permissions to view.")
}
return nil, ctxerr.Wrap(ctx, err, "getting software version by id")
}
return software, nil
}
/////////////////////////////////////////////////////////////////////////////////
// Count
/////////////////////////////////////////////////////////////////////////////////
type countSoftwareRequest struct {
fleet.SoftwareListOptions
}
type countSoftwareResponse struct {
Count int `json:"count"`
Err error `json:"error,omitempty"`
}
func (r countSoftwareResponse) error() error { return r.Err }
// DEPRECATED: counts are now included directly in the listSoftwareVersionsResponse. This
// endpoint is retained for backwards compatibility.
func countSoftwareEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*countSoftwareRequest)
count, err := svc.CountSoftware(ctx, req.SoftwareListOptions)
if err != nil {
return countSoftwareResponse{Err: err}, nil
}
return countSoftwareResponse{Count: count}, nil
}
func (svc Service) CountSoftware(ctx context.Context, opt fleet.SoftwareListOptions) (int, error) {
if err := svc.authz.Authorize(ctx, &fleet.AuthzSoftwareInventory{
TeamID: opt.TeamID,
}, fleet.ActionRead); err != nil {
return 0, err
}
return svc.ds.CountSoftware(ctx, opt)
}