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-53425]: Added additional checks for POST type APIs #209

Merged
merged 7 commits into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/mattermost/mattermost-plugin-todo
go 1.13

require (
github.com/gorilla/mux v1.8.0
github.com/mattermost/mattermost-plugin-api v0.0.11
github.com/mattermost/mattermost-server/v5 v5.3.2-0.20200804063212-d4dac31b042a
github.com/pkg/errors v0.9.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,8 @@ github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51
github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
Expand Down
169 changes: 102 additions & 67 deletions server/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import (
"encoding/json"
"fmt"
"net/http"
"runtime/debug"
"strconv"
"sync"
"time"

"github.com/gorilla/mux"
"github.com/mattermost/mattermost-plugin-api/experimental/telemetry"
"github.com/mattermost/mattermost-server/v5/model"
"github.com/mattermost/mattermost-server/v5/plugin"
Expand Down Expand Up @@ -57,6 +59,8 @@ type Plugin struct {
// configurationLock synchronizes access to the configuration.
configurationLock sync.RWMutex

router *mux.Router

// configuration is the active plugin configuration. Consult getConfiguration and
// setConfiguration for usage.
configuration *configuration
Expand Down Expand Up @@ -85,6 +89,8 @@ func (p *Plugin) OnActivate() error {

p.listManager = NewListManager(p.API)

p.initializeAPI()

p.telemetryClient, err = telemetry.NewRudderClient()
if err != nil {
p.API.LogWarn("telemetry client not started", "error", err.Error())
Expand All @@ -104,31 +110,56 @@ func (p *Plugin) OnDeactivate() error {
return nil
}

func (p *Plugin) initializeAPI() {
p.router = mux.NewRouter()
p.router.Use(p.withRecovery)

p.router.HandleFunc("/add", p.checkAuth(p.handleAdd)).Methods(http.MethodPost)
p.router.HandleFunc("/list", p.checkAuth(p.handleList)).Methods(http.MethodGet)
p.router.HandleFunc("/remove", p.checkAuth(p.handleRemove)).Methods(http.MethodPost)
p.router.HandleFunc("/complete", p.checkAuth(p.handleComplete)).Methods(http.MethodPost)
p.router.HandleFunc("/accept", p.checkAuth(p.handleAccept)).Methods(http.MethodPost)
p.router.HandleFunc("/bump", p.checkAuth(p.handleBump)).Methods(http.MethodPost)
p.router.HandleFunc("/telemetry", p.checkAuth(p.handleTelemetry)).Methods(http.MethodPost)
p.router.HandleFunc("/config", p.checkAuth(p.handleConfig)).Methods(http.MethodGet)
p.router.HandleFunc("/edit", p.checkAuth(p.handleEdit)).Methods(http.MethodPut)
p.router.HandleFunc("/change_assignment", p.checkAuth(p.handleChangeAssignment)).Methods(http.MethodPost)

// 404 handler
p.router.Handle("{anything:.*}", http.NotFoundHandler())
}

// ServeHTTP demonstrates a plugin that handles HTTP requests by greeting the world.
func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/add":
p.handleAdd(w, r)
case "/list":
p.handleList(w, r)
case "/remove":
p.handleRemove(w, r)
case "/complete":
p.handleComplete(w, r)
case "/accept":
p.handleAccept(w, r)
case "/bump":
p.handleBump(w, r)
case "/telemetry":
p.handleTelemetry(w, r)
case "/config":
p.handleConfig(w, r)
case "/edit":
p.handleEdit(w, r)
case "/change_assignment":
p.handleChangeAssignment(w, r)
default:
http.NotFound(w, r)
w.Header().Set("Content-Type", "application/json")

p.router.ServeHTTP(w, r)
}

func (p *Plugin) withRecovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if x := recover(); x != nil {
p.API.LogWarn("Recovered from a panic",
"url", r.URL.String(),
"error", x,
"stack", string(debug.Stack()))
}
}()

next.ServeHTTP(w, r)
})
}

