mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 00:45:19 +00:00
Update team integrations to reference global integrations (part of failing policies automation support) (#6156)
This commit is contained in:
parent
abe33f1d8d
commit
7f9bb6431e
@ -5505,18 +5505,14 @@ _Available in Fleet Premium_
|
||||
| destination_url | string | body | The URL to deliver the webhook requests to. |
|
||||
| policy_ids | array | body | List of policy IDs to enable failing policies webhook. |
|
||||
| host_batch_size | integer | body | Maximum number of hosts to batch on failing policy webhook requests. The default, 0, means no batching (all hosts failing a policy are sent on one request). |
|
||||
| integrations | object | body | Integrations settings for the team. |
|
||||
| integrations | object | body | Integrations settings for the team. Note that integrations referenced here must already exist at the global level, created by a call to [Modify configuration](#modify-configuration). |
|
||||
| jira | array | body | Jira integrations configuration. |
|
||||
| url | string | body | The URL of the Jira server to integrate with. |
|
||||
| username | string | body | The Jira username to use for this Jira integration. |
|
||||
| api_token | string | body | The API token of the Jira username to use for this Jira integration. |
|
||||
| project_key | string | body | The Jira project key to use for this integration. Jira tickets will be created in this project. |
|
||||
| url | string | body | The URL of the Jira server to use. |
|
||||
| project_key | string | body | The project key of the Jira integration to use. Jira tickets will be created in this project. |
|
||||
| enable_failing_policies | boolean | body | Whether or not that Jira integration is enabled for failing policies. Only one failing policy automation can be enabled at a given time (enable_failing_policies_webhook and enable_failing_policies). |
|
||||
| zendesk | array | body | Zendesk integrations configuration. |
|
||||
| url | string | body | The URL of the Zendesk server to integrate with. |
|
||||
| email | string | body | The Zendesk user email to use for this Zendesk integration. |
|
||||
| api_token | string | body | The Zendesk API token to use for this Zendesk integration. |
|
||||
| group_id | integer | body | The Zendesk group id to use for this integration. Zendesk tickets will be created in this group. |
|
||||
| url | string | body | The URL of the Zendesk server to use. |
|
||||
| group_id | integer | body | The Zendesk group id to use. Zendesk tickets will be created in this group. |
|
||||
| enable_failing_policies | boolean | body | Whether or not that Zendesk integration is enabled for failing policies. Only one failing policy automation can be enabled at a given time (enable_failing_policies_webhook and enable_failing_policies). |
|
||||
|
||||
#### Example (add users to a team)
|
||||
|
@ -46,17 +46,3 @@ func NewService(
|
||||
license: license,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TODO(mna): copied from server/service/transport_error.go for now, should
|
||||
// eventually have common implementations of HTTP-related errors. #4406
|
||||
type badRequestError struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func (e *badRequestError) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (e *badRequestError) BadRequestError() []map[string]string {
|
||||
return nil
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server"
|
||||
@ -96,30 +95,21 @@ func (svc *Service) ModifyTeam(ctx context.Context, teamID uint, payload fleet.T
|
||||
}
|
||||
|
||||
if payload.Integrations != nil {
|
||||
oriJiraByProjectKey, err := fleet.IndexTeamJiraIntegrations(team.Config.Integrations.Jira)
|
||||
// the team integrations must reference an existing global config integration.
|
||||
appCfg, err := svc.ds.AppConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "modify Team")
|
||||
return nil, err
|
||||
}
|
||||
if _, err := payload.Integrations.MatchWithIntegrations(appCfg.Integrations); err != nil {
|
||||
return nil, fleet.NewInvalidArgumentError("integrations", err.Error())
|
||||
}
|
||||
|
||||
oriZendeskByGroupID, err := fleet.IndexTeamZendeskIntegrations(team.Config.Integrations.Zendesk)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "modify Team")
|
||||
// integrations must be unique
|
||||
if err := payload.Integrations.Validate(); err != nil {
|
||||
return nil, fleet.NewInvalidArgumentError("integrations", err.Error())
|
||||
}
|
||||
|
||||
if err := fleet.ValidateTeamJiraIntegrations(ctx, oriJiraByProjectKey, payload.Integrations.Jira); err != nil {
|
||||
if errors.As(err, &fleet.IntegrationTestError{}) {
|
||||
return nil, ctxerr.Wrap(ctx, &badRequestError{message: err.Error()})
|
||||
}
|
||||
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("Jira integration", err.Error()))
|
||||
}
|
||||
team.Config.Integrations.Jira = payload.Integrations.Jira
|
||||
|
||||
if err := fleet.ValidateTeamZendeskIntegrations(ctx, oriZendeskByGroupID, payload.Integrations.Zendesk); err != nil {
|
||||
if errors.As(err, &fleet.IntegrationTestError{}) {
|
||||
return nil, ctxerr.Wrap(ctx, &badRequestError{message: err.Error()})
|
||||
}
|
||||
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("Zendesk integration", err.Error()))
|
||||
}
|
||||
team.Config.Integrations.Zendesk = payload.Integrations.Zendesk
|
||||
}
|
||||
|
||||
|
@ -300,3 +300,54 @@ func (ds *Datastore) TeamAgentOptions(ctx context.Context, tid uint) (*json.RawM
|
||||
}
|
||||
return agentOptions, nil
|
||||
}
|
||||
|
||||
// DeleteIntegrationsFromTeams removes the deleted integrations from any team
|
||||
// that uses it.
|
||||
func (ds *Datastore) DeleteIntegrationsFromTeams(ctx context.Context, deletedIntgs fleet.Integrations) error {
|
||||
const (
|
||||
listTeams = `SELECT id, config FROM teams WHERE config IS NOT NULL`
|
||||
updateTeam = `UPDATE teams SET config = ? WHERE id = ?`
|
||||
)
|
||||
|
||||
rows, err := ds.writer.QueryxContext(ctx, listTeams)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "query teams")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var tm fleet.Team
|
||||
if err := rows.StructScan(&tm); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "scan team row in struct")
|
||||
}
|
||||
|
||||
// ignore errors, it's ok for some integrations to not match with the
|
||||
// batch of deleted integrations, we're only interested in knowing if
|
||||
// some did match.
|
||||
if matches, _ := tm.Config.Integrations.MatchWithIntegrations(deletedIntgs); len(matches.Jira)+len(matches.Zendesk) > 0 {
|
||||
delJira, _ := fleet.IndexJiraIntegrations(matches.Jira)
|
||||
delZendesk, _ := fleet.IndexZendeskIntegrations(matches.Zendesk)
|
||||
|
||||
var keepJira []*fleet.TeamJiraIntegration
|
||||
for _, tmIntg := range tm.Config.Integrations.Jira {
|
||||
if _, ok := delJira[tmIntg.UniqueKey()]; !ok {
|
||||
keepJira = append(keepJira, tmIntg)
|
||||
}
|
||||
}
|
||||
|
||||
var keepZendesk []*fleet.TeamZendeskIntegration
|
||||
for _, tmIntg := range tm.Config.Integrations.Zendesk {
|
||||
if _, ok := delZendesk[tmIntg.UniqueKey()]; !ok {
|
||||
keepZendesk = append(keepZendesk, tmIntg)
|
||||
}
|
||||
}
|
||||
|
||||
tm.Config.Integrations.Jira = keepJira
|
||||
tm.Config.Integrations.Zendesk = keepZendesk
|
||||
if _, err := ds.writer.ExecContext(ctx, updateTeam, tm.Config, tm.ID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "update team config")
|
||||
}
|
||||
}
|
||||
}
|
||||
return rows.Err()
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ func TestTeams(t *testing.T) {
|
||||
{"EnrollSecrets", testTeamsEnrollSecrets},
|
||||
{"TeamAgentOptions", testTeamsAgentOptions},
|
||||
{"TeamsDeleteRename", testTeamsDeleteRename},
|
||||
{"DeleteIntegrationsFromTeams", testTeamsDeleteIntegrationsFromTeams},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
@ -318,3 +319,108 @@ func testTeamsAgentOptions(t *testing.T, ds *Datastore) {
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, string(agentOptions), string(*teamAgentOptions2))
|
||||
}
|
||||
|
||||
func testTeamsDeleteIntegrationsFromTeams(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
|
||||
urla, urlb, urlc, urld, urle, urlf, urlg :=
|
||||
"http://a.com", "http://b.com", "http://c.com", "http://d.com", "http://e.com", "http://f.com", "http://g.com"
|
||||
|
||||
// create some teams
|
||||
team1, err := ds.NewTeam(ctx, &fleet.Team{
|
||||
Name: "team1",
|
||||
Config: fleet.TeamConfig{
|
||||
Integrations: fleet.TeamIntegrations{
|
||||
Jira: []*fleet.TeamJiraIntegration{
|
||||
{URL: urla, ProjectKey: "A"},
|
||||
{URL: urlb, ProjectKey: "B"},
|
||||
},
|
||||
Zendesk: []*fleet.TeamZendeskIntegration{
|
||||
{URL: urlc, GroupID: 1},
|
||||
{URL: urld, GroupID: 2},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
team2, err := ds.NewTeam(ctx, &fleet.Team{
|
||||
Name: "team2",
|
||||
Config: fleet.TeamConfig{
|
||||
Integrations: fleet.TeamIntegrations{
|
||||
Jira: []*fleet.TeamJiraIntegration{
|
||||
{URL: urla, ProjectKey: "A"},
|
||||
{URL: urle, ProjectKey: "E"},
|
||||
},
|
||||
Zendesk: []*fleet.TeamZendeskIntegration{
|
||||
{URL: urlc, GroupID: 1},
|
||||
{URL: urlf, GroupID: 3},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
team3, err := ds.NewTeam(ctx, &fleet.Team{
|
||||
Name: "team3",
|
||||
Config: fleet.TeamConfig{
|
||||
Integrations: fleet.TeamIntegrations{
|
||||
Jira: []*fleet.TeamJiraIntegration{
|
||||
{URL: urle, ProjectKey: "E"},
|
||||
},
|
||||
Zendesk: []*fleet.TeamZendeskIntegration{
|
||||
{URL: urlf, GroupID: 3},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
assertIntgURLs := func(wantTm1, wantTm2, wantTm3 []string) {
|
||||
// assert that the integrations' URLs of each team corresponds to the
|
||||
// expected values
|
||||
expected := [][]string{wantTm1, wantTm2, wantTm3}
|
||||
for i, id := range []uint{team1.ID, team2.ID, team3.ID} {
|
||||
tm, err := ds.Team(ctx, id)
|
||||
require.NoError(t, err)
|
||||
|
||||
var urls []string
|
||||
for _, j := range tm.Config.Integrations.Jira {
|
||||
urls = append(urls, j.URL)
|
||||
}
|
||||
for _, z := range tm.Config.Integrations.Zendesk {
|
||||
urls = append(urls, z.URL)
|
||||
}
|
||||
|
||||
want := expected[i]
|
||||
require.ElementsMatch(t, want, urls)
|
||||
}
|
||||
}
|
||||
|
||||
// delete nothing
|
||||
err = ds.DeleteIntegrationsFromTeams(context.Background(), fleet.Integrations{})
|
||||
require.NoError(t, err)
|
||||
assertIntgURLs([]string{urla, urlb, urlc, urld}, []string{urla, urle, urlc, urlf}, []string{urle, urlf})
|
||||
|
||||
// delete a, b, c (in the url) so that team1 and team2 are impacted
|
||||
err = ds.DeleteIntegrationsFromTeams(context.Background(), fleet.Integrations{
|
||||
Jira: []*fleet.JiraIntegration{
|
||||
{URL: urla, ProjectKey: "A"},
|
||||
{URL: urlb, ProjectKey: "B"},
|
||||
},
|
||||
Zendesk: []*fleet.ZendeskIntegration{
|
||||
{URL: urlc, GroupID: 1},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assertIntgURLs([]string{urld}, []string{urle, urlf}, []string{urle, urlf})
|
||||
|
||||
// delete g, no team is impacted
|
||||
err = ds.DeleteIntegrationsFromTeams(context.Background(), fleet.Integrations{
|
||||
Jira: []*fleet.JiraIntegration{
|
||||
{URL: urlg, ProjectKey: "G"},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assertIntgURLs([]string{urld}, []string{urle, urlf}, []string{urle, urlf})
|
||||
}
|
||||
|
@ -339,6 +339,9 @@ type Datastore interface {
|
||||
SearchTeams(ctx context.Context, filter TeamFilter, matchQuery string, omit ...uint) ([]*Team, error)
|
||||
// TeamEnrollSecrets lists the enroll secrets for the team.
|
||||
TeamEnrollSecrets(ctx context.Context, teamID uint) ([]*EnrollSecret, error)
|
||||
// DeleteIntegrationsFromTeams deletes integrations used by teams, as they
|
||||
// are being deleted from the global configuration.
|
||||
DeleteIntegrationsFromTeams(ctx context.Context, deletedIntgs Integrations) error
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// SoftwareStore
|
||||
|
@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/service/externalsvc"
|
||||
)
|
||||
@ -15,64 +17,117 @@ type TeamIntegrations struct {
|
||||
Zendesk []*TeamZendeskIntegration `json:"zendesk"`
|
||||
}
|
||||
|
||||
// ToIntegrations converts a TeamIntegrations to an Integrations struct.
|
||||
func (ti TeamIntegrations) ToIntegrations() Integrations {
|
||||
var intgs Integrations
|
||||
intgs.Jira = make([]*JiraIntegration, len(ti.Jira))
|
||||
for i, j := range ti.Jira {
|
||||
intgs.Jira[i] = j.ToJiraIntegration()
|
||||
// MatchWithIntegrations matches the team integrations to their corresponding
|
||||
// global integrations found in globalIntgs, returning the resulting
|
||||
// integrations struct. It returns an error if any team integration does not
|
||||
// map to a global integration, but it will still return the complete list
|
||||
// of integrations that do match.
|
||||
func (ti TeamIntegrations) MatchWithIntegrations(globalIntgs Integrations) (Integrations, error) {
|
||||
var result Integrations
|
||||
|
||||
jiraIntgs, err := IndexJiraIntegrations(globalIntgs.Jira)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
intgs.Zendesk = make([]*ZendeskIntegration, len(ti.Zendesk))
|
||||
for i, z := range ti.Zendesk {
|
||||
intgs.Zendesk[i] = z.ToZendeskIntegration()
|
||||
zendeskIntgs, err := IndexZendeskIntegrations(globalIntgs.Zendesk)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
return intgs
|
||||
|
||||
var errs []string
|
||||
for _, tmJira := range ti.Jira {
|
||||
key := tmJira.UniqueKey()
|
||||
intg, ok := jiraIntgs[key]
|
||||
if !ok {
|
||||
errs = append(errs, fmt.Sprintf("unknown Jira integration for url %s and project key %s", tmJira.URL, tmJira.ProjectKey))
|
||||
continue
|
||||
}
|
||||
intg.EnableFailingPolicies = tmJira.EnableFailingPolicies
|
||||
result.Jira = append(result.Jira, &intg)
|
||||
}
|
||||
for _, tmZendesk := range ti.Zendesk {
|
||||
key := tmZendesk.UniqueKey()
|
||||
intg, ok := zendeskIntgs[key]
|
||||
if !ok {
|
||||
errs = append(errs, fmt.Sprintf("unknown Zendesk integration for url %s and group ID %v", tmZendesk.URL, tmZendesk.GroupID))
|
||||
continue
|
||||
}
|
||||
intg.EnableFailingPolicies = tmZendesk.EnableFailingPolicies
|
||||
result.Zendesk = append(result.Zendesk, &intg)
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
err = errors.New(strings.Join(errs, "\n"))
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Validate validates the team integrations for uniqueness.
|
||||
func (ti TeamIntegrations) Validate() error {
|
||||
jira := make(map[string]*TeamJiraIntegration, len(ti.Jira))
|
||||
for _, j := range ti.Jira {
|
||||
key := j.UniqueKey()
|
||||
if _, ok := jira[key]; ok {
|
||||
return fmt.Errorf("duplicate Jira integration for url %s and project key %s", j.URL, j.ProjectKey)
|
||||
}
|
||||
jira[key] = j
|
||||
}
|
||||
|
||||
zendesk := make(map[string]*TeamZendeskIntegration, len(ti.Zendesk))
|
||||
for _, z := range ti.Zendesk {
|
||||
key := z.UniqueKey()
|
||||
if _, ok := zendesk[key]; ok {
|
||||
return fmt.Errorf("duplicate Zendesk integration for url %s and group ID %v", z.URL, z.GroupID)
|
||||
}
|
||||
zendesk[key] = z
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TeamJiraIntegration configures an instance of an integration with the Jira
|
||||
// system for a team.
|
||||
type TeamJiraIntegration struct {
|
||||
URL string `json:"url"`
|
||||
Username string `json:"username"`
|
||||
APIToken string `json:"api_token"`
|
||||
ProjectKey string `json:"project_key"`
|
||||
EnableFailingPolicies bool `json:"enable_failing_policies"`
|
||||
}
|
||||
|
||||
// ToJiraIntegration converts a TeamJiraIntegration to a JiraIntegration
|
||||
// struct, leaving additional fields to their zero value.
|
||||
func (ti TeamJiraIntegration) ToJiraIntegration() *JiraIntegration {
|
||||
return &JiraIntegration{TeamJiraIntegration: ti}
|
||||
// UniqueKey returns the unique key of this integration.
|
||||
func (j TeamJiraIntegration) UniqueKey() string {
|
||||
return j.URL + "\n" + j.ProjectKey
|
||||
}
|
||||
|
||||
// TeamZendeskIntegration configures an instance of an integration with the
|
||||
// external Zendesk service for a team.
|
||||
type TeamZendeskIntegration struct {
|
||||
URL string `json:"url"`
|
||||
Email string `json:"email"`
|
||||
APIToken string `json:"api_token"`
|
||||
GroupID int64 `json:"group_id"`
|
||||
EnableFailingPolicies bool `json:"enable_failing_policies"`
|
||||
}
|
||||
|
||||
// ToZendeskIntegration converts a TeamZendeskIntegration to a ZendeskIntegration
|
||||
// struct, leaving additional fields to their zero value.
|
||||
func (ti TeamZendeskIntegration) ToZendeskIntegration() *ZendeskIntegration {
|
||||
return &ZendeskIntegration{TeamZendeskIntegration: ti}
|
||||
// UniqueKey returns the unique key of this integration.
|
||||
func (z TeamZendeskIntegration) UniqueKey() string {
|
||||
return z.URL + "\n" + strconv.FormatInt(z.GroupID, 10)
|
||||
}
|
||||
|
||||
// JiraIntegration configures an instance of an integration with the Jira
|
||||
// system.
|
||||
type JiraIntegration struct {
|
||||
// It is a superset of TeamJiraIntegration.
|
||||
TeamJiraIntegration
|
||||
URL string `json:"url"`
|
||||
Username string `json:"username"`
|
||||
APIToken string `json:"api_token"`
|
||||
ProjectKey string `json:"project_key"`
|
||||
EnableFailingPolicies bool `json:"enable_failing_policies"`
|
||||
EnableSoftwareVulnerabilities bool `json:"enable_software_vulnerabilities"`
|
||||
}
|
||||
|
||||
EnableSoftwareVulnerabilities bool `json:"enable_software_vulnerabilities"`
|
||||
func (j JiraIntegration) uniqueKey() string {
|
||||
return j.URL + "\n" + j.ProjectKey
|
||||
}
|
||||
|
||||
// IndexJiraIntegrations indexes the provided Jira integrations in a map keyed
|
||||
// by the project key. It returns an error if a duplicate configuration is
|
||||
// found for the same project key. This is typically used to index the original
|
||||
// by 'URL\nProjectKey'. It returns an error if a duplicate configuration is
|
||||
// found for the same combination. This is typically used to index the original
|
||||
// integrations before applying the changes requested to modify the AppConfig.
|
||||
//
|
||||
// Note that the returned map uses non-pointer JiraIntegration struct values,
|
||||
@ -80,55 +135,37 @@ type JiraIntegration struct {
|
||||
// map. This is important because of how changes are merged with the original
|
||||
// AppConfig when modifying it.
|
||||
func IndexJiraIntegrations(jiraIntgs []*JiraIntegration) (map[string]JiraIntegration, error) {
|
||||
byProjKey := make(map[string]JiraIntegration, len(jiraIntgs))
|
||||
indexed := make(map[string]JiraIntegration, len(jiraIntgs))
|
||||
for _, intg := range jiraIntgs {
|
||||
if _, ok := byProjKey[intg.ProjectKey]; ok {
|
||||
return nil, fmt.Errorf("duplicate Jira integration for project key %s", intg.ProjectKey)
|
||||
key := intg.uniqueKey()
|
||||
if _, ok := indexed[key]; ok {
|
||||
return nil, fmt.Errorf("duplicate Jira integration for url %s and project key %s", intg.URL, intg.ProjectKey)
|
||||
}
|
||||
byProjKey[intg.ProjectKey] = *intg
|
||||
indexed[key] = *intg
|
||||
}
|
||||
return byProjKey, nil
|
||||
}
|
||||
|
||||
// IndexTeamJiraIntegrations is the same as IndexJiraIntegrations, but for
|
||||
// team-specific integration structs.
|
||||
func IndexTeamJiraIntegrations(teamJiraIntgs []*TeamJiraIntegration) (map[string]TeamJiraIntegration, error) {
|
||||
jiraIntgs := make([]*JiraIntegration, len(teamJiraIntgs))
|
||||
for i, t := range teamJiraIntgs {
|
||||
jiraIntgs[i] = t.ToJiraIntegration()
|
||||
}
|
||||
|
||||
indexed, err := IndexJiraIntegrations(jiraIntgs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
teamIndexed := make(map[string]TeamJiraIntegration, len(indexed))
|
||||
for k, v := range indexed {
|
||||
teamIndexed[k] = v.TeamJiraIntegration
|
||||
}
|
||||
return teamIndexed, nil
|
||||
return indexed, nil
|
||||
}
|
||||
|
||||
// ValidateJiraIntegrations validates that the merge of the original and new
|
||||
// Jira integrations does not result in any duplicate configuration, and that
|
||||
// each modified or added integration can successfully connect to the external
|
||||
// Jira service.
|
||||
// Jira service. It returns the list of integrations that were deleted, if any.
|
||||
//
|
||||
// On successful return, the newJiraIntgs slice is ready to be saved - it may
|
||||
// have been updated using the original integrations if the API token was
|
||||
// missing.
|
||||
func ValidateJiraIntegrations(ctx context.Context, oriJiraIntgsByProjKey map[string]JiraIntegration, newJiraIntgs []*JiraIntegration) error {
|
||||
newByProjKey := make(map[string]*JiraIntegration, len(newJiraIntgs))
|
||||
func ValidateJiraIntegrations(ctx context.Context, oriJiraIntgsIndexed map[string]JiraIntegration, newJiraIntgs []*JiraIntegration) (deleted []*JiraIntegration, err error) {
|
||||
newIndexed := make(map[string]*JiraIntegration, len(newJiraIntgs))
|
||||
for i, new := range newJiraIntgs {
|
||||
// first check for project key uniqueness
|
||||
if _, ok := newByProjKey[new.ProjectKey]; ok {
|
||||
return fmt.Errorf("duplicate Jira integration for project key %s", new.ProjectKey)
|
||||
// first check for uniqueness
|
||||
key := new.uniqueKey()
|
||||
if _, ok := newIndexed[key]; ok {
|
||||
return nil, fmt.Errorf("duplicate Jira integration for url %s and project key %s", new.URL, new.ProjectKey)
|
||||
}
|
||||
newByProjKey[new.ProjectKey] = new
|
||||
newIndexed[key] = new
|
||||
|
||||
// check if existing integration is being edited
|
||||
if old, ok := oriJiraIntgsByProjKey[new.ProjectKey]; ok {
|
||||
if old, ok := oriJiraIntgsIndexed[key]; ok {
|
||||
if old == *new {
|
||||
// no further validation for unchanged integration
|
||||
continue
|
||||
@ -143,36 +180,18 @@ func ValidateJiraIntegrations(ctx context.Context, oriJiraIntgsByProjKey map[str
|
||||
|
||||
// new or updated, test it
|
||||
if err := makeTestJiraRequest(ctx, new); err != nil {
|
||||
return fmt.Errorf("Jira integration at index %d: %w", i, err)
|
||||
return nil, fmt.Errorf("Jira integration at index %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateTeamJiraIntegrations applies the same validations as
|
||||
// ValidateJiraIntegrations, but for team-specific integration structs.
|
||||
func ValidateTeamJiraIntegrations(ctx context.Context, oriTeamJiraIntgsByProjKey map[string]TeamJiraIntegration, newTeamJiraIntgs []*TeamJiraIntegration) error {
|
||||
newJiraIntgs := make([]*JiraIntegration, len(newTeamJiraIntgs))
|
||||
for i, t := range newTeamJiraIntgs {
|
||||
newJiraIntgs[i] = t.ToJiraIntegration()
|
||||
// collect any deleted integration
|
||||
for key, intg := range oriJiraIntgsIndexed {
|
||||
intg := intg // do not take address of iteration variable
|
||||
if _, ok := newIndexed[key]; !ok {
|
||||
deleted = append(deleted, &intg)
|
||||
}
|
||||
}
|
||||
|
||||
oriJiraIntgsByProjKey := make(map[string]JiraIntegration, len(oriTeamJiraIntgsByProjKey))
|
||||
for k, v := range oriTeamJiraIntgsByProjKey {
|
||||
oriJiraIntgsByProjKey[k] = *v.ToJiraIntegration()
|
||||
}
|
||||
|
||||
if err := ValidateJiraIntegrations(ctx, oriJiraIntgsByProjKey, newJiraIntgs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// assign back the newJiraIntgs to newTeamJiraIntgs, as they may have been
|
||||
// updated by the call and we need to pass that change back to the caller
|
||||
for i, v := range newJiraIntgs {
|
||||
teamJira := newTeamJiraIntgs[i]
|
||||
*teamJira = v.TeamJiraIntegration
|
||||
}
|
||||
return nil
|
||||
return deleted, nil
|
||||
}
|
||||
|
||||
// IntegrationTestError is the type of error returned when a validation of an
|
||||
@ -211,71 +230,60 @@ func makeTestJiraRequest(ctx context.Context, intg *JiraIntegration) error {
|
||||
|
||||
// ZendeskIntegration configures an instance of an integration with the external Zendesk service.
|
||||
type ZendeskIntegration struct {
|
||||
// It is a superset of TeamZendeskIntegration.
|
||||
TeamZendeskIntegration
|
||||
URL string `json:"url"`
|
||||
Email string `json:"email"`
|
||||
APIToken string `json:"api_token"`
|
||||
GroupID int64 `json:"group_id"`
|
||||
EnableFailingPolicies bool `json:"enable_failing_policies"`
|
||||
EnableSoftwareVulnerabilities bool `json:"enable_software_vulnerabilities"`
|
||||
}
|
||||
|
||||
EnableSoftwareVulnerabilities bool `json:"enable_software_vulnerabilities"`
|
||||
func (z ZendeskIntegration) uniqueKey() string {
|
||||
return z.URL + "\n" + strconv.FormatInt(z.GroupID, 10)
|
||||
}
|
||||
|
||||
// IndexZendeskIntegrations indexes the provided Zendesk integrations in a map
|
||||
// keyed by the group ID. It returns an error if a duplicate configuration is
|
||||
// found for the same group ID. This is typically used to index the original
|
||||
// keyed by 'URL\nGroupID'. It returns an error if a duplicate configuration is
|
||||
// found for the same combination. This is typically used to index the original
|
||||
// integrations before applying the changes requested to modify the AppConfig.
|
||||
//
|
||||
// Note that the returned map uses non-pointer ZendeskIntegration struct
|
||||
// values, so that any changes to the original value does not modify the value
|
||||
// in the map. This is important because of how changes are merged with the
|
||||
// original AppConfig when modifying it.
|
||||
func IndexZendeskIntegrations(zendeskIntgs []*ZendeskIntegration) (map[int64]ZendeskIntegration, error) {
|
||||
byGroupID := make(map[int64]ZendeskIntegration, len(zendeskIntgs))
|
||||
func IndexZendeskIntegrations(zendeskIntgs []*ZendeskIntegration) (map[string]ZendeskIntegration, error) {
|
||||
indexed := make(map[string]ZendeskIntegration, len(zendeskIntgs))
|
||||
for _, intg := range zendeskIntgs {
|
||||
if _, ok := byGroupID[intg.GroupID]; ok {
|
||||
return nil, fmt.Errorf("duplicate Zendesk integration for group id %v", intg.GroupID)
|
||||
key := intg.uniqueKey()
|
||||
if _, ok := indexed[key]; ok {
|
||||
return nil, fmt.Errorf("duplicate Zendesk integration for url %s and group id %v", intg.URL, intg.GroupID)
|
||||
}
|
||||
byGroupID[intg.GroupID] = *intg
|
||||
indexed[key] = *intg
|
||||
}
|
||||
return byGroupID, nil
|
||||
}
|
||||
|
||||
// IndexTeamZendeskIntegrations is the same as IndexZendeskIntegrations, but
|
||||
// for team-specific integration structs.
|
||||
func IndexTeamZendeskIntegrations(teamZendeskIntgs []*TeamZendeskIntegration) (map[int64]TeamZendeskIntegration, error) {
|
||||
zendeskIntgs := make([]*ZendeskIntegration, len(teamZendeskIntgs))
|
||||
for i, t := range teamZendeskIntgs {
|
||||
zendeskIntgs[i] = t.ToZendeskIntegration()
|
||||
}
|
||||
|
||||
indexed, err := IndexZendeskIntegrations(zendeskIntgs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
teamIndexed := make(map[int64]TeamZendeskIntegration, len(indexed))
|
||||
for k, v := range indexed {
|
||||
teamIndexed[k] = v.TeamZendeskIntegration
|
||||
}
|
||||
return teamIndexed, nil
|
||||
return indexed, nil
|
||||
}
|
||||
|
||||
// ValidateZendeskIntegrations validates that the merge of the original and
|
||||
// new Zendesk integrations does not result in any duplicate configuration,
|
||||
// and that each modified or added integration can successfully connect to the
|
||||
// external Zendesk service.
|
||||
// external Zendesk service. It returns the list of integrations that were
|
||||
// deleted, if any.
|
||||
//
|
||||
// On successful return, the newZendeskIntgs slice is ready to be saved - it
|
||||
// may have been updated using the original integrations if the API token was
|
||||
// missing.
|
||||
func ValidateZendeskIntegrations(ctx context.Context, oriZendeskIntgsByGroupID map[int64]ZendeskIntegration, newZendeskIntgs []*ZendeskIntegration) error {
|
||||
newByGroupID := make(map[int64]*ZendeskIntegration, len(newZendeskIntgs))
|
||||
func ValidateZendeskIntegrations(ctx context.Context, oriZendeskIntgsIndexed map[string]ZendeskIntegration, newZendeskIntgs []*ZendeskIntegration) (deleted []*ZendeskIntegration, err error) {
|
||||
newIndexed := make(map[string]*ZendeskIntegration, len(newZendeskIntgs))
|
||||
for i, new := range newZendeskIntgs {
|
||||
// first check for group id uniqueness
|
||||
if _, ok := newByGroupID[new.GroupID]; ok {
|
||||
return fmt.Errorf("duplicate Zendesk integration for group id %v", new.GroupID)
|
||||
key := new.uniqueKey()
|
||||
// first check for uniqueness
|
||||
if _, ok := newIndexed[key]; ok {
|
||||
return nil, fmt.Errorf("duplicate Zendesk integration for url %s and group id %v", new.URL, new.GroupID)
|
||||
}
|
||||
newByGroupID[new.GroupID] = new
|
||||
newIndexed[key] = new
|
||||
|
||||
// check if existing integration is being edited
|
||||
if old, ok := oriZendeskIntgsByGroupID[new.GroupID]; ok {
|
||||
if old, ok := oriZendeskIntgsIndexed[key]; ok {
|
||||
if old == *new {
|
||||
// no further validation for unchanged integration
|
||||
continue
|
||||
@ -290,37 +298,18 @@ func ValidateZendeskIntegrations(ctx context.Context, oriZendeskIntgsByGroupID m
|
||||
|
||||
// new or updated, test it
|
||||
if err := makeTestZendeskRequest(ctx, new); err != nil {
|
||||
return fmt.Errorf("Zendesk integration at index %d: %w", i, err)
|
||||
return nil, fmt.Errorf("Zendesk integration at index %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateTeamZendeskIntegrations applies the same validations as
|
||||
// ValidateZendeskIntegrations, but for team-specific integration structs.
|
||||
func ValidateTeamZendeskIntegrations(ctx context.Context, oriTeamZendeskIntgsByGroupID map[int64]TeamZendeskIntegration, newTeamZendeskIntgs []*TeamZendeskIntegration) error {
|
||||
newZendeskIntgs := make([]*ZendeskIntegration, len(newTeamZendeskIntgs))
|
||||
for i, t := range newTeamZendeskIntgs {
|
||||
newZendeskIntgs[i] = t.ToZendeskIntegration()
|
||||
// collect any deleted integration
|
||||
for key, intg := range oriZendeskIntgsIndexed {
|
||||
intg := intg // do not take address of iteration variable
|
||||
if _, ok := newIndexed[key]; !ok {
|
||||
deleted = append(deleted, &intg)
|
||||
}
|
||||
}
|
||||
|
||||
oriZendeskIntgsByGroupID := make(map[int64]ZendeskIntegration, len(oriTeamZendeskIntgsByGroupID))
|
||||
for k, v := range oriTeamZendeskIntgsByGroupID {
|
||||
oriZendeskIntgsByGroupID[k] = *v.ToZendeskIntegration()
|
||||
}
|
||||
|
||||
if err := ValidateZendeskIntegrations(ctx, oriZendeskIntgsByGroupID, newZendeskIntgs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// assign back the newZendeskIntgs to newTeamZendeskIntgs, as they may have
|
||||
// been updated by the call and we need to pass that change back to the
|
||||
// caller
|
||||
for i, v := range newZendeskIntgs {
|
||||
teamZendesk := newTeamZendeskIntgs[i]
|
||||
*teamZendesk = v.TeamZendeskIntegration
|
||||
}
|
||||
return nil
|
||||
return deleted, nil
|
||||
}
|
||||
|
||||
func makeTestZendeskRequest(ctx context.Context, intg *ZendeskIntegration) error {
|
||||
@ -422,6 +411,23 @@ func ValidateEnabledFailingPoliciesIntegrations(webhook FailingPoliciesWebhookSe
|
||||
// ValidateEnabledFailingPoliciesIntegrations, but for team-specific
|
||||
// integration structs.
|
||||
func ValidateEnabledFailingPoliciesTeamIntegrations(webhook FailingPoliciesWebhookSettings, teamIntgs TeamIntegrations, invalid *InvalidArgumentError) {
|
||||
intgs := teamIntgs.ToIntegrations()
|
||||
intgs := Integrations{
|
||||
Jira: make([]*JiraIntegration, len(teamIntgs.Jira)),
|
||||
Zendesk: make([]*ZendeskIntegration, len(teamIntgs.Zendesk)),
|
||||
}
|
||||
for i, j := range teamIntgs.Jira {
|
||||
intgs.Jira[i] = &JiraIntegration{
|
||||
URL: j.URL,
|
||||
ProjectKey: j.ProjectKey,
|
||||
EnableFailingPolicies: j.EnableFailingPolicies,
|
||||
}
|
||||
}
|
||||
for i, z := range teamIntgs.Zendesk {
|
||||
intgs.Zendesk[i] = &ZendeskIntegration{
|
||||
URL: z.URL,
|
||||
GroupID: z.GroupID,
|
||||
EnableFailingPolicies: z.EnableFailingPolicies,
|
||||
}
|
||||
}
|
||||
ValidateEnabledFailingPoliciesIntegrations(webhook, intgs, invalid)
|
||||
}
|
||||
|
@ -272,6 +272,8 @@ type SearchTeamsFunc func(ctx context.Context, filter fleet.TeamFilter, matchQue
|
||||
|
||||
type TeamEnrollSecretsFunc func(ctx context.Context, teamID uint) ([]*fleet.EnrollSecret, error)
|
||||
|
||||
type DeleteIntegrationsFromTeamsFunc func(ctx context.Context, deletedIntgs fleet.Integrations) error
|
||||
|
||||
type ListSoftwareForVulnDetectionFunc func(ctx context.Context, hostID uint) ([]fleet.Software, error)
|
||||
|
||||
type ListSoftwareVulnerabilitiesFunc func(ctx context.Context, hostIDs []uint) (map[uint][]fleet.SoftwareVulnerability, error)
|
||||
@ -803,6 +805,9 @@ type DataStore struct {
|
||||
TeamEnrollSecretsFunc TeamEnrollSecretsFunc
|
||||
TeamEnrollSecretsFuncInvoked bool
|
||||
|
||||
DeleteIntegrationsFromTeamsFunc DeleteIntegrationsFromTeamsFunc
|
||||
DeleteIntegrationsFromTeamsFuncInvoked bool
|
||||
|
||||
ListSoftwareForVulnDetectionFunc ListSoftwareForVulnDetectionFunc
|
||||
ListSoftwareForVulnDetectionFuncInvoked bool
|
||||
|
||||
@ -1664,6 +1669,11 @@ func (s *DataStore) TeamEnrollSecrets(ctx context.Context, teamID uint) ([]*flee
|
||||
return s.TeamEnrollSecretsFunc(ctx, teamID)
|
||||
}
|
||||
|
||||
func (s *DataStore) DeleteIntegrationsFromTeams(ctx context.Context, deletedIntgs fleet.Integrations) error {
|
||||
s.DeleteIntegrationsFromTeamsFuncInvoked = true
|
||||
return s.DeleteIntegrationsFromTeamsFunc(ctx, deletedIntgs)
|
||||
}
|
||||
|
||||
func (s *DataStore) ListSoftwareForVulnDetection(ctx context.Context, hostID uint) ([]fleet.Software, error) {
|
||||
s.ListSoftwareForVulnDetectionFuncInvoked = true
|
||||
return s.ListSoftwareForVulnDetectionFunc(ctx, hostID)
|
||||
|
@ -79,7 +79,7 @@ func TriggerFailingPoliciesAutomation(
|
||||
}
|
||||
|
||||
// prepare the per-team configuration caches
|
||||
getTeam := makeTeamConfigCache(ds)
|
||||
getTeam := makeTeamConfigCache(ds, appConfig.Integrations)
|
||||
|
||||
policySets, err := failingPoliciesSet.ListSets()
|
||||
if err != nil {
|
||||
@ -150,8 +150,9 @@ func TriggerFailingPoliciesAutomation(
|
||||
return nil
|
||||
}
|
||||
|
||||
func makeTeamConfigCache(ds fleet.Datastore) func(ctx context.Context, teamID uint) (FailingPolicyAutomationConfig, error) {
|
||||
func makeTeamConfigCache(ds fleet.Datastore, globalIntgs fleet.Integrations) func(ctx context.Context, teamID uint) (FailingPolicyAutomationConfig, error) {
|
||||
teamCfgs := make(map[uint]FailingPolicyAutomationConfig)
|
||||
|
||||
return func(ctx context.Context, teamID uint) (FailingPolicyAutomationConfig, error) {
|
||||
cfg, ok := teamCfgs[teamID]
|
||||
if ok {
|
||||
@ -163,7 +164,12 @@ func makeTeamConfigCache(ds fleet.Datastore) func(ctx context.Context, teamID ui
|
||||
return cfg, ctxerr.Wrapf(ctx, err, "get team: %d", teamID)
|
||||
}
|
||||
|
||||
teamAutomation := getActiveAutomation(team.Config.WebhookSettings.FailingPoliciesWebhook, team.Config.Integrations.ToIntegrations())
|
||||
intgs, err := team.Config.Integrations.MatchWithIntegrations(globalIntgs)
|
||||
if err != nil {
|
||||
return cfg, ctxerr.Wrap(ctx, err, "map team integrations to global integrations")
|
||||
}
|
||||
|
||||
teamAutomation := getActiveAutomation(team.Config.WebhookSettings.FailingPoliciesWebhook, intgs)
|
||||
teamCfg := FailingPolicyAutomationConfig{
|
||||
AutomationType: teamAutomation,
|
||||
}
|
||||
|
@ -27,7 +27,8 @@ func TestTriggerFailingPolicies(t *testing.T) {
|
||||
// pol-teamB-{7-9}: team B policies (only 7 and 8 is enabled), ids 7-8-9
|
||||
// pol-teamC-10: team C policy, team does not exist, id 10
|
||||
// pol-unknown-11: policy that does not exist anymore, id 11
|
||||
// pol-teamD-{12-14}: team C policies (only 12 and 13 is enabled), ids 12-13-14
|
||||
// pol-teamD-{12-14}: team D policies (only 12 and 13 is enabled), ids 12-13-14
|
||||
// pol-teamE-15: team E policy, integration does not exist at the global level
|
||||
//
|
||||
// Global config uses the webhook, team A a Jira integration, team B a
|
||||
// Zendesk integration, team D a webhook.
|
||||
@ -46,6 +47,7 @@ func TestTriggerFailingPolicies(t *testing.T) {
|
||||
12: {ID: 12, Name: "pol-teamD-12", TeamID: ptr.Uint(4)},
|
||||
13: {ID: 13, Name: "pol-teamD-13", TeamID: ptr.Uint(4)},
|
||||
14: {ID: 14, Name: "pol-teamD-14", TeamID: ptr.Uint(4)},
|
||||
15: {ID: 15, Name: "pol-teamE-15", TeamID: ptr.Uint(5)},
|
||||
}
|
||||
ds.PolicyFunc = func(ctx context.Context, id uint) (*fleet.Policy, error) {
|
||||
pd, ok := pols[id]
|
||||
@ -64,7 +66,7 @@ func TestTriggerFailingPolicies(t *testing.T) {
|
||||
},
|
||||
Integrations: fleet.TeamIntegrations{
|
||||
Jira: []*fleet.TeamJiraIntegration{
|
||||
{EnableFailingPolicies: true},
|
||||
{URL: "http://j.com", ProjectKey: "A", EnableFailingPolicies: true},
|
||||
},
|
||||
},
|
||||
}},
|
||||
@ -76,7 +78,7 @@ func TestTriggerFailingPolicies(t *testing.T) {
|
||||
},
|
||||
Integrations: fleet.TeamIntegrations{
|
||||
Zendesk: []*fleet.TeamZendeskIntegration{
|
||||
{EnableFailingPolicies: true},
|
||||
{URL: "http://z.com", GroupID: 1, EnableFailingPolicies: true},
|
||||
},
|
||||
},
|
||||
}},
|
||||
@ -89,10 +91,22 @@ func TestTriggerFailingPolicies(t *testing.T) {
|
||||
},
|
||||
Integrations: fleet.TeamIntegrations{
|
||||
Zendesk: []*fleet.TeamZendeskIntegration{
|
||||
{EnableFailingPolicies: false},
|
||||
{URL: "http://z.com", GroupID: 1, EnableFailingPolicies: false},
|
||||
},
|
||||
Jira: []*fleet.TeamJiraIntegration{
|
||||
{EnableFailingPolicies: false},
|
||||
{URL: "http://j.com", ProjectKey: "A", EnableFailingPolicies: false},
|
||||
},
|
||||
},
|
||||
}},
|
||||
5: {ID: 5, Name: "teamE", Config: fleet.TeamConfig{
|
||||
WebhookSettings: fleet.TeamWebhookSettings{
|
||||
FailingPoliciesWebhook: fleet.FailingPoliciesWebhookSettings{
|
||||
PolicyIDs: []uint{15},
|
||||
},
|
||||
},
|
||||
Integrations: fleet.TeamIntegrations{
|
||||
Zendesk: []*fleet.TeamZendeskIntegration{
|
||||
{URL: "http://notexist", GroupID: 999, EnableFailingPolicies: true},
|
||||
},
|
||||
},
|
||||
}},
|
||||
@ -113,6 +127,14 @@ func TestTriggerFailingPolicies(t *testing.T) {
|
||||
PolicyIDs: []uint{1, 2},
|
||||
},
|
||||
},
|
||||
Integrations: fleet.Integrations{
|
||||
Jira: []*fleet.JiraIntegration{
|
||||
{URL: "http://j.com", ProjectKey: "A", Username: "jirauser", APIToken: "secret"},
|
||||
},
|
||||
Zendesk: []*fleet.ZendeskIntegration{
|
||||
{URL: "http://z.com", GroupID: 1, Email: "zendesk@z.com", APIToken: "secret"},
|
||||
},
|
||||
},
|
||||
ServerSettings: fleet.ServerSettings{
|
||||
ServerURL: "https://fleet.example.com",
|
||||
},
|
||||
@ -167,11 +189,17 @@ func TestTriggerFailingPolicies(t *testing.T) {
|
||||
// failing policies set is now cleared
|
||||
polSets, err := failingPolicySet.ListSets()
|
||||
require.NoError(t, err)
|
||||
var countHosts int
|
||||
var remainingHosts []uint
|
||||
for _, set := range polSets {
|
||||
hosts, err := failingPolicySet.ListHosts(set)
|
||||
require.NoError(t, err)
|
||||
countHosts += len(hosts)
|
||||
for _, h := range hosts {
|
||||
remainingHosts = append(remainingHosts, h.ID)
|
||||
}
|
||||
}
|
||||
require.Zero(t, countHosts)
|
||||
// there's one remaining host ID in the failing policy sets, and it's the one
|
||||
// with the invalid integration (it did not remove the failing policy set so
|
||||
// that it can retry once the integration is fixed).
|
||||
require.Len(t, remainingHosts, 1)
|
||||
require.Equal(t, remainingHosts[0], uint(15)) // host id used is the same as the policy id
|
||||
}
|
||||
|
@ -247,7 +247,8 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte) (*fleet.AppCo
|
||||
appConfig.SMTPSettings.SMTPConfigured = false
|
||||
}
|
||||
|
||||
if err := fleet.ValidateJiraIntegrations(ctx, storedJiraByProjectKey, newAppConfig.Integrations.Jira); err != nil {
|
||||
delJira, err := fleet.ValidateJiraIntegrations(ctx, storedJiraByProjectKey, newAppConfig.Integrations.Jira)
|
||||
if err != nil {
|
||||
if errors.As(err, &fleet.IntegrationTestError{}) {
|
||||
return nil, ctxerr.Wrap(ctx, &badRequestError{message: err.Error()})
|
||||
}
|
||||
@ -255,7 +256,8 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte) (*fleet.AppCo
|
||||
}
|
||||
appConfig.Integrations.Jira = newAppConfig.Integrations.Jira
|
||||
|
||||
if err := fleet.ValidateZendeskIntegrations(ctx, storedZendeskByGroupID, newAppConfig.Integrations.Zendesk); err != nil {
|
||||
delZendesk, err := fleet.ValidateZendeskIntegrations(ctx, storedZendeskByGroupID, newAppConfig.Integrations.Zendesk)
|
||||
if err != nil {
|
||||
if errors.As(err, &fleet.IntegrationTestError{}) {
|
||||
return nil, ctxerr.Wrap(ctx, &badRequestError{message: err.Error()})
|
||||
}
|
||||
@ -263,6 +265,13 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte) (*fleet.AppCo
|
||||
}
|
||||
appConfig.Integrations.Zendesk = newAppConfig.Integrations.Zendesk
|
||||
|
||||
// if any integration was deleted, remove it from any team that uses it
|
||||
if len(delJira)+len(delZendesk) > 0 {
|
||||
if err := svc.ds.DeleteIntegrationsFromTeams(ctx, fleet.Integrations{Jira: delJira, Zendesk: delZendesk}); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "delete integrations from teams")
|
||||
}
|
||||
}
|
||||
|
||||
transparencyURL := appConfig.FleetDesktop.TransparencyURL
|
||||
if transparencyURL != "" && license.Tier != "premium" {
|
||||
invalid.Append("transparency_url", ErrMissingLicense.Error())
|
||||
|
@ -291,10 +291,10 @@ func TestAppConfigSecretsObfuscated(t *testing.T) {
|
||||
SMTPSettings: fleet.SMTPSettings{SMTPPassword: "smtppassword"},
|
||||
Integrations: fleet.Integrations{
|
||||
Jira: []*fleet.JiraIntegration{
|
||||
{TeamJiraIntegration: fleet.TeamJiraIntegration{APIToken: "jiratoken"}},
|
||||
{APIToken: "jiratoken"},
|
||||
},
|
||||
Zendesk: []*fleet.ZendeskIntegration{
|
||||
{TeamZendeskIntegration: fleet.TeamZendeskIntegration{APIToken: "zendesktoken"}},
|
||||
{APIToken: "zendesktoken"},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
|
@ -493,13 +493,42 @@ func (s *integrationEnterpriseTestSuite) TestExternalIntegrationsTeamConfig() {
|
||||
require.True(t, tmResp.Team.Config.WebhookSettings.FailingPoliciesWebhook.Enable)
|
||||
require.Equal(t, "http://example.com", tmResp.Team.Config.WebhookSettings.FailingPoliciesWebhook.DestinationURL)
|
||||
|
||||
// add an unknown automation - does not exist at the global level
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{Integrations: &fleet.TeamIntegrations{
|
||||
Jira: []*fleet.TeamJiraIntegration{
|
||||
{
|
||||
URL: srvURL,
|
||||
ProjectKey: "qux",
|
||||
EnableFailingPolicies: false,
|
||||
},
|
||||
},
|
||||
}}, http.StatusUnprocessableEntity, &tmResp)
|
||||
|
||||
// add a couple Jira integrations at the global level (qux and qux2)
|
||||
s.DoRaw("PATCH", "/api/v1/fleet/config", []byte(fmt.Sprintf(`{
|
||||
"integrations": {
|
||||
"jira": [
|
||||
{
|
||||
"url": %q,
|
||||
"username": "ok",
|
||||
"api_token": "foo",
|
||||
"project_key": "qux"
|
||||
},
|
||||
{
|
||||
"url": %[1]q,
|
||||
"username": "ok",
|
||||
"api_token": "foo",
|
||||
"project_key": "qux2"
|
||||
}
|
||||
]
|
||||
}
|
||||
}`, srvURL)), http.StatusOK)
|
||||
|
||||
// enable an automation - should fail as the webhook is enabled too
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{Integrations: &fleet.TeamIntegrations{
|
||||
Jira: []*fleet.TeamJiraIntegration{
|
||||
{
|
||||
URL: srvURL,
|
||||
Username: "ok",
|
||||
APIToken: "foo",
|
||||
ProjectKey: "qux",
|
||||
EnableFailingPolicies: true,
|
||||
},
|
||||
@ -512,14 +541,12 @@ func (s *integrationEnterpriseTestSuite) TestExternalIntegrationsTeamConfig() {
|
||||
require.Len(t, getResp.Team.Config.Integrations.Jira, 0)
|
||||
require.Len(t, getResp.Team.Config.Integrations.Zendesk, 0)
|
||||
|
||||
// disable the webhook and enable the automation, should work with valid user
|
||||
// disable the webhook and enable the automation
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{
|
||||
Integrations: &fleet.TeamIntegrations{
|
||||
Jira: []*fleet.TeamJiraIntegration{
|
||||
{
|
||||
URL: srvURL,
|
||||
Username: "ok",
|
||||
APIToken: "foo",
|
||||
ProjectKey: "qux",
|
||||
EnableFailingPolicies: true,
|
||||
},
|
||||
@ -533,7 +560,7 @@ func (s *integrationEnterpriseTestSuite) TestExternalIntegrationsTeamConfig() {
|
||||
},
|
||||
}, http.StatusOK, &tmResp)
|
||||
require.Len(t, tmResp.Team.Config.Integrations.Jira, 1)
|
||||
require.Equal(t, fleet.MaskedPassword, tmResp.Team.Config.Integrations.Jira[0].APIToken)
|
||||
require.Equal(t, "qux", tmResp.Team.Config.Integrations.Jira[0].ProjectKey)
|
||||
|
||||
// enable the webhook without changing the integration should fail (an integration is already enabled)
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{WebhookSettings: &fleet.TeamWebhookSettings{
|
||||
@ -549,15 +576,11 @@ func (s *integrationEnterpriseTestSuite) TestExternalIntegrationsTeamConfig() {
|
||||
Jira: []*fleet.TeamJiraIntegration{
|
||||
{
|
||||
URL: srvURL,
|
||||
Username: "ok",
|
||||
APIToken: "foo",
|
||||
ProjectKey: "qux",
|
||||
EnableFailingPolicies: true,
|
||||
},
|
||||
{
|
||||
URL: srvURL,
|
||||
Username: "ok",
|
||||
APIToken: "foo2",
|
||||
ProjectKey: "qux2",
|
||||
EnableFailingPolicies: false,
|
||||
},
|
||||
@ -565,8 +588,8 @@ func (s *integrationEnterpriseTestSuite) TestExternalIntegrationsTeamConfig() {
|
||||
},
|
||||
}, http.StatusOK, &tmResp)
|
||||
require.Len(t, tmResp.Team.Config.Integrations.Jira, 2)
|
||||
require.Equal(t, fleet.MaskedPassword, tmResp.Team.Config.Integrations.Jira[0].APIToken)
|
||||
require.Equal(t, fleet.MaskedPassword, tmResp.Team.Config.Integrations.Jira[1].APIToken)
|
||||
require.Equal(t, "qux", tmResp.Team.Config.Integrations.Jira[0].ProjectKey)
|
||||
require.Equal(t, "qux2", tmResp.Team.Config.Integrations.Jira[1].ProjectKey)
|
||||
|
||||
// enabling the second without disabling the first fails
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{
|
||||
@ -574,15 +597,11 @@ func (s *integrationEnterpriseTestSuite) TestExternalIntegrationsTeamConfig() {
|
||||
Jira: []*fleet.TeamJiraIntegration{
|
||||
{
|
||||
URL: srvURL,
|
||||
Username: "ok",
|
||||
APIToken: "foo",
|
||||
ProjectKey: "qux",
|
||||
EnableFailingPolicies: true,
|
||||
},
|
||||
{
|
||||
URL: srvURL,
|
||||
Username: "ok",
|
||||
APIToken: "foo2",
|
||||
ProjectKey: "qux2",
|
||||
EnableFailingPolicies: true,
|
||||
},
|
||||
@ -590,65 +609,17 @@ func (s *integrationEnterpriseTestSuite) TestExternalIntegrationsTeamConfig() {
|
||||
},
|
||||
}, http.StatusUnprocessableEntity, &tmResp)
|
||||
|
||||
// updating the integration with invalid credentials fails
|
||||
// updating to use the same project key fails (must be unique)
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{
|
||||
Integrations: &fleet.TeamIntegrations{
|
||||
Jira: []*fleet.TeamJiraIntegration{
|
||||
{
|
||||
URL: srvURL,
|
||||
Username: "fail",
|
||||
APIToken: "foo",
|
||||
ProjectKey: "qux",
|
||||
EnableFailingPolicies: true,
|
||||
},
|
||||
{
|
||||
URL: srvURL,
|
||||
Username: "ok",
|
||||
APIToken: "foo2",
|
||||
ProjectKey: "qux2",
|
||||
EnableFailingPolicies: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, http.StatusBadRequest, &tmResp)
|
||||
|
||||
// updating the integration with invalid credentials fails even if the integration is disabled
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{
|
||||
Integrations: &fleet.TeamIntegrations{
|
||||
Jira: []*fleet.TeamJiraIntegration{
|
||||
{
|
||||
URL: srvURL,
|
||||
Username: "ok",
|
||||
APIToken: "foo",
|
||||
ProjectKey: "qux",
|
||||
EnableFailingPolicies: true,
|
||||
},
|
||||
{
|
||||
URL: srvURL,
|
||||
Username: "fail",
|
||||
APIToken: "foo2",
|
||||
ProjectKey: "qux2",
|
||||
EnableFailingPolicies: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, http.StatusBadRequest, &tmResp)
|
||||
|
||||
// updating to use the same project key fails (must be unique per project key)
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{
|
||||
Integrations: &fleet.TeamIntegrations{
|
||||
Jira: []*fleet.TeamJiraIntegration{
|
||||
{
|
||||
URL: srvURL,
|
||||
Username: "ok",
|
||||
APIToken: "foo",
|
||||
ProjectKey: "qux",
|
||||
EnableFailingPolicies: true,
|
||||
},
|
||||
{
|
||||
URL: srvURL,
|
||||
Username: "ok",
|
||||
APIToken: "foo2",
|
||||
ProjectKey: "qux",
|
||||
EnableFailingPolicies: false,
|
||||
},
|
||||
@ -656,36 +627,12 @@ func (s *integrationEnterpriseTestSuite) TestExternalIntegrationsTeamConfig() {
|
||||
},
|
||||
}, http.StatusUnprocessableEntity, &tmResp)
|
||||
|
||||
// unknown project key fails
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{
|
||||
Integrations: &fleet.TeamIntegrations{
|
||||
Jira: []*fleet.TeamJiraIntegration{
|
||||
{
|
||||
URL: srvURL,
|
||||
Username: "ok",
|
||||
APIToken: "foo",
|
||||
ProjectKey: "qux",
|
||||
EnableFailingPolicies: true,
|
||||
},
|
||||
{
|
||||
URL: srvURL,
|
||||
Username: "ok",
|
||||
APIToken: "foo2",
|
||||
ProjectKey: "nosuchproject",
|
||||
EnableFailingPolicies: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, http.StatusBadRequest, &tmResp)
|
||||
|
||||
// remove second integration, disable first so that nothing is enabled now
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{
|
||||
Integrations: &fleet.TeamIntegrations{
|
||||
Jira: []*fleet.TeamJiraIntegration{
|
||||
{
|
||||
URL: srvURL,
|
||||
Username: "ok",
|
||||
APIToken: "foo",
|
||||
ProjectKey: "qux",
|
||||
EnableFailingPolicies: false,
|
||||
},
|
||||
@ -704,27 +651,68 @@ func (s *integrationEnterpriseTestSuite) TestExternalIntegrationsTeamConfig() {
|
||||
// set environmental varible to use Zendesk test client
|
||||
os.Setenv("TEST_ZENDESK_CLIENT", "true")
|
||||
|
||||
// add an unknown automation - does not exist at the global level
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{Integrations: &fleet.TeamIntegrations{
|
||||
Zendesk: []*fleet.TeamZendeskIntegration{
|
||||
{
|
||||
URL: srvURL,
|
||||
GroupID: 122,
|
||||
EnableFailingPolicies: false,
|
||||
},
|
||||
},
|
||||
}}, http.StatusUnprocessableEntity, &tmResp)
|
||||
|
||||
// add a couple Zendesk integrations at the global level (122 and 123), keep the jira ones too
|
||||
s.DoRaw("PATCH", "/api/v1/fleet/config", []byte(fmt.Sprintf(`{
|
||||
"integrations": {
|
||||
"zendesk": [
|
||||
{
|
||||
"url": %q,
|
||||
"email": "a@b.c",
|
||||
"api_token": "ok",
|
||||
"group_id": 122
|
||||
},
|
||||
{
|
||||
"url": %[1]q,
|
||||
"email": "b@b.c",
|
||||
"api_token": "ok",
|
||||
"group_id": 123
|
||||
}
|
||||
],
|
||||
"jira": [
|
||||
{
|
||||
"url": %[1]q,
|
||||
"username": "ok",
|
||||
"api_token": "foo",
|
||||
"project_key": "qux"
|
||||
},
|
||||
{
|
||||
"url": %[1]q,
|
||||
"username": "ok",
|
||||
"api_token": "foo",
|
||||
"project_key": "qux2"
|
||||
}
|
||||
]
|
||||
}
|
||||
}`, srvURL)), http.StatusOK)
|
||||
|
||||
// enable a Zendesk automation - should fail as the webhook is enabled too
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{Integrations: &fleet.TeamIntegrations{
|
||||
Zendesk: []*fleet.TeamZendeskIntegration{
|
||||
{
|
||||
URL: srvURL,
|
||||
Email: "a@b.c",
|
||||
APIToken: "ok",
|
||||
GroupID: 122,
|
||||
EnableFailingPolicies: true,
|
||||
},
|
||||
},
|
||||
}}, http.StatusUnprocessableEntity, &tmResp)
|
||||
|
||||
// disable the webhook and enable the automation, should work with valid user
|
||||
// disable the webhook and enable the automation
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{
|
||||
Integrations: &fleet.TeamIntegrations{
|
||||
Zendesk: []*fleet.TeamZendeskIntegration{
|
||||
{
|
||||
URL: srvURL,
|
||||
Email: "a@b.c",
|
||||
APIToken: "ok",
|
||||
GroupID: 122,
|
||||
EnableFailingPolicies: true,
|
||||
},
|
||||
@ -738,7 +726,7 @@ func (s *integrationEnterpriseTestSuite) TestExternalIntegrationsTeamConfig() {
|
||||
},
|
||||
}, http.StatusOK, &tmResp)
|
||||
require.Len(t, tmResp.Team.Config.Integrations.Zendesk, 1)
|
||||
require.Equal(t, fleet.MaskedPassword, tmResp.Team.Config.Integrations.Zendesk[0].APIToken)
|
||||
require.Equal(t, int64(122), tmResp.Team.Config.Integrations.Zendesk[0].GroupID)
|
||||
|
||||
// enable the webhook without changing the integration should fail (an integration is already enabled)
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{WebhookSettings: &fleet.TeamWebhookSettings{
|
||||
@ -754,15 +742,11 @@ func (s *integrationEnterpriseTestSuite) TestExternalIntegrationsTeamConfig() {
|
||||
Zendesk: []*fleet.TeamZendeskIntegration{
|
||||
{
|
||||
URL: srvURL,
|
||||
Email: "a@b.c",
|
||||
APIToken: "ok",
|
||||
GroupID: 122,
|
||||
EnableFailingPolicies: true,
|
||||
},
|
||||
{
|
||||
URL: srvURL,
|
||||
Email: "b@b.c",
|
||||
APIToken: "ok",
|
||||
GroupID: 123,
|
||||
EnableFailingPolicies: false,
|
||||
},
|
||||
@ -770,8 +754,8 @@ func (s *integrationEnterpriseTestSuite) TestExternalIntegrationsTeamConfig() {
|
||||
},
|
||||
}, http.StatusOK, &tmResp)
|
||||
require.Len(t, tmResp.Team.Config.Integrations.Zendesk, 2)
|
||||
require.Equal(t, fleet.MaskedPassword, tmResp.Team.Config.Integrations.Zendesk[0].APIToken)
|
||||
require.Equal(t, fleet.MaskedPassword, tmResp.Team.Config.Integrations.Zendesk[1].APIToken)
|
||||
require.Equal(t, int64(122), tmResp.Team.Config.Integrations.Zendesk[0].GroupID)
|
||||
require.Equal(t, int64(123), tmResp.Team.Config.Integrations.Zendesk[1].GroupID)
|
||||
|
||||
// enabling the second without disabling the first fails
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{
|
||||
@ -779,15 +763,11 @@ func (s *integrationEnterpriseTestSuite) TestExternalIntegrationsTeamConfig() {
|
||||
Zendesk: []*fleet.TeamZendeskIntegration{
|
||||
{
|
||||
URL: srvURL,
|
||||
Email: "a@b.c",
|
||||
APIToken: "ok",
|
||||
GroupID: 122,
|
||||
EnableFailingPolicies: true,
|
||||
},
|
||||
{
|
||||
URL: srvURL,
|
||||
Email: "b@b.c",
|
||||
APIToken: "ok",
|
||||
GroupID: 123,
|
||||
EnableFailingPolicies: true,
|
||||
},
|
||||
@ -795,65 +775,17 @@ func (s *integrationEnterpriseTestSuite) TestExternalIntegrationsTeamConfig() {
|
||||
},
|
||||
}, http.StatusUnprocessableEntity, &tmResp)
|
||||
|
||||
// updating the integration with invalid credentials fails
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{
|
||||
Integrations: &fleet.TeamIntegrations{
|
||||
Zendesk: []*fleet.TeamZendeskIntegration{
|
||||
{
|
||||
URL: srvURL,
|
||||
Email: "a@b.c",
|
||||
APIToken: "fail",
|
||||
GroupID: 122,
|
||||
EnableFailingPolicies: true,
|
||||
},
|
||||
{
|
||||
URL: srvURL,
|
||||
Email: "b@b.c",
|
||||
APIToken: "ok",
|
||||
GroupID: 123,
|
||||
EnableFailingPolicies: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, http.StatusBadRequest, &tmResp)
|
||||
|
||||
// updating the integration with invalid credentials fails, even if the integration is disabled
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{
|
||||
Integrations: &fleet.TeamIntegrations{
|
||||
Zendesk: []*fleet.TeamZendeskIntegration{
|
||||
{
|
||||
URL: srvURL,
|
||||
Email: "a@b.c",
|
||||
APIToken: "ok",
|
||||
GroupID: 122,
|
||||
EnableFailingPolicies: true,
|
||||
},
|
||||
{
|
||||
URL: srvURL,
|
||||
Email: "b@b.c",
|
||||
APIToken: "fail",
|
||||
GroupID: 123,
|
||||
EnableFailingPolicies: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, http.StatusBadRequest, &tmResp)
|
||||
|
||||
// updating to use the same group ID fails (must be unique per group ID)
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{
|
||||
Integrations: &fleet.TeamIntegrations{
|
||||
Zendesk: []*fleet.TeamZendeskIntegration{
|
||||
{
|
||||
URL: srvURL,
|
||||
Email: "a@b.c",
|
||||
APIToken: "ok",
|
||||
GroupID: 123,
|
||||
EnableFailingPolicies: true,
|
||||
},
|
||||
{
|
||||
URL: srvURL,
|
||||
Email: "b@b.c",
|
||||
APIToken: "ok",
|
||||
GroupID: 123,
|
||||
EnableFailingPolicies: false,
|
||||
},
|
||||
@ -861,36 +793,12 @@ func (s *integrationEnterpriseTestSuite) TestExternalIntegrationsTeamConfig() {
|
||||
},
|
||||
}, http.StatusUnprocessableEntity, &tmResp)
|
||||
|
||||
// unknown group ID fails
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{
|
||||
Integrations: &fleet.TeamIntegrations{
|
||||
Zendesk: []*fleet.TeamZendeskIntegration{
|
||||
{
|
||||
URL: srvURL,
|
||||
Email: "a@b.c",
|
||||
APIToken: "ok",
|
||||
GroupID: 122,
|
||||
EnableFailingPolicies: true,
|
||||
},
|
||||
{
|
||||
URL: srvURL,
|
||||
Email: "b@b.c",
|
||||
APIToken: "ok",
|
||||
GroupID: 999,
|
||||
EnableFailingPolicies: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, http.StatusBadRequest, &tmResp)
|
||||
|
||||
// remove second Zendesk integration, add disabled Jira integration
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{
|
||||
Integrations: &fleet.TeamIntegrations{
|
||||
Zendesk: []*fleet.TeamZendeskIntegration{
|
||||
{
|
||||
URL: srvURL,
|
||||
Email: "a@b.c",
|
||||
APIToken: "ok",
|
||||
GroupID: 122,
|
||||
EnableFailingPolicies: true,
|
||||
},
|
||||
@ -898,8 +806,6 @@ func (s *integrationEnterpriseTestSuite) TestExternalIntegrationsTeamConfig() {
|
||||
Jira: []*fleet.TeamJiraIntegration{
|
||||
{
|
||||
URL: srvURL,
|
||||
Username: "ok",
|
||||
APIToken: "foo",
|
||||
ProjectKey: "qux",
|
||||
EnableFailingPolicies: false,
|
||||
},
|
||||
@ -907,9 +813,9 @@ func (s *integrationEnterpriseTestSuite) TestExternalIntegrationsTeamConfig() {
|
||||
},
|
||||
}, http.StatusOK, &tmResp)
|
||||
require.Len(t, tmResp.Team.Config.Integrations.Jira, 1)
|
||||
require.Equal(t, fleet.MaskedPassword, tmResp.Team.Config.Integrations.Jira[0].APIToken)
|
||||
require.Equal(t, "qux", tmResp.Team.Config.Integrations.Jira[0].ProjectKey)
|
||||
require.Len(t, tmResp.Team.Config.Integrations.Zendesk, 1)
|
||||
require.Equal(t, fleet.MaskedPassword, tmResp.Team.Config.Integrations.Zendesk[0].APIToken)
|
||||
require.Equal(t, int64(122), tmResp.Team.Config.Integrations.Zendesk[0].GroupID)
|
||||
|
||||
// enabling a Jira integration when a Zendesk one is enabled fails
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{
|
||||
@ -917,8 +823,6 @@ func (s *integrationEnterpriseTestSuite) TestExternalIntegrationsTeamConfig() {
|
||||
Zendesk: []*fleet.TeamZendeskIntegration{
|
||||
{
|
||||
URL: srvURL,
|
||||
Email: "a@b.c",
|
||||
APIToken: "ok",
|
||||
GroupID: 122,
|
||||
EnableFailingPolicies: true,
|
||||
},
|
||||
@ -926,8 +830,6 @@ func (s *integrationEnterpriseTestSuite) TestExternalIntegrationsTeamConfig() {
|
||||
Jira: []*fleet.TeamJiraIntegration{
|
||||
{
|
||||
URL: srvURL,
|
||||
Username: "ok",
|
||||
APIToken: "foo",
|
||||
ProjectKey: "qux",
|
||||
EnableFailingPolicies: true,
|
||||
},
|
||||
@ -935,6 +837,140 @@ func (s *integrationEnterpriseTestSuite) TestExternalIntegrationsTeamConfig() {
|
||||
},
|
||||
}, http.StatusUnprocessableEntity, &tmResp)
|
||||
|
||||
// set additional integrations on the team
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{
|
||||
Integrations: &fleet.TeamIntegrations{
|
||||
Zendesk: []*fleet.TeamZendeskIntegration{
|
||||
{
|
||||
URL: srvURL,
|
||||
GroupID: 122,
|
||||
EnableFailingPolicies: true,
|
||||
},
|
||||
{
|
||||
URL: srvURL,
|
||||
GroupID: 123,
|
||||
EnableFailingPolicies: false,
|
||||
},
|
||||
},
|
||||
Jira: []*fleet.TeamJiraIntegration{
|
||||
{
|
||||
URL: srvURL,
|
||||
ProjectKey: "qux",
|
||||
EnableFailingPolicies: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, http.StatusOK, &tmResp)
|
||||
|
||||
// removing Zendesk 122 from the global config removes it from the team too
|
||||
s.DoRaw("PATCH", "/api/v1/fleet/config", []byte(fmt.Sprintf(`{
|
||||
"integrations": {
|
||||
"zendesk": [
|
||||
{
|
||||
"url": %[1]q,
|
||||
"email": "b@b.c",
|
||||
"api_token": "ok",
|
||||
"group_id": 123
|
||||
}
|
||||
],
|
||||
"jira": [
|
||||
{
|
||||
"url": %[1]q,
|
||||
"username": "ok",
|
||||
"api_token": "foo",
|
||||
"project_key": "qux"
|
||||
},
|
||||
{
|
||||
"url": %[1]q,
|
||||
"username": "ok",
|
||||
"api_token": "foo",
|
||||
"project_key": "qux2"
|
||||
}
|
||||
]
|
||||
}
|
||||
}`, srvURL)), http.StatusOK)
|
||||
|
||||
// get the team, only one Zendesk integration remains, none are enabled
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &getResp)
|
||||
require.Len(t, getResp.Team.Config.Integrations.Jira, 1)
|
||||
require.Equal(t, "qux", getResp.Team.Config.Integrations.Jira[0].ProjectKey)
|
||||
require.False(t, getResp.Team.Config.Integrations.Jira[0].EnableFailingPolicies)
|
||||
require.Len(t, getResp.Team.Config.Integrations.Zendesk, 1)
|
||||
require.Equal(t, int64(123), getResp.Team.Config.Integrations.Zendesk[0].GroupID)
|
||||
require.False(t, getResp.Team.Config.Integrations.Zendesk[0].EnableFailingPolicies)
|
||||
|
||||
// removing Jira qux2 from the global config does not impact the team as it is unused.
|
||||
s.DoRaw("PATCH", "/api/v1/fleet/config", []byte(fmt.Sprintf(`{
|
||||
"integrations": {
|
||||
"zendesk": [
|
||||
{
|
||||
"url": %[1]q,
|
||||
"email": "b@b.c",
|
||||
"api_token": "ok",
|
||||
"group_id": 123
|
||||
}
|
||||
],
|
||||
"jira": [
|
||||
{
|
||||
"url": %[1]q,
|
||||
"username": "ok",
|
||||
"api_token": "foo",
|
||||
"project_key": "qux"
|
||||
}
|
||||
]
|
||||
}
|
||||
}`, srvURL)), http.StatusOK)
|
||||
|
||||
// get the team, integrations are unchanged
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &getResp)
|
||||
require.Len(t, getResp.Team.Config.Integrations.Jira, 1)
|
||||
require.Equal(t, "qux", getResp.Team.Config.Integrations.Jira[0].ProjectKey)
|
||||
require.False(t, getResp.Team.Config.Integrations.Jira[0].EnableFailingPolicies)
|
||||
require.Len(t, getResp.Team.Config.Integrations.Zendesk, 1)
|
||||
require.Equal(t, int64(123), getResp.Team.Config.Integrations.Zendesk[0].GroupID)
|
||||
require.False(t, getResp.Team.Config.Integrations.Zendesk[0].EnableFailingPolicies)
|
||||
|
||||
// enable Jira qux for the team
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{
|
||||
Integrations: &fleet.TeamIntegrations{
|
||||
Zendesk: []*fleet.TeamZendeskIntegration{
|
||||
{
|
||||
URL: srvURL,
|
||||
GroupID: 123,
|
||||
EnableFailingPolicies: false,
|
||||
},
|
||||
},
|
||||
Jira: []*fleet.TeamJiraIntegration{
|
||||
{
|
||||
URL: srvURL,
|
||||
ProjectKey: "qux",
|
||||
EnableFailingPolicies: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, http.StatusOK, &tmResp)
|
||||
|
||||
// removing Zendesk 123 from the global config removes it from the team but
|
||||
// leaves the Jira integration enabled.
|
||||
s.DoRaw("PATCH", "/api/v1/fleet/config", []byte(fmt.Sprintf(`{
|
||||
"integrations": {
|
||||
"jira": [
|
||||
{
|
||||
"url": %[1]q,
|
||||
"username": "ok",
|
||||
"api_token": "foo",
|
||||
"project_key": "qux"
|
||||
}
|
||||
]
|
||||
}
|
||||
}`, srvURL)), http.StatusOK)
|
||||
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &getResp)
|
||||
require.Len(t, getResp.Team.Config.Integrations.Jira, 1)
|
||||
require.Equal(t, "qux", getResp.Team.Config.Integrations.Jira[0].ProjectKey)
|
||||
require.True(t, getResp.Team.Config.Integrations.Jira[0].EnableFailingPolicies)
|
||||
require.Len(t, getResp.Team.Config.Integrations.Zendesk, 0)
|
||||
|
||||
// remove all integrations on exit, so that other tests can enable the
|
||||
// webhook as needed
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{
|
||||
@ -948,6 +984,10 @@ func (s *integrationEnterpriseTestSuite) TestExternalIntegrationsTeamConfig() {
|
||||
require.Len(t, tmResp.Team.Config.Integrations.Zendesk, 0)
|
||||
require.False(t, tmResp.Team.Config.WebhookSettings.FailingPoliciesWebhook.Enable)
|
||||
require.Empty(t, tmResp.Team.Config.WebhookSettings.FailingPoliciesWebhook.DestinationURL)
|
||||
|
||||
s.DoRaw("PATCH", "/api/v1/fleet/config", []byte(`{
|
||||
"integrations": {}
|
||||
}`), http.StatusOK)
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestListDevicePolicies() {
|
||||
|
@ -31,13 +31,6 @@ func listTeamsEndpoint(ctx context.Context, request interface{}, svc fleet.Servi
|
||||
|
||||
resp := listTeamsResponse{Teams: []fleet.Team{}}
|
||||
for _, team := range teams {
|
||||
// mask the API token of integrations
|
||||
for _, intg := range team.Config.Integrations.Jira {
|
||||
intg.APIToken = fleet.MaskedPassword
|
||||
}
|
||||
for _, intg := range team.Config.Integrations.Zendesk {
|
||||
intg.APIToken = fleet.MaskedPassword
|
||||
}
|
||||
resp.Teams = append(resp.Teams, *team)
|
||||
}
|
||||
return resp, nil
|
||||
@ -72,14 +65,6 @@ func getTeamEndpoint(ctx context.Context, request interface{}, svc fleet.Service
|
||||
if err != nil {
|
||||
return getTeamResponse{Err: err}, nil
|
||||
}
|
||||
|
||||
// mask the API token of integrations
|
||||
for _, intg := range team.Config.Integrations.Jira {
|
||||
intg.APIToken = fleet.MaskedPassword
|
||||
}
|
||||
for _, intg := range team.Config.Integrations.Zendesk {
|
||||
intg.APIToken = fleet.MaskedPassword
|
||||
}
|
||||
return getTeamResponse{Team: team}, nil
|
||||
}
|
||||
|
||||
@ -113,14 +98,6 @@ func createTeamEndpoint(ctx context.Context, request interface{}, svc fleet.Serv
|
||||
if err != nil {
|
||||
return teamResponse{Err: err}, nil
|
||||
}
|
||||
|
||||
// mask the API token of integrations
|
||||
for _, intg := range team.Config.Integrations.Jira {
|
||||
intg.APIToken = fleet.MaskedPassword
|
||||
}
|
||||
for _, intg := range team.Config.Integrations.Zendesk {
|
||||
intg.APIToken = fleet.MaskedPassword
|
||||
}
|
||||
return teamResponse{Team: team}, nil
|
||||
}
|
||||
|
||||
@ -147,14 +124,6 @@ func modifyTeamEndpoint(ctx context.Context, request interface{}, svc fleet.Serv
|
||||
if err != nil {
|
||||
return teamResponse{Err: err}, nil
|
||||
}
|
||||
|
||||
// mask the API token of integrations
|
||||
for _, intg := range team.Config.Integrations.Jira {
|
||||
intg.APIToken = fleet.MaskedPassword
|
||||
}
|
||||
for _, intg := range team.Config.Integrations.Zendesk {
|
||||
intg.APIToken = fleet.MaskedPassword
|
||||
}
|
||||
return teamResponse{Team: team}, err
|
||||
}
|
||||
|
||||
@ -243,14 +212,6 @@ func modifyTeamAgentOptionsEndpoint(ctx context.Context, request interface{}, sv
|
||||
if err != nil {
|
||||
return teamResponse{Err: err}, nil
|
||||
}
|
||||
|
||||
// mask the API token of integrations
|
||||
for _, intg := range team.Config.Integrations.Jira {
|
||||
intg.APIToken = fleet.MaskedPassword
|
||||
}
|
||||
for _, intg := range team.Config.Integrations.Zendesk {
|
||||
intg.APIToken = fleet.MaskedPassword
|
||||
}
|
||||
return teamResponse{Team: team}, err
|
||||
}
|
||||
|
||||
@ -311,14 +272,6 @@ func addTeamUsersEndpoint(ctx context.Context, request interface{}, svc fleet.Se
|
||||
if err != nil {
|
||||
return teamResponse{Err: err}, nil
|
||||
}
|
||||
|
||||
// mask the API token of integrations
|
||||
for _, intg := range team.Config.Integrations.Jira {
|
||||
intg.APIToken = fleet.MaskedPassword
|
||||
}
|
||||
for _, intg := range team.Config.Integrations.Zendesk {
|
||||
intg.APIToken = fleet.MaskedPassword
|
||||
}
|
||||
return teamResponse{Team: team}, err
|
||||
}
|
||||
|
||||
@ -336,14 +289,6 @@ func deleteTeamUsersEndpoint(ctx context.Context, request interface{}, svc fleet
|
||||
if err != nil {
|
||||
return teamResponse{Err: err}, nil
|
||||
}
|
||||
|
||||
// mask the API token of integrations
|
||||
for _, intg := range team.Config.Integrations.Jira {
|
||||
intg.APIToken = fleet.MaskedPassword
|
||||
}
|
||||
for _, intg := range team.Config.Integrations.Zendesk {
|
||||
intg.APIToken = fleet.MaskedPassword
|
||||
}
|
||||
return teamResponse{Team: team}, err
|
||||
}
|
||||
|
||||
|
@ -126,6 +126,11 @@ func (j *Jira) getClient(ctx context.Context, args jiraArgs) (JiraClient, error)
|
||||
key += fmt.Sprint(teamID)
|
||||
}
|
||||
|
||||
ac, err := j.Datastore.AppConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// load the config that would be used to create the client first - it is
|
||||
// needed to check if an existing client is configured the same or if its
|
||||
// configuration has changed since it was created.
|
||||
@ -136,7 +141,11 @@ func (j *Jira) getClient(ctx context.Context, args jiraArgs) (JiraClient, error)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, intg := range tm.Config.Integrations.Jira {
|
||||
intgs, err := tm.Config.Integrations.MatchWithIntegrations(ac.Integrations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, intg := range intgs.Jira {
|
||||
if intgType == intgTypeFailingPolicy && intg.EnableFailingPolicies {
|
||||
opts = &externalsvc.JiraOptions{
|
||||
BaseURL: intg.URL,
|
||||
@ -148,10 +157,6 @@ func (j *Jira) getClient(ctx context.Context, args jiraArgs) (JiraClient, error)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ac, err := j.Datastore.AppConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, intg := range ac.Integrations.Jira {
|
||||
if (intgType == intgTypeVuln && intg.EnableSoftwareVulnerabilities) ||
|
||||
(intgType == intgTypeFailingPolicy && intg.EnableFailingPolicies) {
|
||||
|
@ -32,7 +32,7 @@ func TestJiraRun(t *testing.T) {
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{Integrations: fleet.Integrations{
|
||||
Jira: []*fleet.JiraIntegration{
|
||||
{EnableSoftwareVulnerabilities: true, TeamJiraIntegration: fleet.TeamJiraIntegration{EnableFailingPolicies: true}},
|
||||
{EnableSoftwareVulnerabilities: true, EnableFailingPolicies: true},
|
||||
},
|
||||
}}, nil
|
||||
}
|
||||
@ -215,7 +215,10 @@ func TestJiraRunClientUpdate(t *testing.T) {
|
||||
globalCount++
|
||||
return &fleet.AppConfig{Integrations: fleet.Integrations{
|
||||
Jira: []*fleet.JiraIntegration{
|
||||
{TeamJiraIntegration: fleet.TeamJiraIntegration{ProjectKey: "0", EnableFailingPolicies: true}},
|
||||
{ProjectKey: "0", EnableFailingPolicies: true},
|
||||
{ProjectKey: "1", EnableFailingPolicies: false}, // the team integration will use the project keys 1-3
|
||||
{ProjectKey: "2", EnableFailingPolicies: false},
|
||||
{ProjectKey: "3", EnableFailingPolicies: false},
|
||||
},
|
||||
}}, nil
|
||||
}
|
||||
@ -290,6 +293,6 @@ func TestJiraRunClientUpdate(t *testing.T) {
|
||||
|
||||
// it should've created 3 clients - the global one, and the first 2 calls with team 123
|
||||
require.Equal(t, []string{"0", "1", "2"}, projectKeys)
|
||||
require.Equal(t, 2, globalCount)
|
||||
require.Equal(t, 5, globalCount) // app config is requested every time
|
||||
require.Equal(t, 3, teamCount)
|
||||
}
|
||||
|
@ -120,6 +120,11 @@ func (z *Zendesk) getClient(ctx context.Context, args zendeskArgs) (ZendeskClien
|
||||
key += fmt.Sprint(teamID)
|
||||
}
|
||||
|
||||
ac, err := z.Datastore.AppConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// load the config that would be used to create the client first - it is
|
||||
// needed to check if an existing client is configured the same or if its
|
||||
// configuration has changed since it was created.
|
||||
@ -130,7 +135,12 @@ func (z *Zendesk) getClient(ctx context.Context, args zendeskArgs) (ZendeskClien
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, intg := range tm.Config.Integrations.Zendesk {
|
||||
intgs, err := tm.Config.Integrations.MatchWithIntegrations(ac.Integrations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, intg := range intgs.Zendesk {
|
||||
if intgType == intgTypeFailingPolicy && intg.EnableFailingPolicies {
|
||||
opts = &externalsvc.ZendeskOptions{
|
||||
URL: intg.URL,
|
||||
@ -142,10 +152,6 @@ func (z *Zendesk) getClient(ctx context.Context, args zendeskArgs) (ZendeskClien
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ac, err := z.Datastore.AppConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, intg := range ac.Integrations.Zendesk {
|
||||
if (intgType == intgTypeVuln && intg.EnableSoftwareVulnerabilities) ||
|
||||
(intgType == intgTypeFailingPolicy && intg.EnableFailingPolicies) {
|
||||
|
@ -32,7 +32,7 @@ func TestZendeskRun(t *testing.T) {
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{Integrations: fleet.Integrations{
|
||||
Zendesk: []*fleet.ZendeskIntegration{
|
||||
{EnableSoftwareVulnerabilities: true, TeamZendeskIntegration: fleet.TeamZendeskIntegration{EnableFailingPolicies: true}},
|
||||
{EnableSoftwareVulnerabilities: true, EnableFailingPolicies: true},
|
||||
},
|
||||
}}, nil
|
||||
}
|
||||
@ -203,7 +203,10 @@ func TestZendeskRunClientUpdate(t *testing.T) {
|
||||
globalCount++
|
||||
return &fleet.AppConfig{Integrations: fleet.Integrations{
|
||||
Zendesk: []*fleet.ZendeskIntegration{
|
||||
{TeamZendeskIntegration: fleet.TeamZendeskIntegration{GroupID: 0, EnableFailingPolicies: true}},
|
||||
{GroupID: 0, EnableFailingPolicies: true},
|
||||
{GroupID: 1, EnableFailingPolicies: false}, // the team integration will use the group IDs 1-3
|
||||
{GroupID: 2, EnableFailingPolicies: false},
|
||||
{GroupID: 3, EnableFailingPolicies: false},
|
||||
},
|
||||
}}, nil
|
||||
}
|
||||
@ -278,6 +281,6 @@ func TestZendeskRunClientUpdate(t *testing.T) {
|
||||
|
||||
// it should've created 3 clients - the global one, and the first 2 calls with team 123
|
||||
require.Equal(t, []int64{0, 1, 2}, groupIDs)
|
||||
require.Equal(t, 2, globalCount)
|
||||
require.Equal(t, 5, globalCount) // app config is requested every time
|
||||
require.Equal(t, 3, teamCount)
|
||||
}
|
||||
|
@ -82,13 +82,11 @@ func main() {
|
||||
Jira: []*fleet.JiraIntegration{
|
||||
{
|
||||
EnableSoftwareVulnerabilities: *cve != "",
|
||||
TeamJiraIntegration: fleet.TeamJiraIntegration{
|
||||
URL: *jiraURL,
|
||||
Username: *jiraUsername,
|
||||
APIToken: jiraPassword,
|
||||
ProjectKey: *jiraProjectKey,
|
||||
EnableFailingPolicies: *failingPolicyID > 0,
|
||||
},
|
||||
URL: *jiraURL,
|
||||
Username: *jiraUsername,
|
||||
APIToken: jiraPassword,
|
||||
ProjectKey: *jiraProjectKey,
|
||||
EnableFailingPolicies: *failingPolicyID > 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -103,8 +101,6 @@ func main() {
|
||||
Jira: []*fleet.TeamJiraIntegration{
|
||||
{
|
||||
URL: *jiraURL,
|
||||
Username: *jiraUsername,
|
||||
APIToken: jiraPassword,
|
||||
ProjectKey: *jiraProjectKey,
|
||||
EnableFailingPolicies: *failingPolicyID > 0,
|
||||
},
|
||||
|
@ -82,13 +82,11 @@ func main() {
|
||||
Zendesk: []*fleet.ZendeskIntegration{
|
||||
{
|
||||
EnableSoftwareVulnerabilities: *cve != "",
|
||||
TeamZendeskIntegration: fleet.TeamZendeskIntegration{
|
||||
URL: *zendeskURL,
|
||||
Email: *zendeskEmail,
|
||||
APIToken: zendeskToken,
|
||||
GroupID: *zendeskGroupID,
|
||||
EnableFailingPolicies: *failingPolicyID > 0,
|
||||
},
|
||||
URL: *zendeskURL,
|
||||
Email: *zendeskEmail,
|
||||
APIToken: zendeskToken,
|
||||
GroupID: *zendeskGroupID,
|
||||
EnableFailingPolicies: *failingPolicyID > 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -103,8 +101,6 @@ func main() {
|
||||
Zendesk: []*fleet.TeamZendeskIntegration{
|
||||
{
|
||||
URL: *zendeskURL,
|
||||
Email: *zendeskEmail,
|
||||
APIToken: zendeskToken,
|
||||
GroupID: *zendeskGroupID,
|
||||
EnableFailingPolicies: *failingPolicyID > 0,
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user