Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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 @@ -43,6 +43,7 @@ require (
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/russellhaering/goxmldsig v1.5.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/tinylib/msgp v1.4.0 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
Expand Down
52 changes: 26 additions & 26 deletions server/activate_hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,19 @@ func (p *Plugin) OnActivate() error {

teams, err := p.API.GetTeams()
if err != nil {
return errors.Wrap(err, "failed to query teams OnActivate")
}

for _, team := range teams {
_, ok := configuration.demoChannelIDs[team.Id]
if !ok {
p.API.LogWarn("No demo channel id for team", "team", team.Id)
continue
}

msg := fmt.Sprintf("OnActivate: %s", manifest.Id)
if err := p.postPluginMessage(team.Id, msg); err != nil {
return errors.Wrap(err, "failed to post OnActivate message")
p.API.LogWarn("Failed to query teams OnActivate, skipping activation messages", "error", err.Error())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It's strange that GetTeams fails. I wonder if we should still fail loudly as that makes things easier for QA to debug.

} else {
for _, team := range teams {
_, ok := configuration.demoChannelIDs[team.Id]
if !ok {
p.API.LogWarn("No demo channel id for team", "team", team.Id)
continue
}

msg := fmt.Sprintf("OnActivate: %s", manifest.Id)
if err := p.postPluginMessage(team.Id, msg); err != nil {
p.API.LogWarn("Failed to post OnActivate message", "error", err.Error())
}
}
}

Expand Down Expand Up @@ -83,19 +83,19 @@ func (p *Plugin) OnDeactivate() error {

teams, err := p.API.GetTeams()
if err != nil {
return errors.Wrap(err, "failed to query teams OnDeactivate")
}

for _, team := range teams {
_, ok := configuration.demoChannelIDs[team.Id]
if !ok {
p.API.LogWarn("No demo channel id for team", "team", team.Id)
continue
}

msg := fmt.Sprintf("OnDeactivate: %s", manifest.Id)
if err := p.postPluginMessage(team.Id, msg); err != nil {
return errors.Wrap(err, "failed to post OnDeactivate message")
p.API.LogWarn("Failed to query teams OnDeactivate, skipping deactivation messages", "error", err.Error())
} else {
for _, team := range teams {
_, ok := configuration.demoChannelIDs[team.Id]
if !ok {
p.API.LogWarn("No demo channel id for team", "team", team.Id)
continue
}

msg := fmt.Sprintf("OnDeactivate: %s", manifest.Id)
if err := p.postPluginMessage(team.Id, msg); err != nil {
p.API.LogWarn("Failed to post OnDeactivate message", "error", err.Error())
}
}
}

Expand Down
154 changes: 154 additions & 0 deletions server/channel_settings_api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package main

import (
"encoding/json"
"net/http"

"github.com/gorilla/mux"
"github.com/mattermost/mattermost/server/public/model"
)

// KV keys are kept well under the 50-char limit (channel IDs are 26 chars).
func csSchemaKey(channelID string) string { return "cs_schema_" + channelID }
func csCustomKey(channelID string) string { return "cs_custom_" + channelID }

// customState is the typed payload for the fully-custom channel settings tab.
type customState struct {
Note string `json:"note"`
PinGreeting bool `json:"pinGreeting"`
}

// requireUser enforces that the request is from an authenticated user. It
// returns false if it already wrote an error response.
func (p *Plugin) requireUser(w http.ResponseWriter, r *http.Request) bool {
if r.Header.Get("Mattermost-User-ID") == "" {
http.Error(w, "Not authorized", http.StatusUnauthorized)
return false
}
return true
}

// requireChannelReader enforces auth and read access to the channel for read
// paths. It returns the authenticated user ID, or false if it already wrote an
// error response.
func (p *Plugin) requireChannelReader(w http.ResponseWriter, r *http.Request, channelID string) (string, bool) {
if !p.requireUser(w, r) {
return "", false
}
userID := r.Header.Get("Mattermost-User-ID")

if !p.API.HasPermissionToChannel(userID, channelID, model.PermissionReadChannel) {
http.Error(w, "Forbidden", http.StatusForbidden)
return "", false
}

return userID, true
}

// requireChannelManager enforces auth and channel-properties permission for
// write paths. It returns the authenticated user ID, or false if it already
// wrote an error response.
func (p *Plugin) requireChannelManager(w http.ResponseWriter, r *http.Request, channelID string) (string, bool) {
if !p.requireUser(w, r) {
return "", false
}
userID := r.Header.Get("Mattermost-User-ID")

channel, appErr := p.API.GetChannel(channelID)
if appErr != nil {
p.API.LogError("Failed to get channel for channel settings", "channel_id", channelID, "err", appErr.Error())
http.Error(w, "Internal server error", http.StatusInternalServerError)
return "", false
}

permission := model.PermissionManagePublicChannelProperties
if channel.Type == model.ChannelTypePrivate {
permission = model.PermissionManagePrivateChannelProperties
}

if !p.API.HasPermissionToChannel(userID, channelID, permission) {
http.Error(w, "Forbidden", http.StatusForbidden)
return "", false
}

return userID, true
}

func (p *Plugin) handleGetChannelSettingsSchema(w http.ResponseWriter, r *http.Request) {
channelID := mux.Vars(r)["channel_id"]
if _, ok := p.requireChannelReader(w, r, channelID); !ok {
return
}

values := map[string]string{}
if err := p.client.KV.Get(csSchemaKey(channelID), &values); err != nil {
p.API.LogError("Failed to get channel settings schema", "channel_id", channelID, "err", err.Error())
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
Comment thread
nickmisasi marked this conversation as resolved.
if values == nil {
values = map[string]string{}
}

p.writeJSON(w, values)
}

func (p *Plugin) handleSaveChannelSettingsSchema(w http.ResponseWriter, r *http.Request) {
channelID := mux.Vars(r)["channel_id"]
if _, ok := p.requireChannelManager(w, r, channelID); !ok {
return
}

var values map[string]string
if err := json.NewDecoder(r.Body).Decode(&values); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
defer r.Body.Close()

if _, err := p.client.KV.Set(csSchemaKey(channelID), values); err != nil {
p.API.LogError("Failed to save channel settings schema", "channel_id", channelID, "err", err.Error())
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}

p.writeJSON(w, values)
}

func (p *Plugin) handleGetChannelSettingsCustom(w http.ResponseWriter, r *http.Request) {
channelID := mux.Vars(r)["channel_id"]
if _, ok := p.requireChannelReader(w, r, channelID); !ok {
return
}

var state customState
if err := p.client.KV.Get(csCustomKey(channelID), &state); err != nil {
p.API.LogError("Failed to get channel settings custom state", "channel_id", channelID, "err", err.Error())
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}

p.writeJSON(w, state)
}

func (p *Plugin) handleSaveChannelSettingsCustom(w http.ResponseWriter, r *http.Request) {
channelID := mux.Vars(r)["channel_id"]
if _, ok := p.requireChannelManager(w, r, channelID); !ok {
return
}

var state customState
if err := json.NewDecoder(r.Body).Decode(&state); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
defer r.Body.Close()

if _, err := p.client.KV.Set(csCustomKey(channelID), state); err != nil {
p.API.LogError("Failed to save channel settings custom state", "channel_id", channelID, "err", err.Error())
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}

p.writeJSON(w, state)
}
158 changes: 158 additions & 0 deletions server/channel_settings_api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package main

import (
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin/plugintest"
"github.com/mattermost/mattermost/server/public/pluginapi"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)

const testChannelID = "channelid1234567890123456a"

func newChannelSettingsPlugin(api *plugintest.API) *Plugin {
p := &Plugin{}
p.SetAPI(api)
p.client = pluginapi.NewClient(api, nil)
p.initializeAPI()
return p
}

func TestChannelSettingsSchemaRoundTrip(t *testing.T) {
api := &plugintest.API{}
defer api.AssertExpectations(t)

store := map[string][]byte{}
api.On("KVSetWithOptions", "cs_schema_"+testChannelID, mock.Anything, mock.Anything).Return(true, nil).Run(func(args mock.Arguments) {
store[args.String(0)] = args.Get(1).([]byte)
})
api.On("KVGet", "cs_schema_"+testChannelID).Return(func(key string) []byte { return store[key] }, nil)
api.On("GetChannel", testChannelID).Return(&model.Channel{Id: testChannelID, Type: model.ChannelTypeOpen}, nil)
api.On("HasPermissionToChannel", "user1", testChannelID, model.PermissionManagePublicChannelProperties).Return(true)
api.On("HasPermissionToChannel", "user1", testChannelID, model.PermissionReadChannel).Return(true)

p := newChannelSettingsPlugin(api)

w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodPost, "/channel_settings/"+testChannelID+"/schema", strings.NewReader(`{"postPrefixStyle":"bold"}`))
r.Header.Set("Mattermost-User-ID", "user1")
p.ServeHTTP(nil, w, r)
require.Equal(t, http.StatusOK, w.Result().StatusCode)

w = httptest.NewRecorder()
r = httptest.NewRequest(http.MethodGet, "/channel_settings/"+testChannelID+"/schema", nil)
r.Header.Set("Mattermost-User-ID", "user1")
p.ServeHTTP(nil, w, r)
require.Equal(t, http.StatusOK, w.Result().StatusCode)
require.JSONEq(t, `{"postPrefixStyle":"bold"}`, w.Body.String())
}

func TestChannelSettingsCustomRoundTrip(t *testing.T) {
api := &plugintest.API{}
defer api.AssertExpectations(t)

store := map[string][]byte{}
api.On("KVSetWithOptions", "cs_custom_"+testChannelID, mock.Anything, mock.Anything).Return(true, nil).Run(func(args mock.Arguments) {
store[args.String(0)] = args.Get(1).([]byte)
})
api.On("KVGet", "cs_custom_"+testChannelID).Return(func(key string) []byte { return store[key] }, nil)
api.On("GetChannel", testChannelID).Return(&model.Channel{Id: testChannelID, Type: model.ChannelTypeOpen}, nil)
api.On("HasPermissionToChannel", "user1", testChannelID, model.PermissionManagePublicChannelProperties).Return(true)
api.On("HasPermissionToChannel", "user1", testChannelID, model.PermissionReadChannel).Return(true)

p := newChannelSettingsPlugin(api)

// GET before any save returns the zero-valued custom state.
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/channel_settings/"+testChannelID+"/custom", nil)
r.Header.Set("Mattermost-User-ID", "user1")
p.ServeHTTP(nil, w, r)
require.Equal(t, http.StatusOK, w.Result().StatusCode)
require.JSONEq(t, `{"note":"","pinGreeting":false}`, w.Body.String())

w = httptest.NewRecorder()
r = httptest.NewRequest(http.MethodPost, "/channel_settings/"+testChannelID+"/custom", strings.NewReader(`{"note":"hello","pinGreeting":true}`))
r.Header.Set("Mattermost-User-ID", "user1")
p.ServeHTTP(nil, w, r)
require.Equal(t, http.StatusOK, w.Result().StatusCode)

w = httptest.NewRecorder()
r = httptest.NewRequest(http.MethodGet, "/channel_settings/"+testChannelID+"/custom", nil)
r.Header.Set("Mattermost-User-ID", "user1")
p.ServeHTTP(nil, w, r)
require.Equal(t, http.StatusOK, w.Result().StatusCode)
require.JSONEq(t, `{"note":"hello","pinGreeting":true}`, w.Body.String())
}

func TestChannelSettingsReadAuthorization(t *testing.T) {
t.Run("get without auth is unauthorized", func(t *testing.T) {
api := &plugintest.API{}
p := newChannelSettingsPlugin(api)

w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/channel_settings/"+testChannelID+"/schema", nil)
p.ServeHTTP(nil, w, r)
require.Equal(t, http.StatusUnauthorized, w.Result().StatusCode)
})

t.Run("get without read permission is forbidden", func(t *testing.T) {
api := &plugintest.API{}
defer api.AssertExpectations(t)
api.On("HasPermissionToChannel", "user1", testChannelID, model.PermissionReadChannel).Return(false)

p := newChannelSettingsPlugin(api)

w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/channel_settings/"+testChannelID+"/custom", nil)
r.Header.Set("Mattermost-User-ID", "user1")
p.ServeHTTP(nil, w, r)
require.Equal(t, http.StatusForbidden, w.Result().StatusCode)
})
}

func TestChannelSettingsMalformedBody(t *testing.T) {
api := &plugintest.API{}
defer api.AssertExpectations(t)
api.On("GetChannel", testChannelID).Return(&model.Channel{Id: testChannelID, Type: model.ChannelTypeOpen}, nil)
api.On("HasPermissionToChannel", "user1", testChannelID, model.PermissionManagePublicChannelProperties).Return(true)

p := newChannelSettingsPlugin(api)

w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodPost, "/channel_settings/"+testChannelID+"/custom", strings.NewReader(`{not json`))
r.Header.Set("Mattermost-User-ID", "user1")
p.ServeHTTP(nil, w, r)
require.Equal(t, http.StatusBadRequest, w.Result().StatusCode)
}

func TestChannelSettingsAuthAndPermission(t *testing.T) {
t.Run("save without auth is unauthorized", func(t *testing.T) {
api := &plugintest.API{}
p := newChannelSettingsPlugin(api)

w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodPost, "/channel_settings/"+testChannelID+"/schema", strings.NewReader(`{}`))
p.ServeHTTP(nil, w, r)
require.Equal(t, http.StatusUnauthorized, w.Result().StatusCode)
})

t.Run("save without permission is forbidden", func(t *testing.T) {
api := &plugintest.API{}
defer api.AssertExpectations(t)
api.On("GetChannel", testChannelID).Return(&model.Channel{Id: testChannelID, Type: model.ChannelTypePrivate}, nil)
api.On("HasPermissionToChannel", "user1", testChannelID, model.PermissionManagePrivateChannelProperties).Return(false)

p := newChannelSettingsPlugin(api)

w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodPost, "/channel_settings/"+testChannelID+"/custom", strings.NewReader(`{"note":"x"}`))
r.Header.Set("Mattermost-User-ID", "user1")
p.ServeHTTP(nil, w, r)
require.Equal(t, http.StatusForbidden, w.Result().StatusCode)
})
}
Loading