Skip to content

Commit

Permalink
[MM-44185] Add conditions for subscription validation (#858)
Browse files Browse the repository at this point in the history
* ensure user has access to selected sec level. disallow "exclude" clauses for sec level

* if a subscription has no configured sec level, assume the issue should have a blank sec level

* add test data file

* fix ci withL npm install --verbose

* enforce empty during webhook call if exclude is being used

* add plugin setting to toggle security level functionality

* fix tests

* add form validation for securitylevel field

* change plugin setting to bool

* change plugin setting description

* WIP

* show message about security level under JQL query

* extract if statement into function

* fix issues from ci

* fix merge issues and lint

---------

Co-authored-by: Mattermod <[email protected]>
Co-authored-by: Mattermost Build <[email protected]>
  • Loading branch information
3 people authored May 2, 2023
1 parent 0ba44b3 commit 5f5e084
Show file tree
Hide file tree
Showing 22 changed files with 828 additions and 45 deletions.
1 change: 0 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:",
Expand Down
13 changes: 7 additions & 6 deletions server/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
23 changes: 23 additions & 0 deletions server/issue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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{})
Expand Down
3 changes: 3 additions & 0 deletions server/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
133 changes: 115 additions & 18 deletions server/subscribe.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -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) {
Expand Down
Loading

0 comments on commit 5f5e084

Please sign in to comment.