Files
fleet/server/service/maintained_apps.go
T
Scott GressandCopilot Autofix powered by AI 4daed869ad Add API param linter (#44045)
Adds a linter to ensure we don't add new instances of `team` or `query`
in API params. This will be used incrementally, but this PR also adds
`nolint` directives to places that still have these terms, both to avoid
false-positives later and to help with full migration away from these
terms in in Fleet 5.

Example:

```
server/fleet/campaigns.go:51:16: json tag "team_id": uses deprecated "team"/"teams" — use "fleet"/"fleets" instead (apiparamcheck)
        Team  *uint   `json:"team_id,omitempty"`
```

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a new static analyzer (apiparamcheck) to flag deprecated API
parameter names ("team/teams") and improper usages of "query/queries".

* **Chores**
  * Integrated the new check into CI tooling and configuration.
  * Added analyzer tests and plugin registration.
* Applied targeted lint-suppression annotations across code and tests
where legacy parameter names must remain.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-11 08:46:02 -05:00

196 lines
7.1 KiB
Go

package service
import (
"context"
"encoding/json"
"errors"
"net/http"
"github.com/fleetdm/fleet/v4/server/contexts/logging"
"github.com/fleetdm/fleet/v4/server/fleet"
maintained_apps "github.com/fleetdm/fleet/v4/server/mdm/maintainedapps"
platform_logging "github.com/fleetdm/fleet/v4/server/platform/logging"
)
type addFleetMaintainedAppRequest struct {
TeamID *uint `json:"team_id"` //nolint:apiparamcheck // alias handled manually via sibling FleetID field (see DecodeRequest below)
// Note that we're adding an explicit FleetID field rather than using `renameto`.
// The POST /software/fleet_maintained_apps endpoint has a custom decoder
// and in this special case it's easier to handle the aliasing manually.
FleetID *uint `json:"fleet_id"`
AppID uint `json:"fleet_maintained_app_id"`
InstallScript string `json:"install_script"`
PreInstallQuery string `json:"pre_install_query"` //nolint:apiparamcheck
PostInstallScript string `json:"post_install_script"`
SelfService bool `json:"self_service"`
UninstallScript string `json:"uninstall_script"`
LabelsIncludeAny []string `json:"labels_include_any"`
LabelsExcludeAny []string `json:"labels_exclude_any"`
LabelsIncludeAll []string `json:"labels_include_all"`
AutomaticInstall bool `json:"automatic_install"`
Categories []string `json:"categories"`
}
// DecodeRequest implements the RequestDecoder interface to support base64-encoded
// script fields. This allows bypassing WAF rules that may block requests containing
// shell/PowerShell script patterns. When the X-Fleet-Scripts-Encoded header is set
// to "base64", the script fields are decoded from base64.
func (addFleetMaintainedAppRequest) DecodeRequest(ctx context.Context, r *http.Request) (any, error) {
var req addFleetMaintainedAppRequest
// Decode JSON body
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, &fleet.BadRequestError{
Message: "failed to decode request body",
InternalErr: err,
}
}
// Resolve fleet_id → team_id aliasing. The struct has both fields so
// json.Decode populates whichever the caller sent; we normalize here.
if req.FleetID != nil {
if req.TeamID != nil {
return nil, &fleet.BadRequestError{
Message: `Specify only one of "team_id" or "fleet_id"`,
}
}
req.TeamID = req.FleetID
req.FleetID = nil
} else if req.TeamID != nil && platform_logging.TopicEnabled(platform_logging.DeprecatedFieldTopic) {
// Add a deprecation warning.
logging.WithExtras(ctx,
"deprecated_fields", "[team_id]",
"deprecation_warning", "use the updated field names (fleet_id) instead",
)
}
// Check if scripts are base64 encoded
if isScriptsEncoded(r) {
var err error
if req.InstallScript, err = decodeBase64Script(req.InstallScript); err != nil {
return nil, fleet.NewInvalidArgumentError("install_script", "invalid base64 encoding")
}
if req.UninstallScript, err = decodeBase64Script(req.UninstallScript); err != nil {
return nil, fleet.NewInvalidArgumentError("uninstall_script", "invalid base64 encoding")
}
if req.PostInstallScript, err = decodeBase64Script(req.PostInstallScript); err != nil {
return nil, fleet.NewInvalidArgumentError("post_install_script", "invalid base64 encoding")
}
if req.PreInstallQuery, err = decodeBase64Script(req.PreInstallQuery); err != nil {
return nil, fleet.NewInvalidArgumentError("pre_install_query", "invalid base64 encoding")
}
}
return &req, nil
}
type addFleetMaintainedAppResponse struct {
SoftwareTitleID uint `json:"software_title_id,omitempty"`
Err error `json:"error,omitempty"`
}
func (r addFleetMaintainedAppResponse) Error() error { return r.Err }
func addFleetMaintainedAppEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
req := request.(*addFleetMaintainedAppRequest)
ctx, cancel := context.WithTimeout(ctx, maintained_apps.InstallerTimeout)
defer cancel()
titleId, err := svc.AddFleetMaintainedApp(
ctx,
req.TeamID,
req.AppID,
req.InstallScript,
req.PreInstallQuery,
req.PostInstallScript,
req.UninstallScript,
req.SelfService,
req.AutomaticInstall,
req.LabelsIncludeAny,
req.LabelsExcludeAny,
req.LabelsIncludeAll,
)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
err = fleet.NewGatewayTimeoutError("Couldn't add. Request timeout. Please make sure your server and load balancer timeout is long enough.", err)
}
return &addFleetMaintainedAppResponse{Err: err}, nil
}
return &addFleetMaintainedAppResponse{SoftwareTitleID: titleId}, nil
}
func (svc *Service) AddFleetMaintainedApp(ctx context.Context, _ *uint, _ uint, _, _, _, _ string, _ bool, _ bool, _, _, _ []string) (uint, error) {
// skipauth: No authorization check needed due to implementation returning
// only license error.
svc.authz.SkipAuthorization(ctx)
return 0, fleet.ErrMissingLicense
}
type listFleetMaintainedAppsRequest struct {
fleet.ListOptions
TeamID *uint `query:"team_id,optional" renameto:"fleet_id"`
}
type listFleetMaintainedAppsResponse struct {
FleetMaintainedApps []fleet.MaintainedApp `json:"fleet_maintained_apps"`
Meta *fleet.PaginationMetadata `json:"meta"`
Err error `json:"error,omitempty"`
}
func (r listFleetMaintainedAppsResponse) Error() error { return r.Err }
func listFleetMaintainedAppsEndpoint(ctx context.Context, request any, svc fleet.Service) (fleet.Errorer, error) {
req := request.(*listFleetMaintainedAppsRequest)
apps, meta, err := svc.ListFleetMaintainedApps(ctx, req.TeamID, req.ListOptions)
if err != nil {
return listFleetMaintainedAppsResponse{Err: err}, nil
}
listResp := listFleetMaintainedAppsResponse{
FleetMaintainedApps: apps,
Meta: meta,
}
return listResp, nil
}
func (svc *Service) ListFleetMaintainedApps(ctx context.Context, teamID *uint, opts fleet.ListOptions) ([]fleet.MaintainedApp, *fleet.PaginationMetadata, error) {
// skipauth: No authorization check needed due to implementation returning
// only license error.
svc.authz.SkipAuthorization(ctx)
return nil, nil, fleet.ErrMissingLicense
}
type getFleetMaintainedAppRequest struct {
AppID uint `url:"app_id"`
TeamID *uint `query:"team_id,optional" renameto:"fleet_id"`
}
type getFleetMaintainedAppResponse struct {
FleetMaintainedApp *fleet.MaintainedApp `json:"fleet_maintained_app"`
Err error `json:"error,omitempty"`
}
func (r getFleetMaintainedAppResponse) Error() error { return r.Err }
func getFleetMaintainedApp(ctx context.Context, request any, svc fleet.Service) (fleet.Errorer, error) {
req := request.(*getFleetMaintainedAppRequest)
app, err := svc.GetFleetMaintainedApp(ctx, req.AppID, req.TeamID)
if err != nil {
return getFleetMaintainedAppResponse{Err: err}, nil
}
return getFleetMaintainedAppResponse{FleetMaintainedApp: app}, nil
}
func (svc *Service) GetFleetMaintainedApp(ctx context.Context, appID uint, teamID *uint) (*fleet.MaintainedApp, error) {
// skipauth: No authorization check needed due to implementation returning
// only license error.
svc.authz.SkipAuthorization(ctx)
return nil, fleet.ErrMissingLicense
}