Skip to content

Commit

Permalink
Implemented modal to configure OAuth2 credentials
Browse files Browse the repository at this point in the history
  • Loading branch information
srkgupta committed Mar 30, 2023
1 parent c24cb05 commit 333f7d3
Show file tree
Hide file tree
Showing 19 changed files with 588 additions and 88 deletions.
2 changes: 2 additions & 0 deletions assets/templates/oauth2/complete.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
.btn-link {
color: #505f79;
background: #f4f5f7;
padding: 10px;
}

.btn-link:hover,
Expand Down Expand Up @@ -69,6 +70,7 @@ <h3>
<div>Jira account: {{ .JiraDisplayName }}</div>
<div>It is now safe to close this browser window.</div>
</div>
<a href="javascript:window.close();" class="btn btn-link">Close</a>
<a href="{{ .RevokeURL }}" class="btn btn-link">Disconnect</a>
</div>
</body>
Expand Down
91 changes: 31 additions & 60 deletions server/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,38 +22,36 @@ const commandTrigger = "jira"

var jiraCommandHandler = CommandHandler{
handlers: map[string]CommandHandlerFunc{
"assign": executeAssign,
"connect": executeConnect,
"disconnect": executeDisconnect,
"help": executeHelp,
"info": executeInfo,
"install/cloud": executeInstanceInstallCloud,
"install/cloud-oauth": executeInstanceInstallCloudOAuth,
"install/server": executeInstanceInstallServer,
"instance/alias": executeInstanceAlias,
"instance/unalias": executeInstanceUnalias,
"instance/connect": executeConnect,
"instance/disconnect": executeDisconnect,
"instance/install/cloud": executeInstanceInstallCloud,
"instance/install/cloud-oauth": executeInstanceInstallCloudOAuth,
"instance/install/server": executeInstanceInstallServer,
"instance/list": executeInstanceList,
"instance/settings": executeSettings,
"instance/uninstall": executeInstanceUninstall,
"instance/v2": executeInstanceV2Legacy,
"issue/assign": executeAssign,
"issue/transition": executeTransition,
"issue/unassign": executeUnassign,
"issue/view": executeView,
"settings": executeSettings,
"subscribe/list": executeSubscribeList,
"transition": executeTransition,
"unassign": executeUnassign,
"uninstall": executeInstanceUninstall,
"view": executeView,
"v2revert": executeV2Revert,
"webhook": executeWebhookURL,
"setup": executeSetup,
"assign": executeAssign,
"connect": executeConnect,
"disconnect": executeDisconnect,
"help": executeHelp,
"info": executeInfo,
"install/cloud": executeInstanceInstallCloud,
"install/server": executeInstanceInstallServer,
"instance/alias": executeInstanceAlias,
"instance/unalias": executeInstanceUnalias,
"instance/connect": executeConnect,
"instance/disconnect": executeDisconnect,
"instance/install/cloud": executeInstanceInstallCloud,
"instance/install/server": executeInstanceInstallServer,
"instance/list": executeInstanceList,
"instance/settings": executeSettings,
"instance/uninstall": executeInstanceUninstall,
"instance/v2": executeInstanceV2Legacy,
"issue/assign": executeAssign,
"issue/transition": executeTransition,
"issue/unassign": executeUnassign,
"issue/view": executeView,
"settings": executeSettings,
"subscribe/list": executeSubscribeList,
"transition": executeTransition,
"unassign": executeUnassign,
"uninstall": executeInstanceUninstall,
"view": executeView,
"v2revert": executeV2Revert,
"webhook": executeWebhookURL,
"setup": executeSetup,
},
defaultHandler: executeJiraDefault,
}
Expand All @@ -79,7 +77,7 @@ const commonHelpText = "\n" +
const sysAdminHelpText = "\n###### For System Administrators:\n" +
"Install Jira instances:\n" +
"* `/jira instance install cloud [jiraURL]` - Connect Mattermost to a Jira Cloud instance located at <jiraURL>\n" +
"* `/jira instance install cloud-oauth [jiraURL]` - Connect Mattermost to a Jira Cloud instance using OAuth 2.0 located at <jiraURL>\n" +
"* `/jira instance install cloud-oauth` - Connect Mattermost to a Jira Cloud instance using OAuth 2.0 located at <jiraURL>\n" +
"* `/jira instance install server [jiraURL]` - Connect Mattermost to a Jira Server or Data Center instance located at <jiraURL>\n" +
"Uninstall Jira instances:\n" +
"* `/jira instance uninstall cloud [jiraURL]` - Disconnect Mattermost from a Jira Cloud instance located at <jiraURL>\n" +
Expand Down Expand Up @@ -801,33 +799,6 @@ func executeInstanceInstallCloud(p *Plugin, c *plugin.Context, header *model.Com
})
}

