diff --git a/Makefile b/Makefile index 28f56104f..db6e28a81 100644 --- a/Makefile +++ b/Makefile @@ -81,7 +81,6 @@ endif ## Ensures NPM dependencies are installed without having to run this all the time. webapp/.npminstall: ifneq ($(HAS_WEBAPP),) - git config --global url."ssh://git@".insteadOf git:// cd webapp && $(NPM) install touch $@ endif diff --git a/go.mod b/go.mod index 53804cab2..071761ca3 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/rbriski/atlassian-jwt v0.0.0-20180307182949-7bb4ae273058 github.com/rudderlabs/analytics-go v3.3.2+incompatible github.com/stretchr/testify v1.8.0 + github.com/trivago/tgo v1.0.1 golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 golang.org/x/text v0.3.7 ) @@ -75,7 +76,6 @@ require ( github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect github.com/tinylib/msgp v1.1.6 // indirect - github.com/trivago/tgo v1.0.1 // indirect github.com/ulikunitz/xz v0.5.10 // indirect github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect diff --git a/plugin.json b/plugin.json index d1c89eef3..af56e9776 100644 --- a/plugin.json +++ b/plugin.json @@ -76,6 +76,14 @@ "placeholder": "", "default": "" }, + { + "key": "SecurityLevelEmptyForJiraSubscriptions", + "display_name": "Default Subscription Security Level to Empty:", + "type": "bool", + "help_text": "Subscriptions will only include issues that have a security level assigned if the appropriate security level has been included as a filter", + "placeholder": "", + "default": true + }, { "key": "JiraAdminAdditionalHelpText", "display_name": "Additional Help Text to be shown with Jira Help:", diff --git a/server/issue.go b/server/issue.go index d5854a8d9..81d0713e2 100644 --- a/server/issue.go +++ b/server/issue.go @@ -22,12 +22,13 @@ import ( ) const ( - labelsField = "labels" - statusField = "status" - reporterField = "reporter" - priorityField = "priority" - descriptionField = "description" - resolutionField = "resolution" + labelsField = "labels" + statusField = "status" + reporterField = "reporter" + priorityField = "priority" + descriptionField = "description" + resolutionField = "resolution" + securityLevelField = "security" ) func makePost(userID, channelID, message string) *model.Post { diff --git a/server/issue_test.go b/server/issue_test.go index c82dbf7a3..09d3fea17 100644 --- a/server/issue_test.go +++ b/server/issue_test.go @@ -16,6 +16,7 @@ import ( "github.com/mattermost/mattermost-server/v6/plugin/plugintest/mock" "github.com/pkg/errors" "github.com/stretchr/testify/assert" + "github.com/trivago/tgo/tcontainer" "github.com/mattermost/mattermost-plugin-jira/server/utils/kvstore" ) @@ -85,6 +86,28 @@ func (client testClient) AddComment(issueKey string, comment *jira.Comment) (*ji return nil, nil } +func (client testClient) GetCreateMetaInfo(api plugin.API, options *jira.GetQueryOptions) (*jira.CreateMetaInfo, error) { + return &jira.CreateMetaInfo{ + Projects: []*jira.MetaProject{ + { + IssueTypes: []*jira.MetaIssueType{ + { + Fields: tcontainer.MarshalMap{ + "security": tcontainer.MarshalMap{ + "allowedValues": []interface{}{ + tcontainer.MarshalMap{ + "id": "10001", + }, + }, + }, + }, + }, + }, + }, + }, + }, nil +} + func TestTransitionJiraIssue(t *testing.T) { api := &plugintest.API{} api.On("SendEphemeralPost", mock.AnythingOfType("string"), mock.AnythingOfType("*model.Post")).Return(&model.Post{}) diff --git a/server/plugin.go b/server/plugin.go index 7b5c6d776..74ea64f1a 100644 --- a/server/plugin.go +++ b/server/plugin.go @@ -70,6 +70,9 @@ type externalConfig struct { // Additional Help Text to be shown in the output of '/jira help' command JiraAdminAdditionalHelpText string + // When enabled, a subscription without security level rules will filter out an issue that has a security level assigned + SecurityLevelEmptyForJiraSubscriptions bool + // Hide issue descriptions and comments in Webhook and Subscription messages HideDecriptionComment bool diff --git a/server/subscribe.go b/server/subscribe.go index 12073746c..5e448e8d2 100644 --- a/server/subscribe.go +++ b/server/subscribe.go @@ -15,6 +15,8 @@ import ( "github.com/gorilla/mux" "github.com/pkg/errors" + "github.com/trivago/tgo/tcontainer" + "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-plugin-jira/server/utils" @@ -140,37 +142,61 @@ func (p *Plugin) matchesSubsciptionFilters(wh *webhook, filters SubscriptionFilt return false } - if filters.IssueTypes.Len() != 0 && !filters.IssueTypes.ContainsAny(wh.JiraWebhook.Issue.Fields.Type.ID) { + issue := &wh.JiraWebhook.Issue + + if filters.IssueTypes.Len() != 0 && !filters.IssueTypes.ContainsAny(issue.Fields.Type.ID) { return false } - if filters.Projects.Len() != 0 && !filters.Projects.ContainsAny(wh.JiraWebhook.Issue.Fields.Project.Key) { + if filters.Projects.Len() != 0 && !filters.Projects.ContainsAny(issue.Fields.Project.Key) { return false } - validFilter := true - + containsSecurityLevelFilter := false + useEmptySecurityLevel := p.getConfig().SecurityLevelEmptyForJiraSubscriptions for _, field := range filters.Fields { + inclusion := field.Inclusion + // Broken filter, values must be provided - if field.Inclusion == "" || (field.Values.Len() == 0 && field.Inclusion != FilterEmpty) { - validFilter = false - break + if inclusion == "" || (field.Values.Len() == 0 && inclusion != FilterEmpty) { + return false + } + + if field.Key == securityLevelField { + containsSecurityLevelFilter = true + if inclusion == FilterExcludeAny && useEmptySecurityLevel { + inclusion = FilterEmpty + } } - value := getIssueFieldValue(&wh.JiraWebhook.Issue, field.Key) - containsAny := value.ContainsAny(field.Values.Elems()...) - containsAll := value.ContainsAll(field.Values.Elems()...) + value := getIssueFieldValue(issue, field.Key) + if !isValidFieldInclusion(field, value, inclusion) { + return false + } + } - if (field.Inclusion == FilterIncludeAny && !containsAny) || - (field.Inclusion == FilterIncludeAll && !containsAll) || - (field.Inclusion == FilterExcludeAny && containsAny) || - (field.Inclusion == FilterEmpty && value.Len() > 0) { - validFilter = false - break + if !containsSecurityLevelFilter && useEmptySecurityLevel { + securityLevel := getIssueFieldValue(issue, securityLevelField) + if securityLevel.Len() > 0 { + return false } } - return validFilter + return true +} + +func isValidFieldInclusion(field FieldFilter, value StringSet, inclusion string) bool { + containsAny := value.ContainsAny(field.Values.Elems()...) + containsAll := value.ContainsAll(field.Values.Elems()...) + + if (inclusion == FilterIncludeAny && !containsAny) || + (inclusion == FilterIncludeAll && !containsAll) || + (inclusion == FilterExcludeAny && containsAny) || + (inclusion == FilterEmpty && value.Len() > 0) { + return false + } + + return true } func (p *Plugin) getChannelsSubscribed(wh *webhook, instanceID types.ID) ([]ChannelSubscription, error) { @@ -298,6 +324,37 @@ func (p *Plugin) validateSubscription(instanceID types.ID, subscription *Channel return errors.New("please provide a project identifier") } + projectKey := subscription.Filters.Projects.Elems()[0] + + var securityLevels StringSet + useEmptySecurityLevel := p.getConfig().SecurityLevelEmptyForJiraSubscriptions + for _, field := range subscription.Filters.Fields { + if field.Key != securityLevelField { + continue + } + + if field.Inclusion == FilterEmpty { + continue + } + + if field.Inclusion == FilterExcludeAny && useEmptySecurityLevel { + return errors.New("security level does not allow for an \"Exclude\" clause") + } + + if securityLevels == nil { + securityLevelsArray, err := p.getSecurityLevelsForProject(client, projectKey) + if err != nil { + return errors.Wrap(err, "failed to get security levels for project") + } + + securityLevels = NewStringSet(securityLevelsArray...) + } + + if !securityLevels.ContainsAll(field.Values.Elems()...) { + return errors.New("invalid access to security level") + } + } + channelID := subscription.ChannelID subs, err := p.getSubscriptionsForChannel(instanceID, channelID) if err != nil { @@ -310,7 +367,6 @@ func (p *Plugin) validateSubscription(instanceID types.ID, subscription *Channel } } - projectKey := subscription.Filters.Projects.Elems()[0] _, err = client.GetProject(projectKey) if err != nil { return errors.WithMessagef(err, "failed to get project %q", projectKey) @@ -319,6 +375,47 @@ func (p *Plugin) validateSubscription(instanceID types.ID, subscription *Channel return nil } +func (p *Plugin) getSecurityLevelsForProject(client Client, projectKey string) ([]string, error) { + createMeta, err := client.GetCreateMetaInfo(p.API, &jira.GetQueryOptions{ + Expand: "projects.issuetypes.fields", + ProjectKeys: projectKey, + }) + if err != nil { + return nil, errors.Wrap(err, "error fetching user security levels") + } + + if len(createMeta.Projects) == 0 || len(createMeta.Projects[0].IssueTypes) == 0 { + return nil, errors.Wrapf(err, "no project found for project key %s", projectKey) + } + + securityLevels1, err := createMeta.Projects[0].IssueTypes[0].Fields.MarshalMap(securityLevelField) + if err != nil { + return nil, errors.Wrap(err, "error parsing user security levels") + } + + allowed, ok := securityLevels1["allowedValues"].([]interface{}) + if !ok { + return nil, errors.New("error parsing user security levels: failed to type assertion on array") + } + + out := []string{} + for _, level := range allowed { + value, ok := level.(tcontainer.MarshalMap) + if !ok { + return nil, errors.New("error parsing user security levels: failed to type assertion on map") + } + + id, ok := value["id"].(string) + if !ok { + return nil, errors.New("error parsing user security levels: failed to type assertion on string") + } + + out = append(out, id) + } + + return out, nil +} + func (p *Plugin) editChannelSubscription(instanceID types.ID, modifiedSubscription *ChannelSubscription, client Client) error { subKey := keyWithInstanceID(instanceID, JiraSubscriptionsKey) return p.client.KV.SetAtomicWithRetries(subKey, func(initialBytes []byte) (interface{}, error) { diff --git a/server/subscribe_test.go b/server/subscribe_test.go index b60b11d08..393c07dc8 100644 --- a/server/subscribe_test.go +++ b/server/subscribe_test.go @@ -20,6 +20,199 @@ import ( "github.com/stretchr/testify/assert" ) +func TestValidateSubscription(t *testing.T) { + p := &Plugin{} + + p.instanceStore = p.getMockInstanceStoreKV(0) + + api := &plugintest.API{} + p.SetAPI(api) + + for name, tc := range map[string]struct { + subscription *ChannelSubscription + errorMessage string + disableSecurityConfig bool + }{ + "no event selected": { + subscription: &ChannelSubscription{ + ID: "id", + Name: "name", + ChannelID: "channelid", + InstanceID: "instance_id", + Filters: SubscriptionFilters{ + Events: NewStringSet(), + Projects: NewStringSet("project"), + IssueTypes: NewStringSet("10001"), + }, + }, + errorMessage: "please provide at least one event type", + }, + "no project selected": { + subscription: &ChannelSubscription{ + ID: "id", + Name: "name", + ChannelID: "channelid", + InstanceID: "instance_id", + Filters: SubscriptionFilters{ + Events: NewStringSet("issue_created"), + Projects: NewStringSet(), + IssueTypes: NewStringSet("10001"), + }, + }, + errorMessage: "please provide a project identifier", + }, + "no issue type selected": { + subscription: &ChannelSubscription{ + ID: "id", + Name: "name", + ChannelID: "channelid", + InstanceID: "instance_id", + Filters: SubscriptionFilters{ + Events: NewStringSet("issue_created"), + Projects: NewStringSet("project"), + IssueTypes: NewStringSet(), + }, + }, + errorMessage: "please provide at least one issue type", + }, + "valid subscription": { + subscription: &ChannelSubscription{ + ID: "id", + Name: "name", + ChannelID: "channelid", + InstanceID: "instance_id", + Filters: SubscriptionFilters{ + Events: NewStringSet("issue_created"), + Projects: NewStringSet("project"), + IssueTypes: NewStringSet("10001"), + }, + }, + errorMessage: "", + }, + "valid subscription with security level": { + subscription: &ChannelSubscription{ + ID: "id", + Name: "name", + ChannelID: "channelid", + InstanceID: "instance_id", + Filters: SubscriptionFilters{ + Events: NewStringSet("issue_created"), + Projects: NewStringSet("TEST"), + IssueTypes: NewStringSet("10001"), + Fields: []FieldFilter{ + { + Key: "security", + Inclusion: FilterIncludeAll, + Values: NewStringSet("10001"), + }, + }, + }, + }, + errorMessage: "", + }, + "invalid 'Exclude' of security level": { + subscription: &ChannelSubscription{ + ID: "id", + Name: "name", + ChannelID: "channelid", + InstanceID: "instance_id", + Filters: SubscriptionFilters{ + Events: NewStringSet("issue_created"), + Projects: NewStringSet("TEST"), + IssueTypes: NewStringSet("10001"), + Fields: []FieldFilter{ + { + Key: "security", + Inclusion: FilterExcludeAny, + Values: NewStringSet("10001"), + }, + }, + }, + }, + errorMessage: "security level does not allow for an \"Exclude\" clause", + }, + "security config disabled, valid 'Exclude' of security level": { + subscription: &ChannelSubscription{ + ID: "id", + Name: "name", + ChannelID: "channelid", + InstanceID: "instance_id", + Filters: SubscriptionFilters{ + Events: NewStringSet("issue_created"), + Projects: NewStringSet("TEST"), + IssueTypes: NewStringSet("10001"), + Fields: []FieldFilter{ + { + Key: "security", + Inclusion: FilterExcludeAny, + Values: NewStringSet("10001"), + }, + }, + }, + }, + disableSecurityConfig: true, + errorMessage: "", + }, + "invalid access to security level": { + subscription: &ChannelSubscription{ + ID: "id", + Name: "name", + ChannelID: "channelid", + InstanceID: "instance_id", + Filters: SubscriptionFilters{ + Events: NewStringSet("issue_created"), + Projects: NewStringSet("TEST"), + IssueTypes: NewStringSet("10001"), + Fields: []FieldFilter{ + { + Key: "security", + Inclusion: FilterIncludeAll, + Values: NewStringSet("10002"), + }, + }, + }, + }, + errorMessage: "invalid access to security level", + }, + "user does not have read access to the project": { + subscription: &ChannelSubscription{ + ID: "id", + Name: "name", + ChannelID: "channelid", + InstanceID: "instance_id", + Filters: SubscriptionFilters{ + Events: NewStringSet("issue_created"), + Projects: NewStringSet(nonExistantProjectKey), + IssueTypes: NewStringSet("10001"), + }, + }, + errorMessage: "failed to get project \"FP\": Project FP not found", + }, + } { + t.Run(name, func(t *testing.T) { + api := &plugintest.API{} + p.SetAPI(api) + p.client = pluginapi.NewClient(p.API, p.Driver) + + api.On("KVGet", testSubKey).Return(nil, nil) + + p.updateConfig(func(conf *config) { + conf.SecurityLevelEmptyForJiraSubscriptions = !tc.disableSecurityConfig + }) + + client := testClient{} + err := p.validateSubscription(testInstance1.InstanceID, tc.subscription, client) + + if tc.errorMessage == "" { + require.NoError(t, err) + } else { + require.Error(t, err) + require.Equal(t, tc.errorMessage, err.Error()) + } + }) + } +} + func TestListChannelSubscriptions(t *testing.T) { p := &Plugin{} p.updateConfig(func(conf *config) { @@ -278,9 +471,10 @@ func TestGetChannelsSubscribed(t *testing.T) { p.instanceStore = p.getMockInstanceStoreKV(0) for name, tc := range map[string]struct { - WebhookTestData string - Subs *Subscriptions - ChannelSubscriptions []ChannelSubscription + WebhookTestData string + Subs *Subscriptions + ChannelSubscriptions []ChannelSubscription + disableSecurityConfig bool }{ "no filters selected": { WebhookTestData: "webhook-issue-created.json", @@ -1360,12 +1554,90 @@ func TestGetChannelsSubscribed(t *testing.T) { }), ChannelSubscriptions: []ChannelSubscription{{ChannelID: "sampleChannelId"}}, }, + "no security level provided in subscription, but security level is present in issue": { + WebhookTestData: "webhook-issue-created-with-security-level.json", + Subs: withExistingChannelSubscriptions([]ChannelSubscription{ + { + ID: "rg86cd65efdjdjezgisgxaitzh", + ChannelID: "sampleChannelId", + Filters: SubscriptionFilters{ + Events: NewStringSet("event_created"), + Projects: NewStringSet("TES"), + IssueTypes: NewStringSet("10001"), + Fields: []FieldFilter{}, + }, + }, + }), + ChannelSubscriptions: []ChannelSubscription{}, + }, + "security config disabled, no security level provided in subscription, but security level is present in issue": { + WebhookTestData: "webhook-issue-created-with-security-level.json", + Subs: withExistingChannelSubscriptions([]ChannelSubscription{ + { + ID: "rg86cd65efdjdjezgisgxaitzh", + ChannelID: "sampleChannelId", + Filters: SubscriptionFilters{ + Events: NewStringSet("event_created"), + Projects: NewStringSet("TES"), + IssueTypes: NewStringSet("10001"), + Fields: []FieldFilter{}, + }, + }, + }), + ChannelSubscriptions: []ChannelSubscription{{ChannelID: "sampleChannelId"}}, + disableSecurityConfig: true, + }, + "security level provided in subscription, but different security level is present in issue": { + WebhookTestData: "webhook-issue-created-with-security-level.json", + Subs: withExistingChannelSubscriptions([]ChannelSubscription{ + { + ID: "rg86cd65efdjdjezgisgxaitzh", + ChannelID: "sampleChannelId", + Filters: SubscriptionFilters{ + Events: NewStringSet("event_created"), + Projects: NewStringSet("TES"), + IssueTypes: NewStringSet("10001"), + Fields: []FieldFilter{ + { + Key: "security", + Inclusion: FilterIncludeAll, + Values: NewStringSet("10002"), + }, + }, + }, + }, + }), + ChannelSubscriptions: []ChannelSubscription{}, + }, + "security level provided in subscription, and same security level is present in issue": { + WebhookTestData: "webhook-issue-created-with-security-level.json", + Subs: withExistingChannelSubscriptions([]ChannelSubscription{ + { + ID: "rg86cd65efdjdjezgisgxaitzh", + ChannelID: "sampleChannelId", + Filters: SubscriptionFilters{ + Events: NewStringSet("event_created"), + Projects: NewStringSet("TES"), + IssueTypes: NewStringSet("10001"), + Fields: []FieldFilter{ + { + Key: "security", + Inclusion: FilterIncludeAll, + Values: NewStringSet("10001"), + }, + }, + }, + }, + }), + ChannelSubscriptions: []ChannelSubscription{{ChannelID: "sampleChannelId"}}, + }, } { t.Run(name, func(t *testing.T) { api := &plugintest.API{} p.updateConfig(func(conf *config) { conf.Secret = someSecret + conf.SecurityLevelEmptyForJiraSubscriptions = !tc.disableSecurityConfig }) p.SetAPI(api) diff --git a/server/testdata/webhook-issue-created-with-security-level.json b/server/testdata/webhook-issue-created-with-security-level.json new file mode 100644 index 000000000..d01f91231 --- /dev/null +++ b/server/testdata/webhook-issue-created-with-security-level.json @@ -0,0 +1,238 @@ +{ + "timestamp": 1550286113023, + "webhookEvent": "jira:issue_created", + "issue_event_type_name": "issue_created", + "user": { + "self": "https://some-instance-test.atlassian.net/rest/api/2/user?accountId=5c5f880629be9642ba529340", + "name": "admin", + "key": "admin", + "accountId": "5c5f880629be9642ba529340", + "emailAddress": "some-instance-test@gmail.com", + "avatarUrls": { + "48x48": "https://avatar-cdn.atlassian.com/d991bc281c0c0ecb0bbb2db3979ddaff?s=48&d=https%3A%2F%2Fsecure.gravatar.com%2Favatar%2Fd991bc281c0c0ecb0bbb2db3979ddaff%3Fd%3Dmm%26s%3D48%26noRedirect%3Dtrue", + "24x24": "https://avatar-cdn.atlassian.com/d991bc281c0c0ecb0bbb2db3979ddaff?s=24&d=https%3A%2F%2Fsecure.gravatar.com%2Favatar%2Fd991bc281c0c0ecb0bbb2db3979ddaff%3Fd%3Dmm%26s%3D24%26noRedirect%3Dtrue", + "16x16": "https://avatar-cdn.atlassian.com/d991bc281c0c0ecb0bbb2db3979ddaff?s=16&d=https%3A%2F%2Fsecure.gravatar.com%2Favatar%2Fd991bc281c0c0ecb0bbb2db3979ddaff%3Fd%3Dmm%26s%3D16%26noRedirect%3Dtrue", + "32x32": "https://avatar-cdn.atlassian.com/d991bc281c0c0ecb0bbb2db3979ddaff?s=32&d=https%3A%2F%2Fsecure.gravatar.com%2Favatar%2Fd991bc281c0c0ecb0bbb2db3979ddaff%3Fd%3Dmm%26s%3D32%26noRedirect%3Dtrue" + }, + "displayName": "Test User", + "active": true, + "timeZone": "America/Los_Angeles" + }, + "issue": { + "id": "10040", + "self": "https://some-instance-test.atlassian.net/rest/api/2/issue/10040", + "key": "TES-41", + "fields": { + "issuetype": { + "self": "https://some-instance-test.atlassian.net/rest/api/2/issuetype/10001", + "id": "10001", + "description": "Stories track functionality or features expressed as user goals.", + "iconUrl": "https://some-instance-test.atlassian.net/secure/viewavatar?size=xsmall&avatarId=10315&avatarType=issuetype", + "name": "Story", + "subtask": false, + "avatarId": 10315 + }, + "timespent": null, + "customfield_10030": null, + "project": { + "self": "https://some-instance-test.atlassian.net/rest/api/2/project/10000", + "id": "10000", + "key": "TES", + "name": "test1", + "projectTypeKey": "software", + "avatarUrls": { + "48x48": "https://some-instance-test.atlassian.net/secure/projectavatar?avatarId=10324", + "24x24": "https://some-instance-test.atlassian.net/secure/projectavatar?size=small&avatarId=10324", + "16x16": "https://some-instance-test.atlassian.net/secure/projectavatar?size=xsmall&avatarId=10324", + "32x32": "https://some-instance-test.atlassian.net/secure/projectavatar?size=medium&avatarId=10324" + } + }, + "fixVersions": [], + "aggregatetimespent": null, + "resolution": null, + "customfield_10027": null, + "resolutiondate": null, + "workratio": -1, + "lastViewed": null, + "watches": { + "self": "https://some-instance-test.atlassian.net/rest/api/2/issue/TES-41/watchers", + "watchCount": 0, + "isWatching": true + }, + "created": "2019-02-15T19:01:52.971-0800", + "customfield_10020": null, + "customfield_10021": null, + "customfield_10022": "0|i00067:", + "priority": { + "self": "https://some-instance-test.atlassian.net/rest/api/2/priority/2", + "iconUrl": "https://some-instance-test.atlassian.net/images/icons/priorities/high.svg", + "name": "High", + "id": "2" + }, + "customfield_10023": null, + "customfield_10024": [], + "customfield_10025": null, + "customfield_10026": null, + "labels": [ + "test-label" + ], + "customfield_10016": null, + "customfield_10017": null, + "customfield_10018": { + "hasEpicLinkFieldDependency": false, + "showField": false, + "nonEditableReason": { + "reason": "PLUGIN_LICENSE_ERROR", + "message": "Portfolio for Jira must be licensed for the Parent Link to be available." + } + }, + "customfield_10019": null, + "aggregatetimeoriginalestimate": null, + "timeestimate": null, + "versions": [], + "issuelinks": [], + "assignee": null, + "updated": "2019-02-15T19:01:52.971-0800", + "status": { + "self": "https://some-instance-test.atlassian.net/rest/api/2/status/10001", + "description": "", + "iconUrl": "https://some-instance-test.atlassian.net/", + "name": "To Do", + "id": "10001", + "statusCategory": { + "self": "https://some-instance-test.atlassian.net/rest/api/2/statuscategory/2", + "id": 2, + "key": "new", + "colorName": "blue-gray", + "name": "New" + } + }, + "components": [ + { + "self": "https://some-instance-test.atlassian.net/rest/api/2/component/10000", + "id": "10000", + "name": "COMP-1", + "description": "Component-1" + } + ], + "timeoriginalestimate": null, + "description": "Unit test description, not that long", + "customfield_10010": null, + "customfield_10014": null, + "customfield_10015": null, + "timetracking": {}, + "customfield_10005": null, + "customfield_10006": null, + "security": "10001", + "customfield_10007": null, + "customfield_10008": null, + "attachment": [], + "customfield_10009": null, + "aggregatetimeestimate": null, + "summary": "Unit test summary", + "creator": { + "self": "https://some-instance-test.atlassian.net/rest/api/2/user?accountId=5c5f880629be9642ba529340", + "name": "admin", + "key": "admin", + "accountId": "5c5f880629be9642ba529340", + "emailAddress": "some-instance-test@gmail.com", + "avatarUrls": { + "48x48": "https://avatar-cdn.atlassian.com/d991bc281c0c0ecb0bbb2db3979ddaff?s=48&d=https%3A%2F%2Fsecure.gravatar.com%2Favatar%2Fd991bc281c0c0ecb0bbb2db3979ddaff%3Fd%3Dmm%26s%3D48%26noRedirect%3Dtrue", + "24x24": "https://avatar-cdn.atlassian.com/d991bc281c0c0ecb0bbb2db3979ddaff?s=24&d=https%3A%2F%2Fsecure.gravatar.com%2Favatar%2Fd991bc281c0c0ecb0bbb2db3979ddaff%3Fd%3Dmm%26s%3D24%26noRedirect%3Dtrue", + "16x16": "https://avatar-cdn.atlassian.com/d991bc281c0c0ecb0bbb2db3979ddaff?s=16&d=https%3A%2F%2Fsecure.gravatar.com%2Favatar%2Fd991bc281c0c0ecb0bbb2db3979ddaff%3Fd%3Dmm%26s%3D16%26noRedirect%3Dtrue", + "32x32": "https://avatar-cdn.atlassian.com/d991bc281c0c0ecb0bbb2db3979ddaff?s=32&d=https%3A%2F%2Fsecure.gravatar.com%2Favatar%2Fd991bc281c0c0ecb0bbb2db3979ddaff%3Fd%3Dmm%26s%3D32%26noRedirect%3Dtrue" + }, + "displayName": "Test User", + "active": true, + "timeZone": "America/Los_Angeles" + }, + "subtasks": [], + "reporter": { + "self": "https://some-instance-test.atlassian.net/rest/api/2/user?accountId=5c5f880629be9642ba529340", + "name": "admin", + "key": "admin", + "accountId": "5c5f880629be9642ba529340", + "emailAddress": "some-instance-test@gmail.com", + "avatarUrls": { + "48x48": "https://avatar-cdn.atlassian.com/d991bc281c0c0ecb0bbb2db3979ddaff?s=48&d=https%3A%2F%2Fsecure.gravatar.com%2Favatar%2Fd991bc281c0c0ecb0bbb2db3979ddaff%3Fd%3Dmm%26s%3D48%26noRedirect%3Dtrue", + "24x24": "https://avatar-cdn.atlassian.com/d991bc281c0c0ecb0bbb2db3979ddaff?s=24&d=https%3A%2F%2Fsecure.gravatar.com%2Favatar%2Fd991bc281c0c0ecb0bbb2db3979ddaff%3Fd%3Dmm%26s%3D24%26noRedirect%3Dtrue", + "16x16": "https://avatar-cdn.atlassian.com/d991bc281c0c0ecb0bbb2db3979ddaff?s=16&d=https%3A%2F%2Fsecure.gravatar.com%2Favatar%2Fd991bc281c0c0ecb0bbb2db3979ddaff%3Fd%3Dmm%26s%3D16%26noRedirect%3Dtrue", + "32x32": "https://avatar-cdn.atlassian.com/d991bc281c0c0ecb0bbb2db3979ddaff?s=32&d=https%3A%2F%2Fsecure.gravatar.com%2Favatar%2Fd991bc281c0c0ecb0bbb2db3979ddaff%3Fd%3Dmm%26s%3D32%26noRedirect%3Dtrue" + }, + "displayName": "Test User", + "active": true, + "timeZone": "America/Los_Angeles" + }, + "customfield_10000": "{}", + "aggregateprogress": { + "progress": 0, + "total": 0 + }, + "customfield_10001": null, + "customfield_10002": null, + "customfield_10003": null, + "customfield_10004": null, + "environment": null, + "duedate": null, + "progress": { + "progress": 0, + "total": 0 + }, + "votes": { + "self": "https://some-instance-test.atlassian.net/rest/api/2/issue/TES-41/votes", + "votes": 0, + "hasVoted": false + } + } + }, + "changelog": { + "id": "10222", + "items": [ + { + "field": "description", + "fieldtype": "jira", + "fieldId": "description", + "from": null, + "fromString": null, + "to": null, + "toString": "Unit test description, not that long" + }, + { + "field": "priority", + "fieldtype": "jira", + "fieldId": "priority", + "from": null, + "fromString": null, + "to": "2", + "toString": "High" + }, + { + "field": "reporter", + "fieldtype": "jira", + "fieldId": "reporter", + "from": null, + "fromString": null, + "to": "admin", + "toString": "Test User" + }, + { + "field": "Status", + "fieldtype": "jira", + "fieldId": "status", + "from": null, + "fromString": null, + "to": "10001", + "toString": "To Do" + }, + { + "field": "summary", + "fieldtype": "jira", + "fieldId": "summary", + "from": null, + "fromString": null, + "to": null, + "toString": "Unit test summary" + } + ] + } +} diff --git a/server/user.go b/server/user.go index 38947e8fd..c2a51162f 100644 --- a/server/user.go +++ b/server/user.go @@ -190,10 +190,13 @@ func (p *Plugin) UpdateUserDefaults(mattermostUserID, instanceID types.ID, proje } func (p *Plugin) httpGetSettingsInfo(w http.ResponseWriter, r *http.Request) (int, error) { + conf := p.getConfig() return respondJSON(w, struct { - UIEnabled bool `json:"ui_enabled"` + UIEnabled bool `json:"ui_enabled"` + SecurityLevelEmptyForJiraSubscriptions bool `json:"security_level_empty_for_jira_subscriptions"` }{ - UIEnabled: p.getConfig().EnableJiraUI, + UIEnabled: conf.EnableJiraUI, + SecurityLevelEmptyForJiraSubscriptions: conf.SecurityLevelEmptyForJiraSubscriptions, }) } diff --git a/webapp/src/components/modals/channel_subscriptions/__snapshots__/channel_subscription_filters.test.tsx.snap b/webapp/src/components/modals/channel_subscriptions/__snapshots__/channel_subscription_filters.test.tsx.snap index ef1cfd844..a33535f58 100644 --- a/webapp/src/components/modals/channel_subscriptions/__snapshots__/channel_subscription_filters.test.tsx.snap +++ b/webapp/src/components/modals/channel_subscriptions/__snapshots__/channel_subscription_filters.test.tsx.snap @@ -155,6 +155,7 @@ exports[`components/ChannelSubscriptionFilters should match snapshot 1`] = ` onChange={[Function]} removeFilter={[Function]} removeValidate={[MockFunction]} + securityLevelEmptyForJiraSubscriptions={true} theme={Object {}} value={ Object { diff --git a/webapp/src/components/modals/channel_subscriptions/__snapshots__/edit_channel_subscription.test.tsx.snap b/webapp/src/components/modals/channel_subscriptions/__snapshots__/edit_channel_subscription.test.tsx.snap index 9ff6146e3..3b0054080 100644 --- a/webapp/src/components/modals/channel_subscriptions/__snapshots__/edit_channel_subscription.test.tsx.snap +++ b/webapp/src/components/modals/channel_subscriptions/__snapshots__/edit_channel_subscription.test.tsx.snap @@ -3544,6 +3544,7 @@ exports[`components/EditChannelSubscription should match snapshot after fetching } onChange={[Function]} removeValidate={[Function]} + securityLevelEmptyForJiraSubscriptions={true} theme={ Object { "awayIndicator": "#ffbc42", @@ -3611,13 +3612,22 @@ exports[`components/EditChannelSubscription should match snapshot after fetching "background": "rgba(61,60,64,0.08)", "borderRadius": "4px", "fontSize": "13px", + "marginBottom": "8px", "marginTop": "8px", "padding": "10px 12px", } } > - Project = KT AND IssueType IN (Bug) AND "MJK - Radio Buttons" IN (1) AND affectedVersion IN (d) AND "Epic Link" IN (IDT-24) + Project = KT AND IssueType IN (Bug) AND "MJK - Radio Buttons" IN (1) AND affectedVersion IN (d) AND "Epic Link" IN (IDT-24) AND "Security Level" IS EMPTY + + +