Skip to content

Commit

Permalink
Merge branch 'master' of github.com:mattermost/mattermost-plugin-jira…
Browse files Browse the repository at this point in the history
… into SEC-6831_autolink_dep
  • Loading branch information
Kshitij-Katiyar committed Jan 16, 2025
2 parents 25b2dfc + 3b40b93 commit 2e841ba
Show file tree
Hide file tree
Showing 15 changed files with 581 additions and 83 deletions.
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ module github.com/mattermost/mattermost-plugin-jira

go 1.22

toolchain go1.22.2
toolchain go1.22.8

require (
github.com/andygrunwald/go-jira v1.16.0
github.com/blang/semver/v4 v4.0.0
github.com/dghubble/oauth1 v0.5.0
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/gorilla/mux v1.8.1
github.com/hashicorp/go-multierror v1.1.1
github.com/jarcoal/httpmock v1.0.8
github.com/mattermost-community/mattermost-plugin-autolink v1.4.1-0.20241105205450-79c240cde7d6
github.com/mattermost/mattermost/server/public v0.1.7
Expand All @@ -20,6 +21,7 @@ require (
github.com/trivago/tgo v1.0.7
golang.org/x/oauth2 v0.21.0
golang.org/x/text v0.16.0
gopkg.in/yaml.v3 v3.0.1
)

require (
Expand All @@ -31,14 +33,13 @@ require (
github.com/francoispqt/gojay v1.2.13 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/golang-jwt/jwt/v4 v4.4.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-plugin v1.6.1 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
github.com/lib/pq v1.10.9 // indirect
Expand Down Expand Up @@ -74,5 +75,4 @@ require (
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqw
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs=
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
Expand Down
22 changes: 20 additions & 2 deletions plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"homepage_url": "https://github.com/mattermost/mattermost-plugin-jira",
"support_url": "https://github.com/mattermost/mattermost-plugin-jira/issues",
"icon_path": "assets/icon.svg",
"min_server_version": "7.8.0",
"min_server_version": "10.5.0",
"server": {
"executables": {
"darwin-amd64": "server/dist/plugin-darwin-amd64",
Expand Down Expand Up @@ -115,11 +115,29 @@
"placeholder": "",
"default": false
},
{
"key": "EncryptionKey",
"display_name": "At Rest Encryption Key:",
"type": "generated",
"help_text": "The encryption key used to encrypt stored API tokens.",
"placeholder": "",
"default": null,
"secret": true
},
{
"key": "AdminAPIToken",
"display_name": "Admin API Token",
"type": "text",
"help_text": "Set this [API token](https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/) to get notified for comment and issue created events even if the user triggering the event is not connected to Jira.\n **Note:** API token should be created using an admin Jira account. Otherwise, the notification will not be delivered for the project the user cannot access.",
"help_text": "Set this [API token](https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/) to get notified for comment and issue created events when the user triggering the event is not connected to Jira. This is also used for setting up autolink in the plugin.\n **Note:** API token should be created using an admin Jira account. Otherwise, the notification will not be delivered for projects that the user cannot access and autolink will not work.",
"placeholder": "",
"secret": true,
"default": ""
},
{
"key": "AdminEmail",
"display_name": "Admin Email",
"type": "text",
"help_text": "**Note** Admin email is necessary to setup autolink for the Jira plugin and to to get notified for comment and issue created events when the user triggering the event is not connected to Jira",
"placeholder": "",
"default": ""
}
Expand Down
49 changes: 48 additions & 1 deletion server/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -1103,7 +1103,10 @@ func (p *Plugin) GetIssueDataWithAPIToken(issueID, instanceID string) (*jira.Iss
return nil, errors.Wrapf(err, "failed to create http request for fetching issue data. IssueID: %s", issueID)
}

req.Header.Set("Authorization", fmt.Sprintf("Basic %s", p.getConfig().AdminAPIToken))
err = p.SetAdminAPITokenRequestHeader(req)
if err != nil {
return nil, err
}

resp, err := client.Do(req)
if err != nil {
Expand Down Expand Up @@ -1134,3 +1137,47 @@ func (p *Plugin) GetIssueDataWithAPIToken(issueID, instanceID string) (*jira.Iss

return issue, nil
}

type ProjectSearchResponse struct {
Self string `json:"self"`
MaxResults int `json:"maxResults"`
StartAt int `json:"startAt"`
Total int `json:"total"`
IsLast bool `json:"isLast"`
Values jira.ProjectList `json:"values"`
}

func (p *Plugin) GetProjectListWithAPIToken(instanceID string) (*jira.ProjectList, error) {
client := &http.Client{}
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/rest/api/3/project/search", instanceID), nil)
if err != nil {
return nil, errors.Wrapf(err, "failed to create HTTP request for fetching project list data. InstanceID: %s", instanceID)
}

err = p.SetAdminAPITokenRequestHeader(req)
if err != nil {
return nil, err
}

resp, err := client.Do(req)
if err != nil {
return nil, errors.Wrapf(err, "failed to fetch project list data. InstanceID: %s", instanceID)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, errors.Errorf("unexpected status code: %d. InstanceID: %s", resp.StatusCode, instanceID)
}

body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "failed to read response body")
}

var projectResponse ProjectSearchResponse
if err = json.Unmarshal(body, &projectResponse); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal project list response")
}

return &projectResponse.Values, nil
}
22 changes: 13 additions & 9 deletions server/kv_mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

jira "github.com/andygrunwald/go-jira"
"github.com/pkg/errors"
"github.com/stretchr/testify/mock"

"github.com/mattermost/mattermost-plugin-jira/server/utils/types"
)
Expand Down Expand Up @@ -98,26 +99,29 @@ func (store mockUserStore) MapUsers(func(*User) error) error {
return nil
}

type mockInstanceStore struct{}
type mockInstanceStore struct {
mock.Mock
}

func (store mockInstanceStore) CreateInactiveCloudInstance(types.ID, string) error {
func (store *mockInstanceStore) CreateInactiveCloudInstance(types.ID, string) error {
return nil
}
func (store mockInstanceStore) DeleteInstance(types.ID) error {
func (store *mockInstanceStore) DeleteInstance(types.ID) error {
return nil
}
func (store mockInstanceStore) LoadInstance(types.ID) (Instance, error) {
return &testInstance{}, nil
func (store *mockInstanceStore) LoadInstance(id types.ID) (Instance, error) {
args := store.Called(id)
return args.Get(0).(Instance), args.Error(1)
}
func (store mockInstanceStore) LoadInstanceFullKey(string) (Instance, error) {
func (store *mockInstanceStore) LoadInstanceFullKey(string) (Instance, error) {
return &testInstance{}, nil
}
func (store mockInstanceStore) LoadInstances() (*Instances, error) {
func (store *mockInstanceStore) LoadInstances() (*Instances, error) {
return NewInstances(), nil
}
func (store mockInstanceStore) StoreInstance(instance Instance) error {
func (store *mockInstanceStore) StoreInstance(instance Instance) error {
return nil
}
func (store mockInstanceStore) StoreInstances(*Instances) error {
func (store *mockInstanceStore) StoreInstances(*Instances) error {
return nil
}
134 changes: 104 additions & 30 deletions server/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"sync"
textTemplate "text/template"

"github.com/andygrunwald/go-jira"
"github.com/gorilla/mux"
"github.com/pkg/errors"

Expand Down Expand Up @@ -82,8 +83,14 @@ type externalConfig struct {
// Display subscription name in notifications
DisplaySubscriptionNameInNotifications bool

// The encryption key used to encrypt stored api tokens
EncryptionKey string

// API token from Jira
AdminAPIToken string

// Email of the admin
AdminEmail string
}

const defaultMaxAttachmentSize = utils.ByteSize(10 * 1024 * 1024) // 10Mb
Expand Down Expand Up @@ -179,6 +186,25 @@ func (p *Plugin) OnConfigurationChange() error {
}
}

jsonBytes, err := json.Marshal(ec.AdminAPIToken)
if err != nil {
p.client.Log.Warn("Error marshaling the admin API token", "error", err.Error())
return err
}

encryptionKey := ec.EncryptionKey
if encryptionKey == "" {
p.client.Log.Warn("Encryption key required to encrypt admin API token")
return errors.New("failed to encrypt admin token. Encryption key not generated")
}

encryptedAdminAPIToken, err := encrypt(jsonBytes, []byte(encryptionKey))
if err != nil {
p.client.Log.Warn("Error encrypting the admin API token", "error", err.Error())
return err
}
ec.AdminAPIToken = string(encryptedAdminAPIToken)

prev := p.getConfig()
p.updateConfig(func(conf *config) {
conf.externalConfig = ec
Expand Down Expand Up @@ -309,41 +335,63 @@ func (p *Plugin) OnActivate() error {
p.enterpriseChecker = enterprise.NewEnterpriseChecker(p.API)

go func() {
for _, url := range instances.IDs() {
var instance Instance
instance, err = p.instanceStore.LoadInstance(url)
if err != nil {
continue
}

ci, ok := instance.(*cloudInstance)
if !ok {
p.client.Log.Info("only cloud instances supported for autolink", "err", err)
continue
}
var status *model.PluginStatus
status, err = p.client.Plugin.GetPluginStatus(autolinkPluginID)
if err != nil {
p.client.Log.Warn("OnActivate: Autolink plugin unavailable. API returned error", "error", err.Error())
continue
}
if status.State != model.PluginStateRunning {
p.client.Log.Warn("OnActivate: Autolink plugin unavailable. Plugin is not running", "status", status)
continue
}

if err = p.AddAutolinksForCloudInstance(ci); err != nil {
p.client.Log.Info("could not install autolinks for cloud instance", "instance", ci.BaseURL, "err", err)
continue
}
}
p.SetupAutolink(instances)
}()

p.initializeTelemetry()

return nil
}

func (p *Plugin) SetupAutolink(instances *Instances) {
for _, url := range instances.IDs() {
var instance Instance
instance, err := p.instanceStore.LoadInstance(url)
if err != nil {
continue
}

if p.getConfig().AdminAPIToken == "" || p.getConfig().AdminEmail == "" {
p.client.Log.Info("unable to setup autolink due to missing API Token or Admin Email")
continue
}

switch instance.(type) {
case *cloudInstance, *cloudOAuthInstance:
default:
p.client.Log.Info("only cloud and cloud-oauth instances supported for autolink")
continue
}

var status *model.PluginStatus
status, err = p.client.Plugin.GetPluginStatus(autolinkPluginID)
if err != nil {
p.client.Log.Warn("OnActivate: Autolink plugin unavailable. API returned error", "error", err.Error())
continue
}

if status.State != model.PluginStateRunning {
p.client.Log.Warn("OnActivate: Autolink plugin unavailable. Plugin is not running", "status", status)
continue
}

switch instance := instance.(type) {
case *cloudInstance:
if err = p.AddAutolinksForCloudInstance(instance); err != nil {
p.client.Log.Info("could not install autolinks for cloud instance", "instance", instance.BaseURL, "error", err.Error())
} else {
p.client.Log.Info("successfully installed autolinks for cloud instance", "instance", instance.BaseURL)
}
case *cloudOAuthInstance:
if err = p.AddAutolinksForCloudOAuthInstance(instance); err != nil {
p.client.Log.Info("could not install autolinks for cloud-oauth instance", "instance", instance.JiraBaseURL, "error", err.Error())
} else {
p.client.Log.Info("successfully installed autolinks for cloud-oauth instance", "instance", instance.JiraBaseURL)
}
}
}
}

func (p *Plugin) AddAutolinksForCloudInstance(ci *cloudInstance) error {
client, err := ci.getClientForBot()
if err != nil {
Expand All @@ -355,9 +403,23 @@ func (p *Plugin) AddAutolinksForCloudInstance(ci *cloudInstance) error {
return fmt.Errorf("unable to get project keys: %w", err)
}

return p.AddAutoLinkForProjects(plist, ci.BaseURL)
}

func (p *Plugin) AddAutolinksForCloudOAuthInstance(coi *cloudOAuthInstance) error {
plist, err := p.GetProjectListWithAPIToken(string(coi.InstanceID))
if err != nil {
return fmt.Errorf("error getting project list: %w", err)
}

return p.AddAutoLinkForProjects(*plist, coi.JiraBaseURL)
}

func (p *Plugin) AddAutoLinkForProjects(plist jira.ProjectList, baseURL string) error {
var err error
for _, proj := range plist {
key := proj.Key
err = p.AddAutolinks(key, ci.BaseURL)
err = p.AddAutolinks(key, baseURL)
}
if err != nil {
return fmt.Errorf("some keys were not installed: %w", err)
Expand All @@ -383,7 +445,10 @@ func (p *Plugin) AddAutolinks(key, baseURL string) error {

client := autolinkclient.NewClientPlugin(p.API)
if err := client.Add(installList...); err != nil {
return fmt.Errorf("unable to add autolinks: %w", err)
// Do not return an error if the status code is 304 (indicating that the autolink for this project is already installed).
if !strings.Contains(err.Error(), `Error: 304, {"status": "OK"}`) {
return fmt.Errorf("unable to add autolinks: %w", err)
}
}

return nil
Expand Down Expand Up @@ -482,6 +547,15 @@ func (c *externalConfig) setDefaults() (bool, error) {
changed = true
}

if c.EncryptionKey == "" {
encryptionKey, err := generateSecret()
if err != nil {
return false, err
}
c.EncryptionKey = encryptionKey
changed = true
}

return changed, nil
}

Expand Down
Loading

0 comments on commit 2e841ba

Please sign in to comment.