diff --git a/client.go b/client.go index 4a75d7b..dff410d 100644 --- a/client.go +++ b/client.go @@ -47,6 +47,7 @@ type Client struct { LDAP LDAPService License LicenseService Metrics MetricsService + Notification NotificationService OIDC OIDCService Permission PermissionService Policy PolicyService @@ -100,6 +101,7 @@ func NewClient(baseURL string, options ...ClientOption) (*Client, error) { client.LDAP = LDAPService{client: &client} client.License = LicenseService{client: &client} client.Metrics = MetricsService{client: &client} + client.Notification = NotificationService{client: &client} client.OIDC = OIDCService{client: &client} client.Permission = PermissionService{client: &client} client.Policy = PolicyService{client: &client} diff --git a/notification.go b/notification.go new file mode 100644 index 0000000..18c764e --- /dev/null +++ b/notification.go @@ -0,0 +1,343 @@ +package dtrack + +import ( + "context" + "fmt" + "net/http" + "net/url" + + "github.com/google/uuid" +) + +type NotificationService struct { + client *Client +} + +type NotificationPublisher struct { + UUID uuid.UUID `json:"uuid"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + PublisherClass string `json:"publisherClass"` + Template string `json:"template,omitempty"` + TemplateMIMEType string `json:"templateMimeType"` + DefaultPublisher bool `json:"defaultPublisher"` +} + +type NotificationRule struct { + UUID uuid.UUID `json:"uuid"` + Name string `json:"name"` + Enabled bool `json:"enabled"` + NotifyChildren bool `json:"notifyChildren"` + LogSuccessfulPublish bool `json:"logSuccessfulPublish"` + Scope NotificationRuleScope `json:"scope"` + NotificationLevel NotificationRuleLevel `json:"notificationLevel,omitempty"` + NotifyOn []NotificationRuleNotifyOn `json:"notifyOn,omitempty"` + TriggerType NotificationRuleTriggerType `json:"triggerType"` + Message string `json:"message,omitempty"` + PublisherConfig string `json:"publisherConfig,omitempty"` + ScheduleLastTriggeredAt int64 `json:"scheduleLastTriggeredAt,omitempty"` + ScheduleNextTriggerAt int64 `json:"scheduleNextTriggerAt,omitempty"` + ScheduleCron string `json:"scheduleCron,omitempty"` + ScheduleSkipUnchanged bool `json:"scheduleSkipUnchanged,omitempty"` + Publisher NotificationPublisher `json:"publisher,omitempty"` + Projects []Project `json:"projects,omitempty"` + Tags []Tag `json:"tags,omitempty"` + Teams []Team `json:"teams,omitempty"` +} + +type CreateScheduledNotificationRuleRequest struct { + Name string `json:"name"` + Scope NotificationRuleScope `json:"scope"` + NotificationLevel NotificationRuleLevel `json:"notificationLevel"` + Publisher Publisher `json:"publisher"` +} + +type Publisher struct { + UUID uuid.UUID `json:"uuid"` +} + +type NotificationRuleScope string + +const ( + NotificationRuleScopeSystem NotificationRuleScope = "SYSTEM" + NotificationRuleScopePortfolio NotificationRuleScope = "PORTFOLIO" +) + +type NotificationRuleLevel string + +const ( + NotificationRuleLevelInformational NotificationRuleLevel = "INFORMATIONAL" + NotificationRuleLevelWarning NotificationRuleLevel = "WARNING" + NotificationRuleLevelError NotificationRuleLevel = "ERROR" +) + +type NotificationRuleNotifyOn string + +type NotificationRuleTriggerType string + +const ( + NotificationRuleTriggerTypeEvent NotificationRuleTriggerType = "EVENT" + NotificationRuleTriggerTypeSchedule NotificationRuleTriggerType = "SCHEDULE" +) + +func (ns NotificationService) GetAllPublishers(ctx context.Context) (ps []NotificationPublisher, err error) { + err = ns.client.assertServerVersionAtLeast("3.2.0") + if err != nil { + return + } + + req, err := ns.client.newRequest(ctx, http.MethodGet, "api/v1/notification/publisher") + if err != nil { + return + } + + _, err = ns.client.doRequest(req, &ps) + return +} + +func (ns NotificationService) CreatePublisher(ctx context.Context, publisher NotificationPublisher) (p NotificationPublisher, err error) { + err = ns.client.assertServerVersionAtLeast("4.6.0") + if err != nil { + return + } + + req, err := ns.client.newRequest(ctx, http.MethodPut, "api/v1/notification/publisher", withBody(publisher)) + if err != nil { + return + } + + _, err = ns.client.doRequest(req, &p) + return +} + +func (ns NotificationService) UpdatePublisher(ctx context.Context, publisher NotificationPublisher) (p NotificationPublisher, err error) { + err = ns.client.assertServerVersionAtLeast("4.6.0") + if err != nil { + return + } + + req, err := ns.client.newRequest(ctx, http.MethodPost, "api/v1/notification/publisher", withBody(publisher)) + if err != nil { + return + } + + _, err = ns.client.doRequest(req, &p) + return +} + +func (ns NotificationService) DeletePublisher(ctx context.Context, publisherUUID uuid.UUID) (err error) { + err = ns.client.assertServerVersionAtLeast("4.6.0") + if err != nil { + return + } + + req, err := ns.client.newRequest(ctx, http.MethodDelete, fmt.Sprintf("api/v1/notification/publisher/%s", publisherUUID.String())) + if err != nil { + return + } + + _, err = ns.client.doRequest(req, nil) + return +} + +func (ns NotificationService) RestoreDefaultTemplates(ctx context.Context) (err error) { + err = ns.client.assertServerVersionAtLeast("4.6.0") + if err != nil { + return + } + + req, err := ns.client.newRequest(ctx, http.MethodPost, "api/v1/notification/publisher/restoreDefaultTemplates") + if err != nil { + return + } + + _, err = ns.client.doRequest(req, nil) + return +} + +func (ns NotificationService) TestRule(ctx context.Context, ruleUUID uuid.UUID) (err error) { + err = ns.client.assertServerVersionAtLeast("4.12.0") + if err != nil { + return + } + + req, err := ns.client.newRequest(ctx, http.MethodPost, fmt.Sprintf("api/v1/notification/publisher/test/%s", ruleUUID.String())) + if err != nil { + return + } + + _, err = ns.client.doRequest(req, nil) + return +} + +func (ns NotificationService) TestSMTP(ctx context.Context, destination string) (err error) { + err = ns.client.assertServerVersionAtLeast("3.4.0") + if err != nil { + return + } + values := url.Values{} + values.Set("destination", destination) + + req, err := ns.client.newRequest(ctx, http.MethodPost, "api/v1/notification/publisher/test/smtp", withBody(values)) + if err != nil { + return + } + + _, err = ns.client.doRequest(req, nil) + return +} + +func (ns NotificationService) AddProjectToRule(ctx context.Context, ruleUUID, projectUUID uuid.UUID) (r NotificationRule, err error) { + err = ns.client.assertServerVersionAtLeast("3.2.0") + if err != nil { + return + } + + req, err := ns.client.newRequest(ctx, http.MethodPost, fmt.Sprintf("api/v1/notification/rule/%s/project/%s", ruleUUID.String(), projectUUID.String())) + if err != nil { + return + } + + _, err = ns.client.doRequest(req, &r) + return +} + +func (ns NotificationService) RemoveProjectFromRule(ctx context.Context, ruleUUID, projectUUID uuid.UUID) (r NotificationRule, err error) { + err = ns.client.assertServerVersionAtLeast("3.2.0") + if err != nil { + return + } + + req, err := ns.client.newRequest(ctx, http.MethodDelete, fmt.Sprintf("api/v1/notification/rule/%s/project/%s", ruleUUID.String(), projectUUID.String())) + if err != nil { + return + } + + _, err = ns.client.doRequest(req, &r) + return +} + +func (ns NotificationService) AddTeamToRule(ctx context.Context, ruleUUID, teamUUID uuid.UUID) (r NotificationRule, err error) { + err = ns.client.assertServerVersionAtLeast("4.7.0") + if err != nil { + return + } + + req, err := ns.client.newRequest(ctx, http.MethodPost, fmt.Sprintf("api/v1/notification/rule/%s/team/%s", ruleUUID.String(), teamUUID.String())) + if err != nil { + return + } + + _, err = ns.client.doRequest(req, &r) + return +} + +func (ns NotificationService) RemoveTeamFromRule(ctx context.Context, ruleUUID, teamUUID uuid.UUID) (r NotificationRule, err error) { + err = ns.client.assertServerVersionAtLeast("4.7.0") + if err != nil { + return + } + + req, err := ns.client.newRequest(ctx, http.MethodDelete, fmt.Sprintf("api/v1/notification/rule/%s/team/%s", ruleUUID.String(), teamUUID.String())) + if err != nil { + return + } + + _, err = ns.client.doRequest(req, &r) + return +} + +type GetAllRulesFilterOptions struct { + TriggerType NotificationRuleTriggerType +} + +func withGetAllRulesFilterOptions(filterOptions GetAllRulesFilterOptions) requestOption { + return func(req *http.Request) error { + query := req.URL.Query() + if len(filterOptions.TriggerType) > 0 { + query.Set("triggerType", string(filterOptions.TriggerType)) + } + req.URL.RawQuery = query.Encode() + return nil + } +} + +func (ns NotificationService) GetAllRules(ctx context.Context, po PageOptions, so SortOptions, filterOptions GetAllRulesFilterOptions) (p Page[NotificationRule], err error) { + err = ns.client.assertServerVersionAtLeast("3.2.0") + if err != nil { + return + } + + req, err := ns.client.newRequest(ctx, http.MethodGet, "api/v1/notification/rule", withPageOptions(po), withSortOptions(so), withGetAllRulesFilterOptions(filterOptions)) + if err != nil { + return + } + + res, err := ns.client.doRequest(req, &p.Items) + if err != nil { + return + } + + p.TotalCount = res.TotalCount + return +} + +func (ns NotificationService) CreateRule(ctx context.Context, ruleReq NotificationRule) (ruleRes NotificationRule, err error) { + err = ns.client.assertServerVersionAtLeast("3.2.0") + if err != nil { + return + } + + req, err := ns.client.newRequest(ctx, http.MethodPut, "api/v1/notification/rule", withBody(ruleReq)) + if err != nil { + return + } + + _, err = ns.client.doRequest(req, &ruleRes) + return +} + +func (ns NotificationService) UpdateRule(ctx context.Context, ruleReq NotificationRule) (ruleRes NotificationRule, err error) { + err = ns.client.assertServerVersionAtLeast("3.2.0") + if err != nil { + return + } + + req, err := ns.client.newRequest(ctx, http.MethodPost, "api/v1/notification/rule", withBody(ruleReq)) + if err != nil { + return + } + + _, err = ns.client.doRequest(req, &ruleRes) + return +} + +func (ns NotificationService) DeleteRule(ctx context.Context, rule NotificationRule) (err error) { + err = ns.client.assertServerVersionAtLeast("3.2.0") + if err != nil { + return + } + + req, err := ns.client.newRequest(ctx, http.MethodDelete, "api/v1/notification/rule", withBody(rule)) + if err != nil { + return + } + + _, err = ns.client.doRequest(req, nil) + return +} + +func (ns NotificationService) CreateScheduledRule(ctx context.Context, schedule CreateScheduledNotificationRuleRequest) (r NotificationRule, err error) { + err = ns.client.assertServerVersionAtLeast("4.13.0") + if err != nil { + return + } + + req, err := ns.client.newRequest(ctx, http.MethodPut, "api/v1/notification/rule/scheduled", withBody(schedule)) + if err != nil { + return + } + + _, err = ns.client.doRequest(req, &r) + return +} diff --git a/notification_test.go b/notification_test.go new file mode 100644 index 0000000..473d01e --- /dev/null +++ b/notification_test.go @@ -0,0 +1,352 @@ +package dtrack + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPublishers(t *testing.T) { + ctx := context.Background() + client := setUpContainer(t, testContainerOptions{ + APIPermissions: []string{ + PermissionSystemConfiguration, + }, + }) + // Create + publisher, err := client.Notification.CreatePublisher(ctx, NotificationPublisher{ + Name: "Test_Publisher", + Description: "Test_Description", + PublisherClass: "org.dependencytrack.notification.publisher.ConsolePublisher", + TemplateMIMEType: "text/plain", + Template: "Test_Template", + }) + { + require.NoError(t, err) + require.NotZero(t, publisher.UUID) + require.Equal(t, publisher.Name, "Test_Publisher") + require.Equal(t, publisher.Description, "Test_Description") + require.Equal(t, publisher.PublisherClass, "org.dependencytrack.notification.publisher.ConsolePublisher") + require.Equal(t, publisher.TemplateMIMEType, "text/plain") + require.Equal(t, publisher.Template, "Test_Template") + require.Equal(t, publisher.DefaultPublisher, false) + } + // Update + { + updatedReq := publisher + updatedReq.Description = "Test_Updated_Description" + updated, err := client.Notification.UpdatePublisher(ctx, updatedReq) + require.NoError(t, err) + require.Equal(t, updated.UUID, publisher.UUID) + require.Equal(t, updated.Name, publisher.Name) + require.Equal(t, updated.Description, "Test_Updated_Description") + require.Equal(t, updated.PublisherClass, publisher.PublisherClass) + require.Equal(t, updated.Template, publisher.Template) + require.Equal(t, updated.TemplateMIMEType, publisher.TemplateMIMEType) + require.Equal(t, updated.DefaultPublisher, publisher.DefaultPublisher) + } + // Fetch + { + allPublishers, err := client.Notification.GetAllPublishers(ctx) + require.NoError(t, err) + found := NotificationPublisher{} + for _, pub := range allPublishers { + if pub.UUID == publisher.UUID { + found = pub + break + } + } + require.Equal(t, found.UUID, publisher.UUID) + require.Equal(t, found.Name, publisher.Name) + require.Equal(t, found.Description, "Test_Updated_Description") + require.Equal(t, found.PublisherClass, publisher.PublisherClass) + require.Equal(t, found.Template, publisher.Template) + require.Equal(t, found.TemplateMIMEType, publisher.TemplateMIMEType) + require.Equal(t, found.DefaultPublisher, publisher.DefaultPublisher) + } + // Delete + { + err := client.Notification.DeletePublisher(ctx, publisher.UUID) + require.NoError(t, err) + } + // Check absence + { + allPublishers, err := client.Notification.GetAllPublishers(ctx) + require.NoError(t, err) + found := NotificationPublisher{} + for _, pub := range allPublishers { + if pub.UUID == publisher.UUID { + found = pub + break + } + } + require.Zero(t, found.UUID) + } +} + +func TestRules(t *testing.T) { + ctx := context.Background() + client := setUpContainer(t, testContainerOptions{ + APIPermissions: []string{ + PermissionSystemConfiguration, + }, + }) + publisher, err := client.Notification.CreatePublisher(ctx, NotificationPublisher{ + Name: "Test_Rule_Publisher", + Description: "Test_Rule_Description", + PublisherClass: "org.dependencytrack.notification.publisher.ConsolePublisher", + TemplateMIMEType: "text/plain", + Template: "Test_Rule_Template", + }) + require.NoError(t, err) + // Create + rule, err := client.Notification.CreateRule(ctx, NotificationRule{ + Name: "Test_Rule_Name", + Scope: NotificationRuleScopePortfolio, + TriggerType: NotificationRuleTriggerTypeEvent, + Publisher: publisher, + }) + { + require.NoError(t, err) + require.NotZero(t, rule.UUID) + require.Equal(t, rule.Name, "Test_Rule_Name") + require.Equal(t, rule.Scope, NotificationRuleScopePortfolio) + require.Equal(t, rule.TriggerType, NotificationRuleTriggerTypeEvent) + require.Equal(t, rule.Enabled, true) + require.Equal(t, rule.NotifyChildren, true) + require.Equal(t, rule.LogSuccessfulPublish, false) + require.Empty(t, rule.NotificationLevel) + require.Empty(t, rule.NotifyOn) + require.Empty(t, rule.Message) + require.Empty(t, rule.PublisherConfig) + require.Empty(t, rule.ScheduleLastTriggeredAt) + require.Empty(t, rule.ScheduleNextTriggerAt) + require.Empty(t, rule.ScheduleCron) + require.Empty(t, rule.ScheduleSkipUnchanged) + require.Equal(t, rule.Publisher.UUID, publisher.UUID) + require.Empty(t, rule.Projects) + require.Empty(t, rule.Tags) + require.Empty(t, rule.Teams) + } + // Update + { + updatedReq := rule + updatedReq.PublisherConfig = "{\"Key\": \"Publisher Config\"}" + updatedRes, err := client.Notification.UpdateRule(ctx, updatedReq) + require.NoError(t, err) + require.Equal(t, updatedRes, updatedReq) + } + // Fetch + { + allRules, err := FetchAll(func(po PageOptions) (Page[NotificationRule], error) { + return client.Notification.GetAllRules(ctx, po, SortOptions{}, GetAllRulesFilterOptions{}) + }) + require.NoError(t, err) + found := NotificationRule{} + for _, rule_ := range allRules { + if rule_.UUID == rule.UUID { + found = rule_ + break + } + } + require.Equal(t, found.UUID, rule.UUID) + require.Equal(t, found.Name, rule.Name) + require.Equal(t, found.Enabled, rule.Enabled) + require.Equal(t, found.NotifyChildren, rule.NotifyChildren) + require.Equal(t, found.LogSuccessfulPublish, rule.LogSuccessfulPublish) + require.Equal(t, found.Scope, rule.Scope) + require.Equal(t, found.NotificationLevel, rule.NotificationLevel) + require.Equal(t, found.NotifyOn, rule.NotifyOn) + require.Equal(t, found.TriggerType, rule.TriggerType) + require.Equal(t, found.Message, rule.Message) + require.Equal(t, found.PublisherConfig, "{\"Key\": \"Publisher Config\"}") + require.Equal(t, found.ScheduleLastTriggeredAt, rule.ScheduleLastTriggeredAt) + require.Equal(t, found.ScheduleNextTriggerAt, rule.ScheduleNextTriggerAt) + require.Equal(t, found.ScheduleCron, rule.ScheduleCron) + require.Equal(t, found.ScheduleSkipUnchanged, rule.ScheduleSkipUnchanged) + require.Equal(t, found.Publisher, rule.Publisher) + require.Equal(t, found.Projects, rule.Projects) + require.Equal(t, found.Tags, rule.Tags) + require.Equal(t, found.Teams, rule.Teams) + } + // Delete + { + err := client.Notification.DeleteRule(ctx, rule) + require.NoError(t, err) + } + // Check Absence + { + allRules, err := FetchAll(func(po PageOptions) (Page[NotificationRule], error) { + return client.Notification.GetAllRules(ctx, po, SortOptions{}, GetAllRulesFilterOptions{}) + }) + require.NoError(t, err) + found := NotificationRule{} + for _, rule_ := range allRules { + if rule_.UUID == rule.UUID { + found = rule_ + break + } + } + require.Zero(t, found.UUID) + } +} + +func TestRuleProjects(t *testing.T) { + ctx := context.Background() + client := setUpContainer(t, testContainerOptions{ + APIPermissions: []string{ + PermissionSystemConfiguration, + PermissionPortfolioManagement, + }, + }) + publisher, err := client.Notification.CreatePublisher(ctx, NotificationPublisher{ + Name: "Test_Rule_Projects_Publisher", + Description: "Test_Rule_Description", + PublisherClass: "org.dependencytrack.notification.publisher.ConsolePublisher", + TemplateMIMEType: "text/plain", + Template: "Test_Rule_Template", + }) + require.NoError(t, err) + rule, err := client.Notification.CreateRule(ctx, NotificationRule{ + Name: "Test_Rule_Projects_Name", + Scope: NotificationRuleScopePortfolio, + TriggerType: NotificationRuleTriggerTypeEvent, + Publisher: publisher, + }) + require.NoError(t, err) + project, err := client.Project.Create(ctx, Project{ + Name: "Test_Rule_Projects_Project", + }) + require.NoError(t, err) + // Add Project + { + updated, err := client.Notification.AddProjectToRule(ctx, rule.UUID, project.UUID) + require.NoError(t, err) + require.Equal(t, updated.UUID, rule.UUID) + require.Equal(t, updated.Projects, []Project{project}) + } + // Fetch + { + allRules, err := FetchAll(func(po PageOptions) (Page[NotificationRule], error) { + return client.Notification.GetAllRules(ctx, po, SortOptions{}, GetAllRulesFilterOptions{}) + }) + require.NoError(t, err) + found := NotificationRule{} + for _, rule_ := range allRules { + if rule_.UUID == rule.UUID { + found = rule_ + break + } + } + require.NotZero(t, found.UUID) + require.Empty(t, project.Tags) + require.Empty(t, project.Properties) + + project.Tags = nil + project.Properties = nil + require.Equal(t, found.Projects, []Project{project}) + } + // Remove Project + { + updated, err := client.Notification.RemoveProjectFromRule(ctx, rule.UUID, project.UUID) + require.NoError(t, err) + require.Equal(t, updated, rule) + } + // Check Absence + { + allRules, err := FetchAll(func(po PageOptions) (Page[NotificationRule], error) { + return client.Notification.GetAllRules(ctx, po, SortOptions{}, GetAllRulesFilterOptions{}) + }) + require.NoError(t, err) + found := NotificationRule{} + for _, rule_ := range allRules { + if rule_.UUID == rule.UUID { + found = rule_ + break + } + } + require.Empty(t, found.Projects) + } +} + +func TestRuleTeams(t *testing.T) { + ctx := context.Background() + client := setUpContainer(t, testContainerOptions{ + APIPermissions: []string{ + PermissionSystemConfiguration, + PermissionAccessManagement, + }, + }) + publisher, err := client.Notification.CreatePublisher(ctx, NotificationPublisher{ + Name: "Test_Rule_Tags_Publisher", + Description: "Test_Rule_Description", + PublisherClass: "org.dependencytrack.notification.publisher.SendMailPublisher", + TemplateMIMEType: "text/plain", + Template: "Test_Rule_Template", + }) + require.NoError(t, err) + rule, err := client.Notification.CreateRule(ctx, NotificationRule{ + Name: "Test_Rule_Tags_Name", + Scope: NotificationRuleScopePortfolio, + TriggerType: NotificationRuleTriggerTypeEvent, + Publisher: publisher, + }) + require.NoError(t, err) + team, err := client.Team.Create(ctx, Team{ + Name: "Test_Rule_Teams_Team", + }) + require.NoError(t, err) + // Add Team + { + updated, err := client.Notification.AddTeamToRule(ctx, rule.UUID, team.UUID) + require.NoError(t, err) + + require.Empty(t, team.APIKeys) + require.Empty(t, team.MappedOIDCGroups) + team.APIKeys = nil + team.MappedOIDCGroups = nil + + require.Equal(t, updated.Teams, []Team{team}) + updated.Teams = []Team{} + require.Equal(t, updated, rule) + } + // Fetch + { + allRules, err := FetchAll(func(po PageOptions) (Page[NotificationRule], error) { + return client.Notification.GetAllRules(ctx, po, SortOptions{}, GetAllRulesFilterOptions{}) + }) + require.NoError(t, err) + found := NotificationRule{} + for _, rule_ := range allRules { + if rule_.UUID == rule.UUID { + found = rule_ + break + } + } + require.Empty(t, team.Permissions) + team.Permissions = nil + require.Equal(t, found.Teams, []Team{team}) + } + // Remove Team + { + updated, err := client.Notification.RemoveTeamFromRule(ctx, rule.UUID, team.UUID) + require.NoError(t, err) + require.Equal(t, updated, rule) + } + // Check Absence + { + allRules, err := FetchAll(func(po PageOptions) (Page[NotificationRule], error) { + return client.Notification.GetAllRules(ctx, po, SortOptions{}, GetAllRulesFilterOptions{}) + }) + require.NoError(t, err) + found := NotificationRule{} + for _, rule_ := range allRules { + if rule_.UUID == rule.UUID { + found = rule_ + break + } + } + require.Empty(t, found.Teams) + } +} diff --git a/oidc.go b/oidc.go index 5d1faf8..3b49077 100644 --- a/oidc.go +++ b/oidc.go @@ -243,7 +243,7 @@ func (s OIDCService) Login(ctx context.Context, tokens OIDCTokens) (token string body.Set("idToken", tokens.ID) body.Set("accessToken", tokens.Access) - req, err := s.client.newRequest(ctx, http.MethodPost, "api/v1/user/oidc/login", withBody(body)) + req, err := s.client.newRequest(ctx, http.MethodPost, "api/v1/user/oidc/login", withBody(body), withAcceptContentType("text/plain")) if err != nil { return }