func (p *Plugin) checkAuth(handler http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
if userID == "" {
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}

handler(w, r)
}
}

Expand All @@ -139,10 +170,6 @@ type telemetryAPIRequest struct {

func (p *Plugin) handleTelemetry(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
if userID == "" {
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}

var telemetryRequest *telemetryAPIRequest
decoder := json.NewDecoder(r.Body)
Expand All @@ -153,6 +180,12 @@ func (p *Plugin) handleTelemetry(w http.ResponseWriter, r *http.Request) {
return
}

if telemetryRequest == nil {
Kshitij-Katiyar marked this conversation as resolved.
Show resolved Hide resolved
Kshitij-Katiyar marked this conversation as resolved.
Show resolved Hide resolved
p.API.LogError("Invalid request body")
p.handleErrorWithCode(w, http.StatusBadRequest, "Unable to decode JSON", errors.New("invalid request body"))
return
}

if telemetryRequest.Event != "" {
p.trackFrontend(userID, telemetryRequest.Event, telemetryRequest.Properties)
}
Expand All @@ -167,10 +200,6 @@ type addAPIRequest struct {

func (p *Plugin) handleAdd(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
if userID == "" {
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}

var addRequest *addAPIRequest
decoder := json.NewDecoder(r.Body)
Expand All @@ -183,6 +212,12 @@ func (p *Plugin) handleAdd(w http.ResponseWriter, r *http.Request) {

senderName := p.listManager.GetUserName(userID)

if addRequest == nil {
p.API.LogError("Invalid request body")
p.handleErrorWithCode(w, http.StatusBadRequest, "Unable to decode JSON", errors.New("invalid request body"))
return
}

if addRequest.SendTo == "" {
_, err = p.listManager.AddIssue(userID, addRequest.Message, addRequest.Description, addRequest.PostID)
if err != nil {
Expand Down Expand Up @@ -266,10 +301,6 @@ func (p *Plugin) postReplyIfNeeded(postID, message, todo string) {

func (p *Plugin) handleList(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
if userID == "" {
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}

listInput := r.URL.Query().Get("list")
listID := MyListKey
Expand Down Expand Up @@ -335,10 +366,6 @@ type editAPIRequest struct {

func (p *Plugin) handleEdit(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
if userID == "" {
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}

var editRequest *editAPIRequest
decoder := json.NewDecoder(r.Body)
Expand All @@ -347,7 +374,12 @@ func (p *Plugin) handleEdit(w http.ResponseWriter, r *http.Request) {
p.handleErrorWithCode(w, http.StatusBadRequest, "Unable to decode JSON", err)
return
}
r.Body.Close()

if editRequest == nil {
p.API.LogError("Invalid request body")
p.handleErrorWithCode(w, http.StatusBadRequest, "Unable to decode JSON", errors.New("invalid request body"))
return
}

foreignUserID, list, oldMessage, err := p.listManager.EditIssue(userID, editRequest.ID, editRequest.Message, editRequest.Description)
if err != nil {
Expand Down Expand Up @@ -381,10 +413,6 @@ type changeAssignmentAPIRequest struct {

func (p *Plugin) handleChangeAssignment(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
if userID == "" {
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}

var changeRequest *changeAssignmentAPIRequest
decoder := json.NewDecoder(r.Body)
Expand All @@ -393,7 +421,12 @@ func (p *Plugin) handleChangeAssignment(w http.ResponseWriter, r *http.Request)
p.handleErrorWithCode(w, http.StatusBadRequest, "Unable to decode JSON", err)
return
}
r.Body.Close()

if changeRequest == nil {
p.API.LogError("Invalid request body")
p.handleErrorWithCode(w, http.StatusBadRequest, "Unable to decode JSON", errors.New("invalid request body"))
return
}

if changeRequest.SendTo == "" {
http.Error(w, "No user specified", http.StatusBadRequest)
Expand Down Expand Up @@ -437,10 +470,6 @@ type acceptAPIRequest struct {

func (p *Plugin) handleAccept(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
if userID == "" {
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}

var acceptRequest *acceptAPIRequest
decoder := json.NewDecoder(r.Body)
Expand All @@ -450,6 +479,12 @@ func (p *Plugin) handleAccept(w http.ResponseWriter, r *http.Request) {
return
}

if acceptRequest == nil {
p.API.LogError("Invalid request body")
p.handleErrorWithCode(w, http.StatusBadRequest, "Unable to decode JSON", errors.New("invalid request body"))
return
}

todoMessage, sender, err := p.listManager.AcceptIssue(userID, acceptRequest.ID)
if err != nil {
p.API.LogError("Unable to accept issue err=" + err.Error())
Expand All @@ -473,10 +508,6 @@ type completeAPIRequest struct {

func (p *Plugin) handleComplete(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
if userID == "" {
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}

var completeRequest *completeAPIRequest
decoder := json.NewDecoder(r.Body)
Expand All @@ -486,6 +517,12 @@ func (p *Plugin) handleComplete(w http.ResponseWriter, r *http.Request) {
return
}

if completeRequest == nil {
p.API.LogError("Invalid request body")
p.handleErrorWithCode(w, http.StatusBadRequest, "Unable to decode JSON", errors.New("invalid request body"))
return
}

issue, foreignID, listToUpdate, err := p.listManager.CompleteIssue(userID, completeRequest.ID)
if err != nil {
p.API.LogError("Unable to complete issue err=" + err.Error())
Expand Down Expand Up @@ -517,10 +554,6 @@ type removeAPIRequest struct {

func (p *Plugin) handleRemove(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
if userID == "" {
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}

var removeRequest *removeAPIRequest
decoder := json.NewDecoder(r.Body)
Expand All @@ -531,6 +564,12 @@ func (p *Plugin) handleRemove(w http.ResponseWriter, r *http.Request) {
return
}

if removeRequest == nil {
p.API.LogError("Invalid request body")
p.handleErrorWithCode(w, http.StatusBadRequest, "Unable to decode JSON", errors.New("invalid request body"))
return
}

issue, foreignID, isSender, listToUpdate, err := p.listManager.RemoveIssue(userID, removeRequest.ID)
if err != nil {
p.API.LogError("Unable to remove issue, err=" + err.Error())
Expand Down Expand Up @@ -568,10 +607,6 @@ type bumpAPIRequest struct {

func (p *Plugin) handleBump(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
if userID == "" {
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}

var bumpRequest *bumpAPIRequest
decoder := json.NewDecoder(r.Body)
Expand All @@ -582,6 +617,12 @@ func (p *Plugin) handleBump(w http.ResponseWriter, r *http.Request) {
return
}

if bumpRequest == nil {
p.API.LogError("Invalid request body")
p.handleErrorWithCode(w, http.StatusBadRequest, "Unable to decode JSON", errors.New("invalid request body"))
return
}

todoMessage, foreignUser, foreignIssueID, err := p.listManager.BumpIssue(userID, bumpRequest.ID)
if err != nil {
p.API.LogError("Unable to bump issue, err=" + err.Error())
Expand All @@ -604,12 +645,6 @@ func (p *Plugin) handleBump(w http.ResponseWriter, r *http.Request) {

// API endpoint to retrieve plugin configurations
func (p *Plugin) handleConfig(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
if userID == "" {
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}

if r.Method != http.MethodGet {
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
return
Expand Down
2 changes: 1 addition & 1 deletion webapp/src/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export const add = (message, description, sendTo, postID) => async (dispatch, ge

export const editIssue = (postID, message, description) => async (dispatch, getState) => {
await fetch(getPluginServerRoute(getState()) + '/edit', Client4.getOptions({
method: 'post',
method: 'put',
Kshitij-Katiyar marked this conversation as resolved.
Show resolved Hide resolved
body: JSON.stringify({id: postID, message, description}),
}));
};
Expand Down