Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MM-1137]: Added Autolink support for cloud-oauth #1139

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
6 changes: 3 additions & 3 deletions plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,14 +119,14 @@
"key": "EncryptionKey",
"display_name": "At Rest Encryption Key:",
"type": "generated",
"help_text": "The AES encryption key used to encrypt stored api tokens.",
"help_text": "The encryption key used to encrypt stored api tokens.",
Kshitij-Katiyar marked this conversation as resolved.
Show resolved Hide resolved
"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 and setup autolink.\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 and autolink will not work.",
"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. 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 the project the user cannot access and autolink will not work.",
Kshitij-Katiyar marked this conversation as resolved.
Show resolved Hide resolved
"placeholder": "",
"secret": true,
"default": ""
Expand All @@ -135,7 +135,7 @@
"key": "AdminEmail",
"display_name": "Admin Email",
"type": "text",
"help_text": "**Note** Admin email is necessary to setup autolink for Jira plugin",
"help_text": "**Note** Admin email is necessary to setup autolink for Jira plugin and to to get notified for comment and issue created events even if the user triggering the event is not connected to Jira",
Kshitij-Katiyar marked this conversation as resolved.
Show resolved Hide resolved
"placeholder": "",
"default": ""
}
Expand Down
25 changes: 2 additions & 23 deletions server/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
package main

import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
Expand Down Expand Up @@ -1104,20 +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)
}

encryptedAdminAPIToken := p.getConfig().AdminAPIToken
jsonBytes, err := decrypt([]byte(encryptedAdminAPIToken), []byte(p.getConfig().EncryptionKey))
err = p.SetAdminAPITokenRequestHeader(req)
if err != nil {
return nil, err
}
var adminAPIToken string
err = json.Unmarshal(jsonBytes, &adminAPIToken)
if err != nil {
return nil, err
}

encodedAuth := base64.StdEncoding.EncodeToString([]byte(p.getConfig().AdminEmail + ":" + adminAPIToken))
req.Header.Set("Authorization", "Basic "+encodedAuth)
req.Header.Set("Accept", "application/json")

resp, err := client.Do(req)
if err != nil {
Expand Down Expand Up @@ -1165,20 +1154,10 @@ func (p *Plugin) GetProjectListWithAPIToken(instanceID string) (*jira.ProjectLis
return nil, errors.Wrapf(err, "failed to create HTTP request for fetching project list data. InstanceID: %s", instanceID)
}

encryptedAdminAPIToken := p.getConfig().AdminAPIToken
jsonBytes, err := decrypt([]byte(encryptedAdminAPIToken), []byte(p.getConfig().EncryptionKey))
err = p.SetAdminAPITokenRequestHeader(req)
if err != nil {
return nil, err
}
var adminAPIToken string
err = json.Unmarshal(jsonBytes, &adminAPIToken)
if err != nil {
return nil, err
}

encodedAuth := base64.StdEncoding.EncodeToString([]byte(p.getConfig().AdminEmail + ":" + adminAPIToken))
req.Header.Set("Authorization", "Basic "+encodedAuth)
req.Header.Set("Accept", "application/json")

resp, err := client.Do(req)
if err != nil {
Expand Down
40 changes: 18 additions & 22 deletions server/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ type externalConfig struct {
// Display subscription name in notifications
DisplaySubscriptionNameInNotifications bool

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

// API token from Jira
Expand Down Expand Up @@ -186,12 +186,17 @@ func (p *Plugin) OnConfigurationChange() error {
}
}

adminAPIToken := ec.AdminAPIToken
jsonBytes, err := json.Marshal(adminAPIToken)
jsonBytes, err := json.Marshal(ec.AdminAPIToken)
if err != nil {
return err
}
Kshitij-Katiyar marked this conversation as resolved.
Show resolved Hide resolved
encryptedAdminAPIToken, err := encrypt(jsonBytes, []byte(p.getConfig().EncryptionKey))

encryptionKey := p.getConfig().EncryptionKey
if encryptionKey == "" {
return errors.New("failed to encrypt admin token. Encryption key not generated")
}

encryptedAdminAPIToken, err := encrypt(jsonBytes, []byte(encryptionKey))
if err != nil {
return err
Kshitij-Katiyar marked this conversation as resolved.
Show resolved Hide resolved
}
Expand Down Expand Up @@ -348,14 +353,6 @@ func (p *Plugin) SetupAutolink(instances *Instances) {
continue
}

ci, ciOk := instance.(*cloudInstance)
coi, coiOk := instance.(*cloudOAuthInstance)

if !ciOk && !coiOk {
p.client.Log.Info("only cloud and cloud-oauth instances supported for autolink")
continue
}

Kshitij-Katiyar marked this conversation as resolved.
Show resolved Hide resolved
var status *model.PluginStatus
status, err = p.client.Plugin.GetPluginStatus(autolinkPluginID)
if err != nil {
Expand All @@ -368,18 +365,17 @@ func (p *Plugin) SetupAutolink(instances *Instances) {
continue
}

if ciOk {
if err = p.AddAutolinksForCloudInstance(ci); err != nil {
p.client.Log.Info("could not install autolinks for cloud instance", "instance", ci.BaseURL, "err", err)
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)
}
}

if coiOk {
if err = p.AddAutolinksForCloudOAuthInstance(coi); err != nil {
p.client.Log.Info("could not install autolinks for cloud-oauth instance", "instance", coi.JiraBaseURL, "err", err)
continue
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)
}
default:
p.client.Log.Info("only cloud and cloud-oauth instances supported for autolink")
}
}
}
Expand Down
39 changes: 21 additions & 18 deletions server/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,22 +156,14 @@ func TestPlugin(t *testing.T) {
}

