mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** For #39344 # Details As a first step to deprecating API params like `team_id` in favor of `fleet_id` and `query_id` in favor of `report_id`, this PR adds `renameto` tags to all deprecated keys. There is no logic in this PR to actually use these tags in any way. The logic and test fixes will be in the next PR, but in the interest of keeping things manageable I'm pushing this out first. There were definitely params with "query" in them that we don't want to change (mainly osquery-related), and I think I kept them all out but it's worth double-checking here. The team -> fleet changes are pretty safe in comparison. # Checklist for submitter If some of the following don't apply, delete the relevant line. - [ ] 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/guides/committing-changes.md#changes-files) for more information. Deferring changelog to PR with logic changes ## Testing - [ ] Added/updated automated tests This should be a no-op. All existing tests shoud pass. - [X] QA'd all new/changed functionality manually
491 lines
16 KiB
Go
491 lines
16 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
|
|
"github.com/fleetdm/fleet/v4/pkg/optjson"
|
|
"golang.org/x/text/unicode/norm"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
)
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// List Teams
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type listTeamsRequest struct {
|
|
ListOptions fleet.ListOptions `url:"list_options"`
|
|
}
|
|
|
|
type listTeamsResponse struct {
|
|
Teams []fleet.Team `json:"teams" renameto:"fleets"`
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r listTeamsResponse) Error() error { return r.Err }
|
|
|
|
func listTeamsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*listTeamsRequest)
|
|
teams, err := svc.ListTeams(ctx, req.ListOptions)
|
|
if err != nil {
|
|
return listTeamsResponse{Err: err}, nil
|
|
}
|
|
|
|
resp := listTeamsResponse{Teams: []fleet.Team{}}
|
|
for _, team := range teams {
|
|
resp.Teams = append(resp.Teams, *team)
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func (svc *Service) ListTeams(ctx context.Context, opt fleet.ListOptions) ([]*fleet.Team, error) {
|
|
// skipauth: No authorization check needed due to implementation returning
|
|
// only license error.
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
return nil, fleet.ErrMissingLicense
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Get Team
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type getTeamRequest struct {
|
|
ID uint `url:"id"`
|
|
}
|
|
|
|
type getTeamResponse struct {
|
|
Team *fleet.Team `json:"team" renameto:"fleet"`
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r getTeamResponse) Error() error { return r.Err }
|
|
|
|
type defaultTeamResponse struct {
|
|
Team *fleet.DefaultTeam `json:"team" renameto:"fleet"`
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r defaultTeamResponse) Error() error { return r.Err }
|
|
|
|
func getTeamEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*getTeamRequest)
|
|
|
|
team, err := svc.GetTeam(ctx, req.ID)
|
|
if err != nil {
|
|
return getTeamResponse{Err: err}, nil
|
|
}
|
|
|
|
// Special handling for team ID 0 - return DefaultTeam structure
|
|
if team.ID == 0 {
|
|
defaultTeam := &fleet.DefaultTeam{
|
|
ID: team.ID,
|
|
Name: team.Name,
|
|
DefaultTeamConfig: fleet.DefaultTeamConfig{
|
|
WebhookSettings: fleet.DefaultTeamWebhookSettings{
|
|
FailingPoliciesWebhook: team.Config.WebhookSettings.FailingPoliciesWebhook,
|
|
},
|
|
Integrations: fleet.DefaultTeamIntegrations{
|
|
Jira: team.Config.Integrations.Jira,
|
|
Zendesk: team.Config.Integrations.Zendesk,
|
|
},
|
|
},
|
|
}
|
|
return defaultTeamResponse{Team: defaultTeam}, nil
|
|
}
|
|
|
|
return getTeamResponse{Team: team}, nil
|
|
}
|
|
|
|
func (svc *Service) GetTeam(ctx context.Context, tid uint) (*fleet.Team, error) {
|
|
// skipauth: No authorization check needed due to implementation returning
|
|
// only license error.
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
return nil, fleet.ErrMissingLicense
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Create Team
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type createTeamRequest struct {
|
|
fleet.TeamPayload
|
|
}
|
|
|
|
type teamResponse struct {
|
|
Team *fleet.Team `json:"team,omitempty" renameto:"fleet"`
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r teamResponse) Error() error { return r.Err }
|
|
|
|
func createTeamEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*createTeamRequest)
|
|
|
|
team, err := svc.NewTeam(ctx, req.TeamPayload)
|
|
if err != nil {
|
|
return teamResponse{Err: err}, nil
|
|
}
|
|
return teamResponse{Team: team}, nil
|
|
}
|
|
|
|
func (svc *Service) NewTeam(ctx context.Context, p fleet.TeamPayload) (*fleet.Team, error) {
|
|
// skipauth: No authorization check needed due to implementation returning
|
|
// only license error.
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
return nil, fleet.ErrMissingLicense
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Modify Team
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type modifyTeamRequest struct {
|
|
ID uint `json:"-" url:"id"`
|
|
fleet.TeamPayload
|
|
}
|
|
|
|
func modifyTeamEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*modifyTeamRequest)
|
|
|
|
// AppleOSUpdateSettings.UpdateNewHosts is only used in macOS ... so ignore any values sent for iOS/iPadOS
|
|
if req.TeamPayload.MDM != nil {
|
|
if req.TeamPayload.MDM.IOSUpdates != nil {
|
|
req.TeamPayload.MDM.IOSUpdates.UpdateNewHosts = optjson.Bool{}
|
|
}
|
|
if req.TeamPayload.MDM.IPadOSUpdates != nil {
|
|
req.TeamPayload.MDM.IPadOSUpdates.UpdateNewHosts = optjson.Bool{}
|
|
}
|
|
}
|
|
|
|
team, err := svc.ModifyTeam(ctx, req.ID, req.TeamPayload)
|
|
if err != nil {
|
|
return teamResponse{Err: err}, nil
|
|
}
|
|
|
|
// Special handling for team ID 0 - return limited fields
|
|
if req.ID == 0 {
|
|
// Convert to DefaultTeam with limited fields
|
|
defaultTeam := &fleet.DefaultTeam{
|
|
ID: team.ID,
|
|
Name: team.Name,
|
|
DefaultTeamConfig: fleet.DefaultTeamConfig{
|
|
WebhookSettings: fleet.DefaultTeamWebhookSettings{
|
|
FailingPoliciesWebhook: team.Config.WebhookSettings.FailingPoliciesWebhook,
|
|
},
|
|
Integrations: fleet.DefaultTeamIntegrations{
|
|
Jira: team.Config.Integrations.Jira,
|
|
Zendesk: team.Config.Integrations.Zendesk,
|
|
},
|
|
},
|
|
}
|
|
return defaultTeamResponse{Team: defaultTeam}, nil
|
|
}
|
|
|
|
return teamResponse{Team: team}, err
|
|
}
|
|
|
|
func (svc *Service) ModifyTeam(ctx context.Context, id uint, payload fleet.TeamPayload) (*fleet.Team, error) {
|
|
// skipauth: No authorization check needed due to implementation returning
|
|
// only license error.
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
return nil, fleet.ErrMissingLicense
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Delete Team
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type deleteTeamRequest struct {
|
|
ID uint `url:"id"`
|
|
}
|
|
|
|
type deleteTeamResponse struct {
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r deleteTeamResponse) Error() error { return r.Err }
|
|
|
|
func deleteTeamEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*deleteTeamRequest)
|
|
err := svc.DeleteTeam(ctx, req.ID)
|
|
if err != nil {
|
|
return deleteTeamResponse{Err: err}, nil
|
|
}
|
|
return deleteTeamResponse{}, nil
|
|
}
|
|
|
|
func (svc *Service) DeleteTeam(ctx context.Context, tid uint) error {
|
|
// skipauth: No authorization check needed due to implementation returning
|
|
// only license error.
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
return fleet.ErrMissingLicense
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Apply Team Specs
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type applyTeamSpecsRequest struct {
|
|
Force bool `json:"-" query:"force,optional"` // if true, bypass strict incoming json validation
|
|
DryRun bool `json:"-" query:"dry_run,optional"` // if true, apply validation but do not save changes
|
|
DryRunAssumptions *fleet.TeamSpecsDryRunAssumptions `json:"dry_run_assumptions,omitempty"`
|
|
Specs []*fleet.TeamSpec `json:"specs"`
|
|
}
|
|
|
|
func (req *applyTeamSpecsRequest) DecodeBody(ctx context.Context, r io.Reader, u url.Values, c []*x509.Certificate) error {
|
|
if err := fleet.JSONStrictDecode(r, req); err != nil {
|
|
err = fleet.NewUserMessageError(err, http.StatusBadRequest)
|
|
if !req.Force || !fleet.IsJSONUnknownFieldError(err) {
|
|
// only unknown field errors can be forced at this point (other errors
|
|
// can be forced later, after agent options' validations)
|
|
return ctxerr.Wrap(ctx, err, "strict decode team specs")
|
|
}
|
|
}
|
|
|
|
// the MacOSSettings field must be validated separately, since it
|
|
// JSON-decodes into a free-form map.
|
|
for _, spec := range req.Specs {
|
|
if spec == nil || spec.MDM.MacOSSettings == nil {
|
|
continue
|
|
}
|
|
|
|
var macOSSettings fleet.MacOSSettings
|
|
validMap := macOSSettings.ToMap()
|
|
|
|
// the keys provided must be valid
|
|
for k := range spec.MDM.MacOSSettings {
|
|
if _, ok := validMap[k]; !ok {
|
|
return ctxerr.Wrap(ctx, fleet.NewUserMessageError(
|
|
fmt.Errorf("json: unknown field %q", k),
|
|
http.StatusBadRequest), "strict decode team specs")
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type applyTeamSpecsResponse struct {
|
|
Err error `json:"error,omitempty"`
|
|
TeamIDsByName map[string]uint `json:"team_ids_by_name,omitempty" renameto:"fleet_ids_by_name"`
|
|
}
|
|
|
|
func (r applyTeamSpecsResponse) Error() error { return r.Err }
|
|
|
|
func applyTeamSpecsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*applyTeamSpecsRequest)
|
|
if !req.DryRun {
|
|
req.DryRunAssumptions = nil
|
|
}
|
|
|
|
// remove any nil spec (may happen in conversion from YAML to JSON with fleetctl, but also
|
|
// with the API should someone send such JSON)
|
|
actualSpecs := make([]*fleet.TeamSpec, 0, len(req.Specs))
|
|
for _, spec := range req.Specs {
|
|
if spec != nil {
|
|
// Normalize the team name for full Unicode support to prevent potential issue further in the spec flow
|
|
spec.Name = norm.NFC.String(spec.Name)
|
|
actualSpecs = append(actualSpecs, spec)
|
|
}
|
|
}
|
|
|
|
idsByName, err := svc.ApplyTeamSpecs(
|
|
ctx, actualSpecs, fleet.ApplyTeamSpecOptions{
|
|
ApplySpecOptions: fleet.ApplySpecOptions{
|
|
Force: req.Force,
|
|
DryRun: req.DryRun,
|
|
},
|
|
DryRunAssumptions: req.DryRunAssumptions,
|
|
})
|
|
if err != nil {
|
|
return applyTeamSpecsResponse{Err: err}, nil
|
|
}
|
|
return applyTeamSpecsResponse{TeamIDsByName: idsByName}, nil
|
|
}
|
|
|
|
func (svc Service) ApplyTeamSpecs(ctx context.Context, _ []*fleet.TeamSpec, _ fleet.ApplyTeamSpecOptions) (map[string]uint, error) {
|
|
// skipauth: No authorization check needed due to implementation returning
|
|
// only license error.
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
return nil, fleet.ErrMissingLicense
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Modify Team Agent Options
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type modifyTeamAgentOptionsRequest struct {
|
|
ID uint `json:"-" url:"id"`
|
|
Force bool `json:"-" query:"force,optional"` // if true, bypass strict incoming json validation
|
|
DryRun bool `json:"-" query:"dry_run,optional"` // if true, apply validation but do not save changes
|
|
json.RawMessage
|
|
}
|
|
|
|
func modifyTeamAgentOptionsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*modifyTeamAgentOptionsRequest)
|
|
team, err := svc.ModifyTeamAgentOptions(ctx, req.ID, req.RawMessage, fleet.ApplySpecOptions{
|
|
Force: req.Force,
|
|
DryRun: req.DryRun,
|
|
})
|
|
if err != nil {
|
|
return teamResponse{Err: err}, nil
|
|
}
|
|
return teamResponse{Team: team}, err
|
|
}
|
|
|
|
func (svc *Service) ModifyTeamAgentOptions(ctx context.Context, id uint, teamOptions json.RawMessage, applyOptions fleet.ApplySpecOptions) (*fleet.Team, error) {
|
|
// skipauth: No authorization check needed due to implementation returning
|
|
// only license error.
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
return nil, fleet.ErrMissingLicense
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// List Team Users
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type listTeamUsersRequest struct {
|
|
TeamID uint `url:"id"`
|
|
ListOptions fleet.ListOptions `url:"list_options"`
|
|
}
|
|
|
|
func listTeamUsersEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*listTeamUsersRequest)
|
|
users, err := svc.ListTeamUsers(ctx, req.TeamID, req.ListOptions)
|
|
if err != nil {
|
|
return listUsersResponse{Err: err}, nil
|
|
}
|
|
|
|
resp := listUsersResponse{Users: []fleet.User{}}
|
|
for _, user := range users {
|
|
resp.Users = append(resp.Users, *user)
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func (svc *Service) ListTeamUsers(ctx context.Context, teamID uint, opt fleet.ListOptions) ([]*fleet.User, error) {
|
|
// skipauth: No authorization check needed due to implementation returning
|
|
// only license error.
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
return nil, fleet.ErrMissingLicense
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Add / Delete Team Users
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// same request struct for add and delete
|
|
type modifyTeamUsersRequest struct {
|
|
TeamID uint `json:"-" url:"id"`
|
|
// User ID and role must be specified for add users, user ID must be
|
|
// specified for delete users.
|
|
Users []fleet.TeamUser `json:"users"`
|
|
}
|
|
|
|
func addTeamUsersEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*modifyTeamUsersRequest)
|
|
team, err := svc.AddTeamUsers(ctx, req.TeamID, req.Users)
|
|
if err != nil {
|
|
return teamResponse{Err: err}, nil
|
|
}
|
|
return teamResponse{Team: team}, err
|
|
}
|
|
|
|
func (svc *Service) AddTeamUsers(ctx context.Context, teamID uint, users []fleet.TeamUser) (*fleet.Team, error) {
|
|
// skipauth: No authorization check needed due to implementation returning
|
|
// only license error.
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
return nil, fleet.ErrMissingLicense
|
|
}
|
|
|
|
func deleteTeamUsersEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*modifyTeamUsersRequest)
|
|
team, err := svc.DeleteTeamUsers(ctx, req.TeamID, req.Users)
|
|
if err != nil {
|
|
return teamResponse{Err: err}, nil
|
|
}
|
|
return teamResponse{Team: team}, err
|
|
}
|
|
|
|
func (svc *Service) DeleteTeamUsers(ctx context.Context, teamID uint, users []fleet.TeamUser) (*fleet.Team, error) {
|
|
// skipauth: No authorization check needed due to implementation returning
|
|
// only license error.
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
return nil, fleet.ErrMissingLicense
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Get enroll secrets for team
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type teamEnrollSecretsRequest struct {
|
|
TeamID uint `url:"id"`
|
|
}
|
|
|
|
type teamEnrollSecretsResponse struct {
|
|
Secrets []*fleet.EnrollSecret `json:"secrets"`
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r teamEnrollSecretsResponse) Error() error { return r.Err }
|
|
|
|
func teamEnrollSecretsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*teamEnrollSecretsRequest)
|
|
secrets, err := svc.TeamEnrollSecrets(ctx, req.TeamID)
|
|
if err != nil {
|
|
return teamEnrollSecretsResponse{Err: err}, nil
|
|
}
|
|
|
|
return teamEnrollSecretsResponse{Secrets: secrets}, err
|
|
}
|
|
|
|
func (svc *Service) TeamEnrollSecrets(ctx context.Context, teamID uint) ([]*fleet.EnrollSecret, error) {
|
|
// skipauth: No authorization check needed due to implementation returning
|
|
// only license error.
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
return nil, fleet.ErrMissingLicense
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Modify enroll secrets for team
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type modifyTeamEnrollSecretsRequest struct {
|
|
TeamID uint `url:"fleet_id"`
|
|
Secrets []fleet.EnrollSecret `json:"secrets"`
|
|
}
|
|
|
|
func modifyTeamEnrollSecretsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*modifyTeamEnrollSecretsRequest)
|
|
secrets, err := svc.ModifyTeamEnrollSecrets(ctx, req.TeamID, req.Secrets)
|
|
if err != nil {
|
|
return teamEnrollSecretsResponse{Err: err}, nil
|
|
}
|
|
|
|
return teamEnrollSecretsResponse{Secrets: secrets}, err
|
|
}
|
|
|
|
func (svc *Service) ModifyTeamEnrollSecrets(ctx context.Context, teamID uint, secrets []fleet.EnrollSecret) ([]*fleet.EnrollSecret, error) {
|
|
// skipauth: No authorization check needed due to implementation returning
|
|
// only license error.
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
return nil, fleet.ErrMissingLicense
|
|
}
|