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-60676] Fix incorrect callback URL in setup flow #827

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 35 additions & 8 deletions server/plugin/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"runtime/debug"
"strconv"
"strings"
Expand Down Expand Up @@ -289,15 +290,20 @@ func (p *Plugin) connectUserToGitHub(c *Context, w http.ResponseWriter, r *http.
privateAllowed = true
}

conf := p.getOAuthConfig(privateAllowed)
conf, err := p.getOAuthConfig(privateAllowed)
if err != nil {
c.Log.WithError(err).Warnf("Failed to generate OAuthConfig")
http.Error(w, "error generating OAuthConfig", http.StatusBadRequest)
return
}

state := OAuthState{
UserID: c.UserID,
Token: model.NewId()[:15],
PrivateAllowed: privateAllowed,
}

_, err := p.store.Set(githubOauthKey+state.Token, state, pluginapi.SetExpiry(TokenTTL))
_, err = p.store.Set(githubOauthKey+state.Token, state, pluginapi.SetExpiry(TokenTTL))
if err != nil {
c.Log.WithError(err).Warnf("error occurred while trying to store oauth state into KV store")
p.writeAPIError(w, &APIErrorResponse{Message: "error saving the oauth state", StatusCode: http.StatusInternalServerError})
Expand Down Expand Up @@ -379,7 +385,12 @@ func (p *Plugin) completeConnectUserToGitHub(c *Context, w http.ResponseWriter,
return
}

conf := p.getOAuthConfig(state.PrivateAllowed)
conf, err := p.getOAuthConfig(state.PrivateAllowed)
if err != nil {
c.Log.WithError(err).Warnf("Failed to generate OAuthConfig")
http.Error(w, "error generating OAuthConfig", http.StatusBadRequest)
return
}

ctx, cancel := context.WithTimeout(context.Background(), oauthCompleteTimeout)
defer cancel()
Expand Down Expand Up @@ -819,10 +830,18 @@ func (p *Plugin) searchIssues(c *UserContext, w http.ResponseWriter, r *http.Req
p.writeJSON(w, allIssues)
}

func (p *Plugin) getPermaLink(postID string) string {
siteURL := *p.client.Configuration.GetConfig().ServiceSettings.SiteURL
func (p *Plugin) getPermaLink(postID string) (string, error) {
siteURL, err := getSiteURL(p.client)
if err != nil {
return "", err
}

return fmt.Sprintf("%v/_redirect/pl/%v", siteURL, postID)
redirectURL, err := url.JoinPath(siteURL, "_redirect", "pl", postID)
if err != nil {
return "", errors.Wrap(err, "failed to build pluginURL")
}

return redirectURL, nil
}

func getFailReason(code int, repo string, username string) string {
Expand Down Expand Up @@ -904,7 +923,11 @@ func (p *Plugin) createIssueComment(c *UserContext, w http.ResponseWriter, r *ht
}

currentUsername := c.GHInfo.GitHubUsername
permalink := p.getPermaLink(req.PostID)
permalink, err := p.getPermaLink(req.PostID)
if err != nil {
p.writeAPIError(w, &APIErrorResponse{ID: "", Message: "failed to generate permalink", StatusCode: http.StatusInternalServerError})
return
}
permalinkMessage := fmt.Sprintf("*@%s attached a* [message](%s) *from %s*\n\n", currentUsername, permalink, commentUsername)

req.Comment = permalinkMessage + req.Comment
Expand Down Expand Up @@ -1347,7 +1370,11 @@ func (p *Plugin) createIssue(c *UserContext, w http.ResponseWriter, r *http.Requ
return
}

permalink = p.getPermaLink(issue.PostID)
permalink, err = p.getPermaLink(issue.PostID)
if err != nil {
p.writeAPIError(w, &APIErrorResponse{ID: "", Message: "failed to generate permalink", StatusCode: http.StatusInternalServerError})
return
}

mmMessage = fmt.Sprintf("_Issue created from a [Mattermost message](%v) *by %s*._", permalink, username)
}
Expand Down
13 changes: 8 additions & 5 deletions server/plugin/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,10 @@ func (p *Plugin) checkIfConfiguredWebhookExists(ctx context.Context, githubClien
opt := &github.ListOptions{
PerPage: PerPageValue,
}
siteURL := *p.client.Configuration.GetConfig().ServiceSettings.SiteURL
siteURL, err := getSiteURL(p.client)
if err != nil {
return false, err
}

for {
var githubHooks []*github.Hook
Expand Down Expand Up @@ -804,9 +807,9 @@ func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*mo
}

if action == "connect" {
siteURL := p.client.Configuration.GetConfig().ServiceSettings.SiteURL
if siteURL == nil {
p.postCommandResponse(args, "Encountered an error connecting to GitHub.")
connectURL, err := buildPluginURL(p.client, "oauth", "connect")
if err != nil {
p.postCommandResponse(args, fmt.Sprintf("Encountered an error connecting to GitHub: %s", err.Error()))
return &model.CommandResponse{}, nil
}

Expand Down Expand Up @@ -834,7 +837,7 @@ func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*mo
qparams = "?private=true"
}

msg := fmt.Sprintf("[Click here to link your GitHub account.](%s/plugins/%s/oauth/connect%s)", *siteURL, Manifest.Id, qparams)
msg := fmt.Sprintf("[Click here to link your GitHub account.](%s%s)", connectURL, qparams)
p.postCommandResponse(args, msg)
return &model.CommandResponse{}, nil
}
Expand Down
22 changes: 18 additions & 4 deletions server/plugin/flows.go
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,11 @@ func (fm *FlowManager) submitEnterpriseConfig(f *flow.Flow, submitted map[string
}

func (fm *FlowManager) stepOAuthInfo() flow.Step {
callbackURL, err := buildPluginURL(fm.client, "oauth", "complete")
if err != nil {
fm.client.Log.Warn("Failed to build callbackURL", "err", err)
}

oauthPretext := `
##### :white_check_mark: Step 1: Register an OAuth Application in GitHub
You must first register the Mattermost GitHub Plugin as an authorized OAuth app.`
Expand All @@ -494,11 +499,11 @@ You must first register the Mattermost GitHub Plugin as an authorized OAuth app.
"2. Set the following values:\n"+
" - Application name: `Mattermost GitHub Plugin - <your company name>`\n"+
" - Homepage URL: `https://github.com/mattermost/mattermost-plugin-github`\n"+
" - Authorization callback URL: `%s/oauth/complete`\n"+
" - Authorization callback URL: `%s`\n"+
"3. Select **Register application**\n"+
"4. Select **Generate a new client secret**.\n"+
"5. If prompted, complete 2FA.",
fm.pluginID,
callbackURL,
)

return flow.NewStep(stepOAuthInfo).
Expand Down Expand Up @@ -597,7 +602,11 @@ func (fm *FlowManager) submitOAuthConfig(f *flow.Flow, submitted map[string]inte

func (fm *FlowManager) stepOAuthConnect() flow.Step {
connectPretext := "##### :white_check_mark: Step {{ if .UsePreregisteredApplication }}1{{ else }}2{{ end }}: Connect your GitHub account"
connectURL := fmt.Sprintf("%s/oauth/connect", fm.pluginID)
connectURL, err := buildPluginURL(fm.client, "oauth", "connect")
if err != nil {
fm.client.Log.Warn("Failed to build connectURL", "err", err)
}

connectText := fmt.Sprintf("Go [here](%s) to connect your account.", connectURL)
return flow.NewStep(stepOAuthConnect).
WithText(connectText).
Expand Down Expand Up @@ -685,11 +694,16 @@ func (fm *FlowManager) submitWebhook(f *flow.Flow, submitted map[string]interfac

webhookEvents := []string{"create", "delete", "issue_comment", "issues", "pull_request", "pull_request_review", "pull_request_review_comment", "push", "star"}

webHookURL, err := buildPluginURL(fm.client, "webhook")
if err != nil {
fm.client.Log.Warn("Failed to build webHookURL", "err", err)
}

webhookConfig := map[string]interface{}{
"content_type": "json",
"insecure_ssl": "0",
"secret": config.WebhookSecret,
"url": fmt.Sprintf("%s/webhook", fm.pluginID),
"url": webHookURL,
}

hook := &github.Hook{
Expand Down
9 changes: 3 additions & 6 deletions server/plugin/graphql/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package graphql
import (
"context"
"net/url"
"path"

"github.com/pkg/errors"
"github.com/shurcooL/githubv4"
Expand Down Expand Up @@ -36,16 +35,14 @@ func NewClient(logger pluginapi.LogService, getOrganizations func() []string, to
getOrganizations: getOrganizations,
}
} else {
baseURL, err := url.Parse(enterpriseBaseURL)
baseURL, err := url.JoinPath(enterpriseBaseURL, "api", "graphql")
if err != nil {
logger.Debug("Not able to parse the URL", "error", err.Error())
logger.Debug("Not able to parse the enterprise URL", "error", err.Error())
return nil
}

baseURL.Path = path.Join(baseURL.Path, "api", "graphql")

client = Client{
client: githubv4.NewEnterpriseClient(baseURL.String(), httpClient),
client: githubv4.NewEnterpriseClient(baseURL, httpClient),
username: username,
org: orgName,
logger: logger,
Expand Down
73 changes: 42 additions & 31 deletions server/plugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"fmt"
"net/http"
"net/url"
"path"
"path/filepath"
"regexp"
"strings"
Expand Down Expand Up @@ -203,14 +202,17 @@ func getGitHubClient(authenticatedClient *http.Client, config *Configuration) (*
if config.EnterpriseBaseURL == "" || config.EnterpriseUploadURL == "" {
return github.NewClient(authenticatedClient), nil
}
baseURL, err := url.JoinPath(config.EnterpriseBaseURL, "api", "v3")
if err != nil {
return nil, err
}

baseURL, _ := url.Parse(config.EnterpriseBaseURL)
baseURL.Path = path.Join(baseURL.Path, "api", "v3")

uploadURL, _ := url.Parse(config.EnterpriseUploadURL)
uploadURL.Path = path.Join(uploadURL.Path, "api", "v3")
uploadURL, err := url.JoinPath(config.EnterpriseUploadURL, "api", "v3")
if err != nil {
return nil, err
}

client, err := github.NewEnterpriseClient(baseURL.String(), uploadURL.String(), authenticatedClient)
client, err := github.NewEnterpriseClient(baseURL, uploadURL, authenticatedClient)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -244,12 +246,12 @@ func (p *Plugin) setDefaultConfiguration() error {
func (p *Plugin) OnActivate() error {
p.ensurePluginAPIClient()

siteURL := p.client.Configuration.GetConfig().ServiceSettings.SiteURL
if siteURL == nil || *siteURL == "" {
return errors.New("siteURL is not set. Please set it and restart the plugin")
_, err := getSiteURL(p.client)
if err != nil {
return err
}

err := p.setDefaultConfiguration()
err = p.setDefaultConfiguration()
if err != nil {
return errors.Wrap(err, "failed to set default configuration")
}
Expand Down Expand Up @@ -525,7 +527,7 @@ func (p *Plugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*mode
return post, ""
}

func (p *Plugin) getOAuthConfig(privateAllowed bool) *oauth2.Config {
func (p *Plugin) getOAuthConfig(privateAllowed bool) (*oauth2.Config, error) {
config := p.getConfiguration()

repo := github.ScopePublicRepo
Expand All @@ -545,45 +547,54 @@ func (p *Plugin) getOAuthConfig(privateAllowed bool) *oauth2.Config {
baseURL = testOAuthServerURL + "/"
}

authURL, _ := url.Parse(baseURL)
tokenURL, _ := url.Parse(baseURL)

authURL.Path = path.Join(authURL.Path, "login", "oauth", "authorize")
tokenURL.Path = path.Join(tokenURL.Path, "login", "oauth", "access_token")
authURL, err := url.JoinPath(baseURL, "login", "oauth", "authorize")
if err != nil {
return nil, errors.Wrap(err, "failed to build AuthURL")
}
tokenURL, err := url.JoinPath(baseURL, "login", "oauth", "access_token")
if err != nil {
return nil, errors.Wrap(err, "failed to build TokenURL")
}

return &oauth2.Config{
ClientID: config.GitHubOAuthClientID,
ClientSecret: config.GitHubOAuthClientSecret,
Scopes: scopes,
Endpoint: oauth2.Endpoint{
AuthURL: authURL.String(),
TokenURL: tokenURL.String(),
AuthURL: authURL,
TokenURL: tokenURL,
AuthStyle: oauth2.AuthStyleInHeader,
},
}
}, nil
}

func (p *Plugin) getOAuthConfigForChimeraApp(scopes []string) *oauth2.Config {
func (p *Plugin) getOAuthConfigForChimeraApp(scopes []string) (*oauth2.Config, error) {
baseURL := fmt.Sprintf("%s/v1/github/%s", p.chimeraURL, chimeraGitHubAppIdentifier)
authURL, _ := url.Parse(baseURL)
tokenURL, _ := url.Parse(baseURL)

authURL.Path = path.Join(authURL.Path, "oauth", "authorize")
tokenURL.Path = path.Join(tokenURL.Path, "oauth", "token")

redirectURL, _ := url.Parse(fmt.Sprintf("%s/plugins/github/oauth/complete", *p.client.Configuration.GetConfig().ServiceSettings.SiteURL))
authURL, err := url.JoinPath(baseURL, "oauth", "authorize")
if err != nil {
return nil, errors.Wrap(err, "failed to build AuthURL")
}
tokenURL, err := url.JoinPath(baseURL, "oauth", "token")
if err != nil {
return nil, errors.Wrap(err, "failed to build TokenURL")
}
redirectURL, err := buildPluginURL(p.client, "oauth", "token")
if err != nil {
return nil, errors.Wrap(err, "failed to build RedirectURL")
}

return &oauth2.Config{
ClientID: "placeholder",
ClientSecret: "placeholder",
Scopes: scopes,
RedirectURL: redirectURL.String(),
RedirectURL: redirectURL,
Endpoint: oauth2.Endpoint{
AuthURL: authURL.String(),
TokenURL: tokenURL.String(),
AuthURL: authURL,
TokenURL: tokenURL,
AuthStyle: oauth2.AuthStyleInHeader,
},
}
}, nil
}

type GitHubUserInfo struct {
Expand Down
25 changes: 25 additions & 0 deletions server/plugin/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import (
"strings"
"unicode"

"github.com/mattermost/mattermost/server/public/pluginapi"

"github.com/google/go-github/v54/github"
"github.com/pkg/errors"
)
Expand Down Expand Up @@ -370,6 +372,29 @@ func isValidURL(rawURL string) error {
return nil
}

func buildPluginURL(client *pluginapi.Client, elem ...string) (string, error) {
siteURL, err := getSiteURL(client)
if err != nil {
return "", err
}

redirectURL, err := url.JoinPath(siteURL, append([]string{"plugins", Manifest.Id}, elem...)...)
if err != nil {
return "", errors.Wrap(err, "failed to build pluginURL")
}

return redirectURL, nil
}

func getSiteURL(client *pluginapi.Client) (string, error) {
siteURL := client.Configuration.GetConfig().ServiceSettings.SiteURL
if siteURL == nil {
return "", errors.New("siteURL is not set. Please set it and restart the plugin")
}

return strings.TrimSuffix(*siteURL, "/"), nil
}

// lastN returns the last n characters of a string, with the rest replaced by *.
// At most 3 characters are replaced. The rest is cut off.
func lastN(s string, n int) string {
Expand Down
Loading