func TestSetupAutolink(t *testing.T) {
mockAPI := &plugintest.API{}
dummyInstanceStore := new(mockInstanceStore)
mockPluginClient := pluginapi.NewClient(mockAPI, nil)
p := &Plugin{
client: mockPluginClient,
instanceStore: dummyInstanceStore,
}

tests := []struct {
name string
setup func()
setup func(*Plugin, *plugintest.API, *mockInstanceStore)
InstanceType InstanceType
}{
{
name: "Missing API token or Admin email",
setup: func() {
setup: func(p *Plugin, mockAPI *plugintest.API, dummyInstanceStore *mockInstanceStore) {
mockAPI.On("LogInfo", "unable to setup autolink due to missing API Token or Admin Email").Return(nil).Times(1)
dummyInstanceStore.On("LoadInstance", mock.Anything).Return(&serverInstance{}, nil).Times(1)

Expand All @@ -184,7 +176,8 @@ func TestSetupAutolink(t *testing.T) {
},
{
name: "Unsupported instance type",
setup: func() {
setup: func(p *Plugin, mockAPI *plugintest.API, dummyInstanceStore *mockInstanceStore) {
mockAPI.On("GetPluginStatus", "mattermost-autolink").Return(&model.PluginStatus{State: model.PluginStateRunning}, nil).Times(1)
mockAPI.On("LogInfo", "only cloud and cloud-oauth instances supported for autolink").Return(nil).Times(1)
dummyInstanceStore.On("LoadInstance", mock.Anything).Return(&serverInstance{}, nil).Times(1)

Expand All @@ -194,7 +187,7 @@ func TestSetupAutolink(t *testing.T) {
},
{
name: "Autolink plugin unavailable API returned error",
setup: func() {
setup: func(p *Plugin, mockAPI *plugintest.API, dummyInstanceStore *mockInstanceStore) {
mockAPI.On("LogWarn", "OnActivate: Autolink plugin unavailable. API returned error", "error", mock.Anything).Return(nil).Times(1)
mockAPI.On("GetPluginStatus", autolinkPluginID).Return(nil, &model.AppError{Message: "error getting plugin status"}).Times(1)
dummyInstanceStore.On("LoadInstance", mock.Anything).Return(&cloudInstance{}, nil).Times(1)
Expand All @@ -205,7 +198,7 @@ func TestSetupAutolink(t *testing.T) {
},
{
name: "Autolink plugin not running",
setup: func() {
setup: func(p *Plugin, mockAPI *plugintest.API, dummyInstanceStore *mockInstanceStore) {
mockAPI.On("LogWarn", "OnActivate: Autolink plugin unavailable. Plugin is not running", "status", &model.PluginStatus{State: model.PluginStateNotRunning}).Return(nil).Times(1)
mockAPI.On("GetPluginStatus", autolinkPluginID).Return(&model.PluginStatus{State: model.PluginStateNotRunning}, nil).Times(1)
dummyInstanceStore.On("LoadInstance", mock.Anything).Return(&cloudInstance{}, nil).Times(1)
Expand All @@ -216,8 +209,8 @@ func TestSetupAutolink(t *testing.T) {
},
{
name: "Error installing autolinks for cloud instance",
setup: func() {
mockAPI.On("LogInfo", "could not install autolinks for cloud instance", "instance", "mockBaseURL", "err", mock.Anything).Return(nil).Times(1)
setup: func(p *Plugin, mockAPI *plugintest.API, dummyInstanceStore *mockInstanceStore) {
mockAPI.On("LogInfo", "could not install autolinks for cloud instance", "instance", "mockBaseURL", "error", mock.Anything).Return(nil).Times(1)
mockAPI.On("GetPluginStatus", autolinkPluginID).Return(&model.PluginStatus{State: model.PluginStateRunning}, nil).Times(1)
dummyInstanceStore.On("LoadInstance", mock.Anything).Return(
&cloudInstance{
Expand All @@ -238,9 +231,11 @@ func TestSetupAutolink(t *testing.T) {
},
{
name: "Error installing autolinks for cloud-oauth instance",
setup: func() {
mockAPI.On("LogInfo", "could not install autolinks for cloud-oauth instance", "instance", "mockBaseURL", "err", mock.Anything).Return(nil).Times(1)
setup: func(p *Plugin, mockAPI *plugintest.API, dummyInstanceStore *mockInstanceStore) {
mockAPI.On("LogWarn", "Error unmarshalling admin API token", "error", mock.Anything).Times(1)
mockAPI.On("LogInfo", "could not install autolinks for cloud-oauth instance", "instance", "mockBaseURL", "error", mock.Anything).Return(nil).Times(1)
mockAPI.On("GetPluginStatus", autolinkPluginID).Return(&model.PluginStatus{State: model.PluginStateRunning}, nil).Times(1)

dummyInstanceStore.On("LoadInstance", mock.Anything).Return(
&cloudOAuthInstance{
InstanceCommon: &InstanceCommon{
Expand All @@ -255,8 +250,16 @@ func TestSetupAutolink(t *testing.T) {
},
}
for _, tt := range tests {
mockAPI := &plugintest.API{}
dummyInstanceStore := new(mockInstanceStore)
mockPluginClient := pluginapi.NewClient(mockAPI, nil)
p := &Plugin{
client: mockPluginClient,
instanceStore: dummyInstanceStore,
}

t.Run(tt.name, func(t *testing.T) {
tt.setup()
tt.setup(p, mockAPI, dummyInstanceStore)
instances := GetInstancesWithType(tt.InstanceType)

p.SetupAutolink(instances)
Expand Down
23 changes: 23 additions & 0 deletions server/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"regexp"
"strings"

Expand Down Expand Up @@ -182,3 +184,24 @@ func getS256PKCEParams() (*PKCEParams, error) {
CodeVerifier: verifier,
}, nil
}

func (p *Plugin) SetAdminAPITokenRequestHeader(req *http.Request) error {
encryptedAdminAPIToken := p.getConfig().AdminAPIToken
jsonBytes, err := decrypt([]byte(encryptedAdminAPIToken), []byte(p.getConfig().EncryptionKey))
if err != nil {
p.client.Log.Warn("Error decrypting admin API token", "error", err)
return err
}
var adminAPIToken string
err = json.Unmarshal(jsonBytes, &adminAPIToken)
if err != nil {
p.client.Log.Warn("Error unmarshalling admin API token", "error", err)
return err
}

encodedAuth := base64.StdEncoding.EncodeToString([]byte(p.getConfig().AdminEmail + ":" + adminAPIToken))
req.Header.Set("Authorization", "Basic "+encodedAuth)
req.Header.Set("Accept", "application/json")

return nil
}
Loading