mirror of
https://github.com/fleetdm/fleet
synced 2026-05-16 05:28:38 +00:00
- Added Jira and Zendesk integrations for "No team". (These are not supported by GitOps for teams) # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] 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. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually ## New Fleet configuration settings - [x] Setting(s) is/are explicitly excluded from GitOps <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - New Features - Default (No Team) responses now include limited integrations (Jira, Zendesk). - You can configure or clear Jira/Zendesk integrations for the Default (No Team) settings. - Bug Fixes - More consistent handling of the Default (No Team) when fetching team details. - Improved validation to prevent conflicting automation settings between webhooks and integrations. - Documentation - Clarified that Jira/Zendesk integrations aren’t supported via GitOps or at the team level (including No Team). - Noted that certain options (e.g., Google Calendar, Conditional Access) aren’t supported for the Default (No Team). <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
479 lines
15 KiB
Go
479 lines
15 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
|
|
"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"`
|
|
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"`
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r getTeamResponse) Error() error { return r.Err }
|
|
|
|
type defaultTeamResponse struct {
|
|
Team *fleet.DefaultTeam `json:"team"`
|
|
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"`
|
|
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)
|
|
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"`
|
|
}
|
|
|
|
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:"team_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
|
|
}
|