fleet/server/service/software_titles.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

154 lines
4.6 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 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"`
}
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)
if err != nil {
return getSoftwareTitleResponse{Err: err}, nil
}
return getSoftwareTitleResponse{SoftwareTitle: software}, nil
}
func (svc *Service) SoftwareTitleByID(ctx context.Context, id uint) (*fleet.SoftwareTitle, 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
}
// get software by id including team_id data from software_title_host_counts
software, err := svc.ds.SoftwareTitleByID(ctx, id, 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 exists
filter := fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}
_, err = svc.ds.SoftwareTitleByID(ctx, id, filter)
if 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 title by id")
}
return software, nil
}