From fa807d8e436e87b8c1749ea54c293a15c67f7f29 Mon Sep 17 00:00:00 2001 From: Christopher Speller Date: Wed, 27 Apr 2016 16:02:58 -0400 Subject: Fixing permalinks to channels your not a memeber of (#2805) --- api/api.go | 6 ++- api/channel.go | 65 +++++++++++++++++++++---------- api/channel_test.go | 32 ++++++++++++++- api/command_join.go | 2 +- api/post.go | 48 +++++++++++++++++++++++ model/client.go | 13 +++++++ webapp/action_creators/global_actions.jsx | 40 +++++++++++++------ webapp/client/client.jsx | 26 +++++++++++++ webapp/components/more_channels.jsx | 5 ++- webapp/i18n/en.json | 3 +- webapp/root.jsx | 26 +++++++------ webapp/stores/channel_store.jsx | 9 +++++ webapp/tests/client_channel.test.jsx | 26 ++++++++++++- 13 files changed, 249 insertions(+), 52 deletions(-) diff --git a/api/api.go b/api/api.go index e9a95b125..fc81dda3a 100644 --- a/api/api.go +++ b/api/api.go @@ -24,8 +24,9 @@ type Routes struct { Teams *mux.Router // 'api/v3/teams' NeedTeam *mux.Router // 'api/v3/teams/{team_id:[A-Za-z0-9]+}' - Channels *mux.Router // 'api/v3/teams/{team_id:[A-Za-z0-9]+}/channels' - NeedChannel *mux.Router // 'api/v3/teams/{team_id:[A-Za-z0-9]+}/channels/{channel_id:[A-Za-z0-9]+}' + Channels *mux.Router // 'api/v3/teams/{team_id:[A-Za-z0-9]+}/channels' + NeedChannel *mux.Router // 'api/v3/teams/{team_id:[A-Za-z0-9]+}/channels/{channel_id:[A-Za-z0-9]+}' + NeedChannelName *mux.Router // 'api/v3/teams/{team_id:[A-Za-z0-9]+}/channels/name/{channel_name:[A-Za-z0-9-]+}' Posts *mux.Router // 'api/v3/teams/{team_id:[A-Za-z0-9]+}/channels/{channel_id:[A-Za-z0-9]+}/posts' NeedPost *mux.Router // 'api/v3/teams/{team_id:[A-Za-z0-9]+}/channels/{channel_id:[A-Za-z0-9]+}/posts/{post_id:[A-Za-z0-9]+}' @@ -56,6 +57,7 @@ func InitApi() { BaseRoutes.NeedTeam = BaseRoutes.Teams.PathPrefix("/{team_id:[A-Za-z0-9]+}").Subrouter() BaseRoutes.Channels = BaseRoutes.NeedTeam.PathPrefix("/channels").Subrouter() BaseRoutes.NeedChannel = BaseRoutes.Channels.PathPrefix("/{channel_id:[A-Za-z0-9]+}").Subrouter() + BaseRoutes.NeedChannelName = BaseRoutes.Channels.PathPrefix("/name/{channel_name:[A-Za-z0-9-]+}").Subrouter() BaseRoutes.Posts = BaseRoutes.NeedChannel.PathPrefix("/posts").Subrouter() BaseRoutes.NeedPost = BaseRoutes.Posts.PathPrefix("/{post_id:[A-Za-z0-9]+}").Subrouter() BaseRoutes.Commands = BaseRoutes.NeedTeam.PathPrefix("/commands").Subrouter() diff --git a/api/channel.go b/api/channel.go index 871477824..d47109045 100644 --- a/api/channel.go +++ b/api/channel.go @@ -8,6 +8,7 @@ import ( l4g "github.com/alecthomas/log4go" "github.com/gorilla/mux" "github.com/mattermost/platform/model" + "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" "net/http" "strconv" @@ -31,6 +32,8 @@ func InitChannel() { BaseRoutes.Channels.Handle("/update_purpose", ApiUserRequired(updateChannelPurpose)).Methods("POST") BaseRoutes.Channels.Handle("/update_notify_props", ApiUserRequired(updateNotifyProps)).Methods("POST") + BaseRoutes.NeedChannelName.Handle("/join", ApiUserRequired(join)).Methods("POST") + BaseRoutes.NeedChannel.Handle("/", ApiUserRequiredActivity(getChannel, false)).Methods("GET") BaseRoutes.NeedChannel.Handle("/extra_info", ApiUserRequired(getChannelExtraInfo)).Methods("GET") BaseRoutes.NeedChannel.Handle("/extra_info/{member_limit:-?[0-9]+}", ApiUserRequired(getChannelExtraInfo)).Methods("GET") @@ -423,48 +426,68 @@ func join(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) channelId := params["channel_id"] + channelName := params["channel_name"] - JoinChannel(c, channelId, "") - - if c.Err != nil { + var outChannel *model.Channel = nil + if channelId != "" { + if err, channel := JoinChannelById(c, c.Session.UserId, channelId); err != nil { + c.Err = err + c.Err.StatusCode = http.StatusForbidden + return + } else { + outChannel = channel + } + } else if channelName != "" { + if err, channel := JoinChannelByName(c, c.Session.UserId, c.TeamId, channelName); err != nil { + c.Err = err + c.Err.StatusCode = http.StatusForbidden + return + } else { + outChannel = channel + } + } else { + c.SetInvalidParam("join", "channel_id, channel_name") return } + w.Write([]byte(outChannel.ToJson())) +} - result := make(map[string]string) - result["id"] = channelId - w.Write([]byte(model.MapToJson(result))) +func JoinChannelByName(c *Context, userId string, teamId string, channelName string) (*model.AppError, *model.Channel) { + channelChannel := Srv.Store.Channel().GetByName(teamId, channelName) + userChannel := Srv.Store.User().Get(userId) + + return joinChannel(c, channelChannel, userChannel) } -func JoinChannel(c *Context, channelId string, role string) { +func JoinChannelById(c *Context, userId string, channelId string) (*model.AppError, *model.Channel) { + channelChannel := Srv.Store.Channel().Get(channelId) + userChannel := Srv.Store.User().Get(userId) - sc := Srv.Store.Channel().Get(channelId) - uc := Srv.Store.User().Get(c.Session.UserId) + return joinChannel(c, channelChannel, userChannel) +} - if cresult := <-sc; cresult.Err != nil { - c.Err = cresult.Err - return - } else if uresult := <-uc; uresult.Err != nil { - c.Err = uresult.Err - return +func joinChannel(c *Context, channelChannel store.StoreChannel, userChannel store.StoreChannel) (*model.AppError, *model.Channel) { + if cresult := <-channelChannel; cresult.Err != nil { + return cresult.Err, nil + } else if uresult := <-userChannel; uresult.Err != nil { + return uresult.Err, nil } else { channel := cresult.Data.(*model.Channel) user := uresult.Data.(*model.User) if !c.HasPermissionsToTeam(channel.TeamId, "join") { - return + return c.Err, nil } if channel.Type == model.CHANNEL_OPEN { if _, err := AddUserToChannel(user, channel); err != nil { - c.Err = err - return + return err, nil } PostUserAddRemoveMessageAndForget(c, channel.Id, fmt.Sprintf(utils.T("api.channel.join_channel.post_and_forget"), user.Username)) } else { - c.Err = model.NewLocAppError("join", "api.channel.join_channel.permissions.app_error", nil, "") - c.Err.StatusCode = http.StatusForbidden - return + return model.NewLocAppError("join", "api.channel.join_channel.permissions.app_error", nil, ""), nil } + return nil, channel } } diff --git a/api/channel_test.go b/api/channel_test.go index 23dd77698..8ac785f77 100644 --- a/api/channel_test.go +++ b/api/channel_test.go @@ -395,7 +395,7 @@ func TestGetChannelCounts(t *testing.T) { } -func TestJoinChannel(t *testing.T) { +func TestJoinChannelById(t *testing.T) { th := Setup().InitBasic() Client := th.BasicClient team := th.BasicTeam @@ -425,6 +425,36 @@ func TestJoinChannel(t *testing.T) { } } +func TestJoinChannelByName(t *testing.T) { + th := Setup().InitBasic() + Client := th.BasicClient + team := th.BasicTeam + + channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + channel3 := &model.Channel{DisplayName: "B Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id} + channel3 = Client.Must(Client.CreateChannel(channel3)).Data.(*model.Channel) + + th.LoginBasic2() + + Client.Must(Client.JoinChannelByName(channel1.Name)) + + if _, err := Client.JoinChannelByName(channel3.Name); err == nil { + t.Fatal("shouldn't be able to join secret group") + } + + rchannel := Client.Must(Client.CreateDirectChannel(th.BasicUser.Id)).Data.(*model.Channel) + + user3 := th.CreateUser(th.BasicClient) + LinkUserToTeam(user3, team) + Client.LoginByEmail(team.Name, user3.Email, "pwd") + + if _, err := Client.JoinChannelByName(rchannel.Name); err == nil { + t.Fatal("shoudn't be able to join direct channel") + } +} + func TestLeaveChannel(t *testing.T) { th := Setup().InitBasic() Client := th.BasicClient diff --git a/api/command_join.go b/api/command_join.go index f59925c06..af4443306 100644 --- a/api/command_join.go +++ b/api/command_join.go @@ -46,7 +46,7 @@ func (me *JoinProvider) DoCommand(c *Context, channelId string, message string) return &model.CommandResponse{Text: c.T("api.command_join.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } - JoinChannel(c, v.Id, "") + JoinChannelById(c, c.Session.UserId, v.Id) if c.Err != nil { c.Err = nil diff --git a/api/post.go b/api/post.go index 4eb87349e..7899145a6 100644 --- a/api/post.go +++ b/api/post.go @@ -27,6 +27,7 @@ func InitPost() { BaseRoutes.NeedTeam.Handle("/posts/search", ApiUserRequired(searchPosts)).Methods("GET") BaseRoutes.NeedTeam.Handle("/posts/{post_id}", ApiUserRequired(getPostById)).Methods("GET") + BaseRoutes.NeedTeam.Handle("/pltmp/{post_id}", ApiUserRequired(getPermalinkTmp)).Methods("GET") BaseRoutes.Posts.Handle("/create", ApiUserRequired(createPost)).Methods("POST") BaseRoutes.Posts.Handle("/update", ApiUserRequired(updatePost)).Methods("POST") @@ -1089,6 +1090,53 @@ func getPostById(c *Context, w http.ResponseWriter, r *http.Request) { } } +func getPermalinkTmp(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + + postId := params["post_id"] + if len(postId) != 26 { + c.SetInvalidParam("getPermalinkTmp", "postId") + return + } + + if result := <-Srv.Store.Post().Get(postId); result.Err != nil { + c.Err = result.Err + return + } else { + list := result.Data.(*model.PostList) + + if len(list.Order) != 1 { + c.Err = model.NewLocAppError("getPermalinkTmp", "api.post_get_post_by_id.get.app_error", nil, "") + return + } + post := list.Posts[list.Order[0]] + + if !c.HasPermissionsToTeam(c.TeamId, "permalink") { + return + } + + cchan := Srv.Store.Channel().CheckPermissionsTo(c.TeamId, post.ChannelId, c.Session.UserId) + if !c.HasPermissionsToChannel(cchan, "getPermalinkTmp") { + // If we don't have permissions attempt to join the channel to fix the problem + if err, _ := JoinChannelById(c, c.Session.UserId, post.ChannelId); err != nil { + // On error just return with permissions error + c.Err = err + return + } else { + // If we sucessfully joined the channel then clear the permissions error and continue + c.Err = nil + } + } + + if HandleEtag(list.Etag(), w, r) { + return + } + + w.Header().Set(model.HEADER_ETAG_SERVER, list.Etag()) + w.Write([]byte(list.ToJson())) + } +} + func deletePost(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) diff --git a/model/client.go b/model/client.go index 4edb859e2..e9f22e452 100644 --- a/model/client.go +++ b/model/client.go @@ -90,6 +90,10 @@ func (c *Client) GetChannelRoute(channelId string) string { return fmt.Sprintf("/teams/%v/channels/%v", c.GetTeamId(), channelId) } +func (c *Client) GetChannelNameRoute(channelName string) string { + return fmt.Sprintf("/teams/%v/channels/name/%v", c.GetTeamId(), channelName) +} + func (c *Client) DoPost(url, data, contentType string) (*http.Response, *AppError) { rq, _ := http.NewRequest("POST", c.Url+url, strings.NewReader(data)) rq.Header.Set("Content-Type", contentType) @@ -806,6 +810,15 @@ func (c *Client) JoinChannel(id string) (*Result, *AppError) { } } +func (c *Client) JoinChannelByName(name string) (*Result, *AppError) { + if r, err := c.DoApiPost(c.GetChannelNameRoute(name)+"/join", ""); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), nil}, nil + } +} + func (c *Client) LeaveChannel(id string) (*Result, *AppError) { if r, err := c.DoApiPost(c.GetChannelRoute(id)+"/leave", ""); err != nil { return nil, err diff --git a/webapp/action_creators/global_actions.jsx b/webapp/action_creators/global_actions.jsx index bf1d82aee..335c20219 100644 --- a/webapp/action_creators/global_actions.jsx +++ b/webapp/action_creators/global_actions.jsx @@ -128,21 +128,31 @@ export function emitInitialLoad(callback) { ); } +export function doFocusPost(channelId, postId, data) { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_FOCUSED_POST, + postId, + post_list: data + }); + AsyncClient.getChannels(true); + AsyncClient.getChannelExtraInfo(channelId); + AsyncClient.getPostsBefore(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS); + AsyncClient.getPostsAfter(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS); +} + export function emitPostFocusEvent(postId) { AsyncClient.getChannels(true); - Client.getPostById( + Client.getPermalinkTmp( postId, (data) => { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_FOCUSED_POST, - postId, - post_list: data - }); - - AsyncClient.getChannelExtraInfo(data.channel_id); - - AsyncClient.getPostsBefore(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS); - AsyncClient.getPostsAfter(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS); + if (!data) { + return; + } + const channelId = data.posts[data.order[0]].channel_id; + doFocusPost(channelId, postId, data); + }, + () => { + browserHistory.push('/error?message=' + encodeURIComponent(Utils.localizeMessage('permalink.error.access', 'Permalink belongs to a channel you do not have access to'))); } ); } @@ -431,3 +441,11 @@ export function emitUserLoggedOutEvent(redirectTo) { } ); } + +export function emitJoinChannelEvent(channel, success, failure) { + Client.joinChannel( + channel.id, + success, + failure, + ); +} diff --git a/webapp/client/client.jsx b/webapp/client/client.jsx index 98e660227..53a514082 100644 --- a/webapp/client/client.jsx +++ b/webapp/client/client.jsx @@ -71,6 +71,10 @@ export default class Client { return `${this.url}${this.urlVersion}/teams/${this.getTeamId()}/channels`; } + getChannelNameRoute(channelName) { + return `${this.url}${this.urlVersion}/teams/${this.getTeamId()}/channels/name/${channelName}`; + } + getChannelNeededRoute(channelId) { return `${this.url}${this.urlVersion}/teams/${this.getTeamId()}/channels/${channelId}`; } @@ -1042,6 +1046,17 @@ export default class Client { this.track('api', 'api_channels_join'); } + joinChannelByName = (name, success, error) => { + request. + post(`${this.getChannelNameRoute(name)}/join`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'joinChannelByName', success, error)); + + this.track('api', 'api_channels_join_name'); + } + deleteChannel = (channelId, success, error) => { request. post(`${this.getChannelNeededRoute(channelId)}/delete`). @@ -1212,6 +1227,17 @@ export default class Client { this.track('api', 'api_posts_create', post.channel_id, 'length', post.message.length); } + // This is a temporary route to get around a problem with the permissions system that + // will be fixed in 3.1 or 3.2 + getPermalinkTmp = (postId, success, error) => { + request. + get(`${this.getTeamNeededRoute()}/pltmp/${postId}`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getPermalinkTmp', success, error)); + } + getPostById = (postId, success, error) => { request. get(`${this.getTeamNeededRoute()}/posts/${postId}`). diff --git a/webapp/components/more_channels.jsx b/webapp/components/more_channels.jsx index 3ab05341b..04c613ce5 100644 --- a/webapp/components/more_channels.jsx +++ b/webapp/components/more_channels.jsx @@ -4,8 +4,8 @@ import $ from 'jquery'; import ReactDOM from 'react-dom'; import * as Utils from 'utils/utils.jsx'; -import client from 'utils/web_client.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; +import * as GlobalActions from 'action_creators/global_actions.jsx'; import ChannelStore from 'stores/channel_store.jsx'; import LoadingScreen from './loading_screen.jsx'; import NewChannelFlow from './new_channel_flow.jsx'; @@ -62,7 +62,8 @@ export default class MoreChannels extends React.Component { } handleJoin(channel, channelIndex) { this.setState({joiningChannel: channelIndex}); - client.joinChannel(channel.id, + GlobalActions.emitJoinChannelEvent( + channel, () => { $(ReactDOM.findDOMNode(this.refs.modal)).modal('hide'); browserHistory.push(Utils.getTeamURLNoOriginFromAddressBar() + '/channels/' + channel.name); diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 6d4f4c287..e56582832 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -1416,5 +1416,6 @@ "web.footer.privacy": "Privacy", "web.footer.terms": "Terms", "web.header.back": "Back", - "web.root.singup_info": "All team communication in one place, searchable and accessible anywhere" + "web.root.singup_info": "All team communication in one place, searchable and accessible anywhere", + "permalink.error.access": "Permalink belongs to a channel you do not have access to" } diff --git a/webapp/root.jsx b/webapp/root.jsx index 1e9adea16..e90d3fdc5 100644 --- a/webapp/root.jsx +++ b/webapp/root.jsx @@ -187,17 +187,11 @@ function onPermalinkEnter(nextState) { GlobalActions.emitPostFocusEvent(postId); } -function onChannelEnter(nextState, replace) { - doChannelChange(nextState, replace); +function onChannelEnter(nextState, replace, callback) { + doChannelChange(nextState, replace, callback); } -function onChannelChange(prevState, nextState, replace) { - if (prevState.params.channel !== nextState.params.channel) { - doChannelChange(nextState, replace); - } -} - -function doChannelChange(state, replace) { +function doChannelChange(state, replace, callback) { let channel; if (state.location.query.fakechannel) { channel = JSON.parse(state.location.query.fakechannel); @@ -207,11 +201,22 @@ function doChannelChange(state, replace) { channel = ChannelStore.getMoreByName(state.params.channel); } if (!channel) { - replace('/'); + Client.joinChannelByName( + state.params.channel, + (data) => { + GlobalActions.emitChannelClickEvent(data); + callback(); + }, + () => { + replace('/'); + callback(); + } + ); return; } } GlobalActions.emitChannelClickEvent(channel); + callback(); } function renderRootComponent() { @@ -311,7 +316,6 @@ function renderRootComponent() { -1) { + this.channels.splice(element, 1); + } + } } var ChannelStore = new ChannelStoreClass(); @@ -349,6 +357,7 @@ ChannelStore.dispatchToken = AppDispatcher.register((payload) => { break; case ActionTypes.LEAVE_CHANNEL: + ChannelStore.leaveChannel(action.id); ChannelStore.emitLeave(action.id); break; diff --git a/webapp/tests/client_channel.test.jsx b/webapp/tests/client_channel.test.jsx index b8374123c..9d88f3de0 100644 --- a/webapp/tests/client_channel.test.jsx +++ b/webapp/tests/client_channel.test.jsx @@ -162,8 +162,30 @@ describe('Client.Channels', function() { function() { TestHelper.basicClient().joinChannel( channel.id, - function(data) { - assert.equal(data.id, channel.id); + function() { + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('joinChannelByName', function(done) { + TestHelper.initBasic(() => { + var channel = TestHelper.basicChannel(); + TestHelper.basicClient().leaveChannel( + channel.id, + function() { + TestHelper.basicClient().joinChannelByName( + channel.name, + function() { done(); }, function(err) { -- cgit v1.2.3-1-g7c22