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-51253][MM-51275]: Added check for full name and refactored error logs. #148

Merged
merged 11 commits into from
Mar 28, 2023
15 changes: 10 additions & 5 deletions server/plugin/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ func (p *Plugin) InitRoutes() {
s := p.router.PathPrefix(constants.APIPrefix).Subrouter()

// OAuth
s.HandleFunc(constants.PathOAuthConnect, p.OAuthConnect).Methods(http.MethodGet)
s.HandleFunc(constants.PathOAuthCallback, p.OAuthComplete).Methods(http.MethodGet)
s.HandleFunc(constants.PathOAuthConnect, p.handleAuthRequired(p.OAuthConnect)).Methods(http.MethodGet)
s.HandleFunc(constants.PathOAuthCallback, p.handleAuthRequired(p.OAuthComplete)).Methods(http.MethodGet)
// Plugin APIs
s.HandleFunc(constants.PathCreateTasks, p.handleAuthRequired(p.checkOAuth(p.handleCreateTask))).Methods(http.MethodPost)
s.HandleFunc(constants.PathLinkProject, p.handleAuthRequired(p.checkOAuth(p.handleLink))).Methods(http.MethodPost)
Expand Down Expand Up @@ -339,10 +339,15 @@ func (p *Plugin) handleCreateSubscription(w http.ResponseWriter, r *http.Request
return
}

createdByDisplayName := fmt.Sprintf("%s %s", user.FirstName, user.LastName)
if len(strings.TrimSpace(createdByDisplayName)) == 0 {
createdByDisplayName = user.Username // If user's first/last name doesn't exist then show username as fallback
createdByDisplayName := user.Username

showFullName := p.API.GetConfig().PrivacySettings.ShowFullName
// If "PrivacySettings.ShowFullName" is true then show the user's first/last name
// If the user's first/last name doesn't exist then show the username as fallback
if showFullName != nil && *showFullName && (user.FirstName != "" || user.LastName != "") {
createdByDisplayName = fmt.Sprintf("%s %s", user.FirstName, user.LastName)
}

if storeErr := p.Store.StoreSubscription(&serializers.SubscriptionDetails{
MattermostUserID: mattermostUserID,
ProjectName: body.Project,
Expand Down
4 changes: 4 additions & 0 deletions server/plugin/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,10 @@ func TestHandleCreateSubscriptions(t *testing.T) {
FirstName: "mockCreatedBy",
}, nil)

showFullName := true
privacySettings := model.PrivacySettings{ShowFullName: &showFullName}
mockAPI.On("GetConfig", mock.AnythingOfType("string")).Return(&model.Config{PrivacySettings: privacySettings}, nil)

monkey.Patch(json.Marshal, func(interface{}) ([]byte, error) {
return []byte{}, testCase.marshalError
})
Expand Down
17 changes: 7 additions & 10 deletions server/plugin/oAuth.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,6 @@ func (p *Plugin) GenerateOAuthConnectURL(mattermostUserID string) string {
// OAuthConnect redirects to the OAuth authorization URL
func (p *Plugin) OAuthConnect(w http.ResponseWriter, r *http.Request) {
mattermostUserID := r.Header.Get(constants.HeaderMattermostUserID)
// TODO: use checkAuth middleware for this
if mattermostUserID == "" {
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}

if isConnected := p.MattermostUserAlreadyConnected(mattermostUserID); isConnected {
p.CloseBrowserWindowWithHTTPResponse(w)
Expand Down Expand Up @@ -107,7 +102,8 @@ func (p *Plugin) OAuthComplete(w http.ResponseWriter, r *http.Request) {
return
}

if err := p.GenerateOAuthToken(code, state); err != nil {
mattermostUserID := r.Header.Get(constants.HeaderMattermostUserID)
if err := p.GenerateOAuthToken(code, state, mattermostUserID); err != nil {
if strings.Contains(err.Error(), "already connected") {
p.API.LogError(constants.UnableToCompleteOAuth, "Error", constants.ErrorMessageAzureDevopsAccountAlreadyConnected)
http.Error(w, err.Error(), http.StatusForbidden)
Expand All @@ -122,13 +118,14 @@ func (p *Plugin) OAuthComplete(w http.ResponseWriter, r *http.Request) {
}

// GenerateOAuthToken generates OAuth token after successful authorization
func (p *Plugin) GenerateOAuthToken(code, state string) error {
func (p *Plugin) GenerateOAuthToken(code, state, authenticatedMattermostUserID string) error {
mattermostUserID := strings.Split(state, "_")[1]

if mattermostUserID != authenticatedMattermostUserID {
return errors.New("failed to complete oAuth, mattermost user is not authenticated")
}

if err := p.Store.VerifyOAuthState(mattermostUserID, state); err != nil {
if _, DMErr := p.DM(mattermostUserID, constants.GenericErrorMessage, false); DMErr != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also make sure every endpoint intended to be used by a MM user is checked for authentication?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we have a check for authentication on all the plugin APIs where we are checking for MM user-id in the header.
Also, OAuthComplete API is used as a redirection URL by the Azure DevOps oAuth app which sends the state query param that we are validating here as authentication.

Copy link
Contributor

@mickmister mickmister Mar 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, OAuthComplete API is used as a redirection URL by the Azure DevOps oAuth app which sends the state query param that we are validating here as authentication.

The redirect is in the browser only though. Any request hitting MM in the process will be authenticated with a MM user token (and we should verify its presence). The Azure server never sends a request to the MM server in this process.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also check that the authenticated user's id matches the user id on line 126

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made the changes

return DMErr
}
return errors.Wrap(err, "failed to verify oAuth state")
}

Expand Down
46 changes: 20 additions & 26 deletions server/plugin/oAuth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package plugin
import (
"bytes"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
Expand Down Expand Up @@ -31,33 +32,25 @@ func TestOAuthConnect(t *testing.T) {
mockAPI := &plugintest.API{}
p.API = mockAPI
for _, testCase := range []struct {
description string
isConnected bool
mattermostUserID string
DMErr error
statusCode int
description string
isConnected bool
DMErr error
statusCode int
}{
{
description: "OAuthConnect: valid",
mattermostUserID: testutils.MockMattermostUserID,
statusCode: http.StatusFound,
description: "OAuthConnect: valid",
statusCode: http.StatusFound,
},
{
description: "OAuthConnect: without mattermostUserID",
statusCode: http.StatusUnauthorized,
},
{
description: "OAuthConnect: user already connected",
isConnected: true,
mattermostUserID: testutils.MockMattermostUserID,
statusCode: http.StatusBadRequest,
description: "OAuthConnect: user already connected",
isConnected: true,
statusCode: http.StatusBadRequest,
},
{
description: "OAuthConnect: user already connected and failed to DM",
isConnected: true,
DMErr: &model.AppError{},
mattermostUserID: testutils.MockMattermostUserID,
statusCode: http.StatusInternalServerError,
description: "OAuthConnect: user already connected and failed to DM",
isConnected: true,
DMErr: &model.AppError{},
statusCode: http.StatusInternalServerError,
},
} {
t.Run(testCase.description, func(t *testing.T) {
Expand All @@ -73,7 +66,6 @@ func TestOAuthConnect(t *testing.T) {
})

req := httptest.NewRequest(http.MethodGet, "/oauth/connect", bytes.NewBufferString(`{}`))
req.Header.Add(constants.HeaderMattermostUserID, testCase.mattermostUserID)

res := httptest.NewRecorder()

Expand Down Expand Up @@ -139,7 +131,7 @@ func TestOAuthComplete(t *testing.T) {
},
} {
t.Run(testCase.description, func(t *testing.T) {
monkey.PatchInstanceMethod(reflect.TypeOf(&p), "GenerateOAuthToken", func(_ *Plugin, _, _ string) error {
monkey.PatchInstanceMethod(reflect.TypeOf(&p), "GenerateOAuthToken", func(_ *Plugin, _, _, _ string) error {
return testCase.oAuthTokenErr
})
monkey.PatchInstanceMethod(reflect.TypeOf(&p), "CloseBrowserWindowWithHTTPResponse", func(_ *Plugin, _ http.ResponseWriter) {})
Expand Down Expand Up @@ -170,14 +162,16 @@ func TestGenerateOAuthToken(t *testing.T) {
description string
code string
state string
mmuserID string
verifyOAuthError error
expectedError string
DMError error
}{
{
description: "GenerateOAuthToken: valid",
code: "mockCode",
state: "mock_state",
state: fmt.Sprintf("mockState_%s", testutils.MockMattermostUserID),
mmuserID: testutils.MockMattermostUserID,
},
} {
t.Run(testCase.description, func(t *testing.T) {
Expand All @@ -191,10 +185,10 @@ func TestGenerateOAuthToken(t *testing.T) {
})

if testCase.expectedError == "" {
mockedStore.EXPECT().VerifyOAuthState("state", testCase.state).Return(testCase.verifyOAuthError)
mockedStore.EXPECT().VerifyOAuthState(testCase.mmuserID, testCase.state).Return(testCase.verifyOAuthError)
}

err := p.GenerateOAuthToken(testCase.code, testCase.state)
err := p.GenerateOAuthToken(testCase.code, testCase.state, testCase.mmuserID)
if testCase.expectedError != "" {
assert.NotNil(t, err)
return
Expand Down