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
Open
18 changes: 17 additions & 1 deletion plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,27 @@
"placeholder": "",
"default": false
},
{
"key": "EncryptionKey",
"display_name": "At Rest Encryption Key:",
"type": "generated",
"help_text": "The encryption key used to encrypt stored API tokens.",
"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
}
116 changes: 87 additions & 29 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
}
Kshitij-Katiyar marked this conversation as resolved.
Show resolved Hide resolved

encryptionKey := p.getConfig().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
Kshitij-Katiyar marked this conversation as resolved.
Show resolved Hide resolved
}
ec.AdminAPIToken = string(encryptedAdminAPIToken)

prev := p.getConfig()
p.updateConfig(func(conf *config) {
conf.externalConfig = ec
Expand Down Expand Up @@ -309,41 +335,59 @@ 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())
}
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())
}
}
}
}

func (p *Plugin) AddAutolinksForCloudInstance(ci *cloudInstance) error {
client, err := ci.getClientForBot()
if err != nil {
Expand All @@ -355,9 +399,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 Down
Loading
Loading