func executeInstanceInstallCloudOAuth(p *Plugin, c *plugin.Context, header *model.CommandArgs, args ...string) *model.CommandResponse {
authorized, err := authorizedSysAdmin(p, header.UserId)
if err != nil {
return p.responsef(header, "%v", err)
}
if !authorized {
return p.responsef(header, "`/jira install` can only be run by a system administrator.")
}
if len(args) != 3 {
return p.help(header)
}

clientID := args[1]
clientSecret := args[2]

jiraURL, instance, err := p.installCloudOAuthInstance(args[0], clientID, clientSecret)
if err != nil {
return p.responsef(header, err.Error())
}

return p.respondCommandTemplate(header, "/command/install_cloud_oath.md", map[string]string{
"JiraURL": jiraURL,
"PluginURL": p.GetPluginURL(),
"MattermostKey": instance.GetMattermostKey(),
})
}

func executeInstanceInstallServer(p *Plugin, c *plugin.Context, header *model.CommandArgs, args ...string) *model.CommandResponse {
authorized, err := authorizedSysAdmin(p, header.UserId)
if err != nil {
Expand Down
5 changes: 4 additions & 1 deletion server/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const (
routeUserConnect = "/user/connect"
routeUserDisconnect = "/user/disconnect"
routeSharePublicly = "/api/v2/share-issue-publicly"
routeCloudOAuthConfigure = "/api/v2/cloud-oauth2"
routeOAuth2Complete = "/oauth2/complete.html"
)

Expand Down Expand Up @@ -159,7 +160,7 @@ func (p *Plugin) serveHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Req
routeACUserDisconnected:
return p.httpACUserInteractive(w, r, callbackInstanceID)

// Command autocomplete
// Command autocomplete
case routeAutocompleteConnect:
return p.httpAutocompleteConnect(w, r)
case routeAutocompleteUserInstance:
Expand All @@ -182,6 +183,8 @@ func (p *Plugin) serveHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Req
// OAuth2 (Jira Cloud)
case routeOAuth2Complete:
return p.httpOAuth2Complete(w, r, callbackInstanceID)
case routeCloudOAuthConfigure:
return p.httpOAuth2Configure(w, r)

// User connect/disconnect links
case routeUserConnect:
Expand Down
23 changes: 15 additions & 8 deletions server/instance_cloud_oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import (
"strings"

jira "github.com/andygrunwald/go-jira"
"github.com/mattermost/mattermost-plugin-jira/server/utils"
"github.com/mattermost/mattermost-plugin-jira/server/utils/types"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/pkg/errors"
"golang.org/x/oauth2"

"github.com/mattermost/mattermost-plugin-jira/server/utils"
"github.com/mattermost/mattermost-plugin-jira/server/utils/types"
)

type cloudOAuthInstance struct {
Expand All @@ -27,16 +28,22 @@ type cloudOAuthInstance struct {
JiraClientSecret string
}

type CloudOAuthConfigure struct {
InstanceURL string `json:"instance_url"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
}

type JiraAccessibleResources []struct {
Id string
ID string
}

var _ Instance = (*cloudOAuthInstance)(nil)

const (
JIRA_SCOPES = "read:jira-user,read:jira-work,write:jira-work"
JIRA_RESPONSE_TYPE = "code"
JIRA_CONSENT = "consent"
JiraScopes = "read:jira-user,read:jira-work,write:jira-work"
JiraResponseType = "code"
JiraConsent = "consent"
)

func (p *Plugin) installCloudOAuthInstance(rawURL string, clientID string, clientSecret string) (string, *cloudOAuthInstance, error) {
Expand Down Expand Up @@ -115,7 +122,7 @@ func (ci *cloudOAuthInstance) GetOAuthConfig() *oauth2.Config {
return &oauth2.Config{
ClientID: ci.JiraClientID,
ClientSecret: ci.JiraClientSecret,
Scopes: strings.Split(JIRA_SCOPES, ","),
Scopes: strings.Split(JiraScopes, ","),
RedirectURL: fmt.Sprintf("%s%s", ci.Plugin.GetPluginURL(), instancePath(routeOAuth2Complete, ci.InstanceID)),
Endpoint: oauth2.Endpoint{
AuthURL: "https://auth.atlassian.com/authorize",
Expand Down Expand Up @@ -175,5 +182,5 @@ func (ci *cloudOAuthInstance) getJiraCloudResourceID(client http.Client) (string
return "", errors.New("No resources available for this Jira Cloud Account")
}

return resources[0].Id, nil
return resources[0].ID, nil
}
21 changes: 4 additions & 17 deletions server/setup_flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ const (
stepCloudAddedInstance flow.Name = "cloud-added"
stepCloudOAuthConfigure flow.Name = "cloud-oauth-configure"
stepCloudOAuthSetCallbackURL flow.Name = "cloud-oauth-callback"
stepCloudOAuthConnect flow.Name = "cloud-oauth-connect"
stepCloudEnableDeveloperMode flow.Name = "cloud-enable-dev"
stepCloudUploadApp flow.Name = "cloud-upload-app"
stepInstalledJiraApp flow.Name = "installed-app"
Expand Down Expand Up @@ -75,7 +74,6 @@ func (p *Plugin) NewSetupFlow() *flow.Flow {
// Jira Cloud OAuth steps
p.stepCloudOAuthConfigure(),
p.stepCloudOAuthSetCallbackURL(),
p.stepCloudOAuthConnect(),

// Jira server steps
p.stepServerAddAppLink(),
Expand Down Expand Up @@ -329,15 +327,15 @@ func (p *Plugin) stepCloudUploadApp() flow.Step {

func (p *Plugin) stepCloudOAuthConfigure() flow.Step {
return flow.NewStep(stepCloudOAuthConfigure).
WithPretext("##### :white_check_mark: Step 2: Register an OAuth 2.0 Application in Jira").
WithPretext("##### :white_check_mark: Step 2(a): Register an OAuth 2.0 Application in Jira").
WithText(fmt.Sprintf("Complete the following steps, then come back here to select **Configure**.\n\n"+
"1. Follow [these instructions](https://developer.atlassian.com/cloud/confluence/oauth-2-3lo-apps/#enabling-oauth-2-0--3lo-) to register an OAuth 2.0 application in Jira.\n"+
"2. Set the following values:\n"+
" - Name: `Mattermost Jira Plugin - <your company name>`\n"+
"3. Select **Permissions** in the left menu. Next to the JIRA API, select **Add**\n"+
"4. Then select **Configure** and ensure following scopes are selected:\n"+
" - Scopes: `%s`\n"+
"3. Copy the **Client ID** and **Secret** from the registed 0Auth Application's **Settings** page and keep it handy.\n", JIRA_SCOPES)).
"3. Copy the **Client ID** and **Secret** from the registered 0Auth Application's **Settings** page and keep it handy.\n", JiraScopes)).
WithButton(flow.Button{
Name: "Configure",
Color: flow.ColorPrimary,
Expand Down Expand Up @@ -377,24 +375,13 @@ func (p *Plugin) stepCloudOAuthConfigure() flow.Step {

func (p *Plugin) stepCloudOAuthSetCallbackURL() flow.Step {
return flow.NewStep(stepCloudOAuthSetCallbackURL).
WithPretext("##### :white_check_mark: Step 3: Set Callback URL in the Jira OAuth 2.0 app").
WithPretext("##### :white_check_mark: Step 2(b): Set Callback URL in the Jira OAuth 2.0 app").
WithText("It is important that you correctly set the Callback URL in the Jira OAuth 2.0 app. Follow the below instructions:\n\n" +
"1. In the Jira Developer console, click on the OAuth 2.0 app you had created and select **Authorization** in the left menu.\n" +
"2. Next to OAuth 2.0 (3LO), select **Configure** and set the Callback URL as follows:\n" +
" `{{.OAuthCompleteURL}}`\n" +
"3. Click **Save Changes**.\n").
WithButton(continueButton(stepCloudOAuthConnect))
}

func (p *Plugin) stepCloudOAuthConnect() flow.Step {
return flow.NewStep(stepCloudOAuthConnect).
WithPretext("##### :white_check_mark: Step 4: Connect your Jira account").
WithText("Go [here]({{.ConnectURL}}) to connect your Jira account\n").
OnRender(func(f *flow.Flow) {
p.trackSetupWizard("oauth_wizard_complete", map[string]interface{}{
keyEdition: f.GetState().GetString(keyEdition),
})(f)
})
WithButton(continueButton(stepInstalledJiraApp))
}

func (p *Plugin) stepInstalledJiraApp() flow.Step {
Expand Down
48 changes: 47 additions & 1 deletion server/user_cloud_oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,61 @@ package main

import (
"context"
"encoding/json"
"io/ioutil"
"net/http"
"path"
"strings"

"github.com/mattermost/mattermost-plugin-jira/server/utils/types"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/pkg/errors"

"github.com/mattermost/mattermost-plugin-jira/server/utils/types"
)

func (p *Plugin) httpOAuth2Configure(w http.ResponseWriter, r *http.Request) (status int, err error) {
if r.Method != http.MethodPost {
return respondErr(w, http.StatusMethodNotAllowed,
errors.New("method "+r.Method+" is not allowed, must be POST"))
}

mattermostUserID := r.Header.Get("Mattermost-User-Id")
if mattermostUserID == "" {
return respondErr(w, http.StatusUnauthorized,
errors.New("not authorized"))
}

authorized, err := authorizedSysAdmin(p, mattermostUserID)
if err != nil {
return respondErr(w, http.StatusInternalServerError, err)
}
if !authorized {
return respondErr(w, http.StatusUnauthorized,
errors.New("not authorized"))
}

body, err := ioutil.ReadAll(r.Body)
if err != nil {
return respondErr(w, http.StatusInternalServerError,
errors.WithMessage(err, "failed to decode request"))
}

var config CloudOAuthConfigure
err = json.Unmarshal(body, &config)
if err != nil {
return respondErr(w, http.StatusBadRequest,
errors.WithMessage(err, "failed to unmarshal request"))
}

_, _, err = p.installCloudOAuthInstance(config.InstanceURL, config.ClientID, config.ClientSecret)
if err != nil {
return respondErr(w, http.StatusBadRequest,
errors.WithMessage(err, "unable to configure cloud oauth"))
}

return respondJSON(w, []string{"OK"})
}

func (p *Plugin) httpOAuth2Complete(w http.ResponseWriter, r *http.Request, instanceID types.ID) (status int, err error) {
code := r.URL.Query().Get("code")
if code == "" {
Expand Down
3 changes: 3 additions & 0 deletions webapp/src/action_types/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,7 @@ export default {

RECEIVED_CHANNEL_SUBSCRIPTIONS: `${PluginId}_recevied_channel_subscriptions`,
DELETED_CHANNEL_SUBSCRIPTION: `${PluginId}_deleted_channel_subscription`,

OPEN_OAUTH_CONFIG_MODAL: `${PluginId}_open_oauth_config_modal`,
CLOSE_OAUTH_CONFIG_MODAL: `${PluginId}_close_oauth_config_modal`,
};
39 changes: 39 additions & 0 deletions webapp/src/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -509,3 +509,42 @@ export function sendEphemeralPost(message: string, channelId?: string) {
});
};
}

export const openOAuthConfigModal = () => {
return {
type: ActionTypes.OPEN_OAUTH_CONFIG_MODAL,
};
};

export const closeOAuthConfigModal = () => {
return {
type: ActionTypes.CLOSE_OAUTH_CONFIG_MODAL,
};
};

export function handleInstallOAuthFlow(instanceID?: string) {
return async (dispatch) => {
dispatch(openOAuthConfigModal());
};
}

export const configureCloudOAuthInstance = (payload) => {
return async (dispatch, getState) => {
const baseUrl = getPluginServerRoute(getState());
try {
const data = await doFetch(`${baseUrl}/api/v2/cloud-oauth2`, {
method: 'post',
body: JSON.stringify(payload),
});

if (data.error) {
return {error: new Error(data.error)};
}

dispatch(sendEphemeralPost('You\'ve finished installing the Jira Cloud OAuth2 instance.'));
return {data};
} catch (error) {
return {error};
}
};
};
Loading

0 comments on commit 333f7d3

Please sign in to comment.