You've already forked focalboard
							
							
				mirror of
				https://github.com/mattermost/focalboard.git
				synced 2025-10-31 00:17:42 +02:00 
			
		
		
		
	Fix for users adding board links to Read-only channels (#4581)
* initial commit for channel permissions * add test * check for write post when returning channels * lint fix * fix test --------- Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
		
							
								
								
									
										3
									
								
								mattermost-plugin/server/manifest.go
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								mattermost-plugin/server/manifest.go
									
									
									
										generated
									
									
									
								
							| @@ -45,7 +45,8 @@ const manifestStr = ` | ||||
|         "type": "bool", | ||||
|         "help_text": "This allows board editors to share boards that can be accessed by anyone with the link.", | ||||
|         "placeholder": "", | ||||
|         "default": false | ||||
|         "default": false, | ||||
|         "hosting": "" | ||||
|       } | ||||
|     ] | ||||
|   } | ||||
|   | ||||
| @@ -109,6 +109,104 @@ exports[`components/rhsChannelBoards renders the RHS for channel boards 1`] = ` | ||||
| </div> | ||||
| `; | ||||
|  | ||||
| exports[`components/rhsChannelBoards renders the RHS for channel boards, no add 1`] = ` | ||||
| <div> | ||||
|   <div | ||||
|     class="focalboard-body" | ||||
|   > | ||||
|     <div | ||||
|       class="RHSChannelBoards" | ||||
|     > | ||||
|       <div | ||||
|         class="rhs-boards-header" | ||||
|       > | ||||
|         <span | ||||
|           class="linked-boards" | ||||
|         > | ||||
|           Linked boards | ||||
|         </span> | ||||
|       </div> | ||||
|       <div | ||||
|         class="rhs-boards-list" | ||||
|       > | ||||
|         <div | ||||
|           class="RHSChannelBoardItem" | ||||
|         > | ||||
|           <div | ||||
|             class="board-info" | ||||
|           > | ||||
|              | ||||
|             <span | ||||
|               class="title" | ||||
|             > | ||||
|               Untitled board | ||||
|             </span> | ||||
|             <div | ||||
|               aria-label="menuwrapper" | ||||
|               class="MenuWrapper" | ||||
|               role="button" | ||||
|             > | ||||
|               <button | ||||
|                 class="IconButton" | ||||
|                 type="button" | ||||
|               > | ||||
|                 <i | ||||
|                   class="CompassIcon icon-dots-horizontal OptionsIcon" | ||||
|                 /> | ||||
|               </button> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div | ||||
|             class="description" | ||||
|           /> | ||||
|           <div | ||||
|             class="date" | ||||
|           > | ||||
|             Last update at: July 08, 2022, 8:10 PM | ||||
|           </div> | ||||
|         </div> | ||||
|         <div | ||||
|           class="RHSChannelBoardItem" | ||||
|         > | ||||
|           <div | ||||
|             class="board-info" | ||||
|           > | ||||
|              | ||||
|             <span | ||||
|               class="title" | ||||
|             > | ||||
|               Untitled board | ||||
|             </span> | ||||
|             <div | ||||
|               aria-label="menuwrapper" | ||||
|               class="MenuWrapper" | ||||
|               role="button" | ||||
|             > | ||||
|               <button | ||||
|                 class="IconButton" | ||||
|                 type="button" | ||||
|               > | ||||
|                 <i | ||||
|                   class="CompassIcon icon-dots-horizontal OptionsIcon" | ||||
|                 /> | ||||
|               </button> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div | ||||
|             class="description" | ||||
|           /> | ||||
|           <div | ||||
|             class="date" | ||||
|           > | ||||
|             Last update at: July 08, 2022, 8:10 PM | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
| `; | ||||
|  | ||||
| exports[`components/rhsChannelBoards renders with empty list of boards 1`] = ` | ||||
| <div> | ||||
|   <div | ||||
| @@ -144,3 +242,31 @@ exports[`components/rhsChannelBoards renders with empty list of boards 1`] = ` | ||||
|   </div> | ||||
| </div> | ||||
| `; | ||||
|  | ||||
| exports[`components/rhsChannelBoards renders with empty list of boards, cannot add 1`] = ` | ||||
| <div> | ||||
|   <div | ||||
|     class="focalboard-body" | ||||
|   > | ||||
|     <div | ||||
|       class="RHSChannelBoards empty" | ||||
|     > | ||||
|       <h2> | ||||
|         No boards are linked to Channel Name yet | ||||
|       </h2> | ||||
|       <div | ||||
|         class="empty-paragraph" | ||||
|       > | ||||
|         Boards is a project management tool that helps define, organize, track and manage work across teams, using a familiar kanban board view. | ||||
|       </div> | ||||
|       <div | ||||
|         class="boards-screenshots" | ||||
|       > | ||||
|         <img | ||||
|           src="test-file-stub" | ||||
|         /> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
| `; | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
|  | ||||
| import React from 'react' | ||||
| import {Provider as ReduxProvider} from 'react-redux' | ||||
| import {act, render} from '@testing-library/react' | ||||
| import {act, render, screen} from '@testing-library/react' | ||||
| import {mocked} from 'jest-mock' | ||||
| import thunk from 'redux-thunk' | ||||
|  | ||||
| @@ -44,6 +44,7 @@ describe('components/rhsChannelBoards', () => { | ||||
|         users: { | ||||
|             me: { | ||||
|                 id: 'user-id', | ||||
|                 permissions: ['create_post'] | ||||
|             }, | ||||
|         }, | ||||
|         language: { | ||||
| @@ -89,7 +90,8 @@ describe('components/rhsChannelBoards', () => { | ||||
|             )) | ||||
|             container = result.container | ||||
|         }) | ||||
|  | ||||
|         const buttonElement = screen.queryByText('Add') | ||||
|         expect(buttonElement).not.toBeNull() | ||||
|         expect(container).toMatchSnapshot() | ||||
|     }) | ||||
|  | ||||
| @@ -107,6 +109,45 @@ describe('components/rhsChannelBoards', () => { | ||||
|             container = result.container | ||||
|         }) | ||||
|  | ||||
|         const buttonElement = screen.queryByText('Link boards to Channel Name') | ||||
|         expect(buttonElement).not.toBeNull() | ||||
|         expect(container).toMatchSnapshot() | ||||
|     }) | ||||
|  | ||||
|     it('renders the RHS for channel boards, no add', async () => { | ||||
|         const localState = {...state, users: {me:{id: 'user-id'}}} | ||||
|         const store = mockStateStore([thunk], localState) | ||||
|         let container: Element | DocumentFragment | null = null | ||||
|         await act(async () => { | ||||
|             const result = render(wrapIntl( | ||||
|                 <ReduxProvider store={store}> | ||||
|                     <RHSChannelBoards/> | ||||
|                 </ReduxProvider> | ||||
|             )) | ||||
|             container = result.container | ||||
|         }) | ||||
|  | ||||
|         const buttonElement = screen.queryByText('Add') | ||||
|         expect(buttonElement).toBeNull() | ||||
|         expect(container).toMatchSnapshot() | ||||
|     }) | ||||
|  | ||||
|     it('renders with empty list of boards, cannot add', async () => { | ||||
|         const localState = {...state, users: {me:{id: 'user-id'}}, boards: {...state.boards, boards: {}}} | ||||
|         const store = mockStateStore([thunk], localState) | ||||
|  | ||||
|         let container: Element | DocumentFragment | null = null | ||||
|         await act(async () => { | ||||
|             const result = render(wrapIntl( | ||||
|                 <ReduxProvider store={store}> | ||||
|                     <RHSChannelBoards/> | ||||
|                 </ReduxProvider> | ||||
|             )) | ||||
|             container = result.container | ||||
|         }) | ||||
|  | ||||
|         const buttonElement = screen.queryByText('Link boards to Channel Name') | ||||
|         expect(buttonElement).toBeNull() | ||||
|         expect(container).toMatchSnapshot() | ||||
|     }) | ||||
| }) | ||||
|   | ||||
| @@ -49,7 +49,7 @@ const RHSChannelBoards = () => { | ||||
|             dispatch(loadMyBoardsMemberships()), | ||||
|             dispatch(fetchMe()), | ||||
|         ]).then(() => setDataLoaded(true)) | ||||
|     }, []) | ||||
|     }, [currentChannel?.id]) | ||||
|  | ||||
|     useWebsockets(teamId || '', (wsClient: WSClient) => { | ||||
|         const onChangeBoardHandler = (_: WSClient, boards: Board[]): void => { | ||||
| @@ -117,17 +117,19 @@ const RHSChannelBoards = () => { | ||||
|                         /> | ||||
|                     </div> | ||||
|                     <div className='boards-screenshots'><img src={Utils.buildURL(boardsScreenshots, true)}/></div> | ||||
|                     <Button | ||||
|                         onClick={() => dispatch(setLinkToChannel(currentChannel.id))} | ||||
|                         emphasis='primary' | ||||
|                         size='medium' | ||||
|                     > | ||||
|                         <FormattedMessage | ||||
|                             id='rhs-boards.link-boards-to-channel' | ||||
|                             defaultMessage='Link boards to {channelName}' | ||||
|                             values={{channelName: channelName}} | ||||
|                         /> | ||||
|                     </Button> | ||||
|                     {me?.permissions?.find((s) => s === 'create_post') && | ||||
|                         <Button | ||||
|                             onClick={() => dispatch(setLinkToChannel(currentChannel.id))} | ||||
|                             emphasis='primary' | ||||
|                             size='medium' | ||||
|                         > | ||||
|                             <FormattedMessage | ||||
|                                 id='rhs-boards.link-boards-to-channel' | ||||
|                                 defaultMessage='Link boards to {channelName}' | ||||
|                                 values={{channelName: channelName}} | ||||
|                             /> | ||||
|                         </Button> | ||||
|                     } | ||||
|                 </div> | ||||
|             </div> | ||||
|         ) | ||||
| @@ -143,16 +145,18 @@ const RHSChannelBoards = () => { | ||||
|                             defaultMessage='Linked boards' | ||||
|                         /> | ||||
|                     </span> | ||||
|                     <Button | ||||
|                         onClick={() => dispatch(setLinkToChannel(currentChannel.id))} | ||||
|                         icon={<AddIcon/>} | ||||
|                         emphasis='primary' | ||||
|                     > | ||||
|                         <FormattedMessage | ||||
|                             id='rhs-boards.add' | ||||
|                             defaultMessage='Add' | ||||
|                         /> | ||||
|                     </Button> | ||||
|                     {me?.permissions?.find((s) => s === 'create_post') && | ||||
|                         <Button | ||||
|                             onClick={() => dispatch(setLinkToChannel(currentChannel.id))} | ||||
|                             icon={<AddIcon/>} | ||||
|                             emphasis='primary' | ||||
|                         > | ||||
|                             <FormattedMessage | ||||
|                                 id='rhs-boards.add' | ||||
|                                 defaultMessage='Add' | ||||
|                             /> | ||||
|                         </Button> | ||||
|                     } | ||||
|                 </div> | ||||
|                 <div className='rhs-boards-list'> | ||||
|                     {channelBoards.map((b) => ( | ||||
|   | ||||
| @@ -249,6 +249,7 @@ export default class Plugin { | ||||
|             if (lastViewedChannel !== currentChannel && currentChannel) { | ||||
|                 localStorage.setItem('focalboardLastViewedChannel:' + currentUserId, currentChannel) | ||||
|                 lastViewedChannel = currentChannel | ||||
|                 octoClient.channelId = currentChannel | ||||
|                 const currentChannelObj = mmStore.getState().entities.channels.channels[lastViewedChannel] | ||||
|                 store.dispatch(setChannel(currentChannelObj)) | ||||
|             } | ||||
|   | ||||
| @@ -113,6 +113,11 @@ func (a *API) handleGetMe(w http.ResponseWriter, r *http.Request) { | ||||
| 	//   description: Team ID | ||||
| 	//   required: false | ||||
| 	//   type: string | ||||
| 	// - name: channelID | ||||
| 	//   in: path | ||||
| 	//   description: Channel ID | ||||
| 	//   required: false | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| @@ -126,6 +131,7 @@ func (a *API) handleGetMe(w http.ResponseWriter, r *http.Request) { | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
| 	query := r.URL.Query() | ||||
| 	teamID := query.Get("teamID") | ||||
| 	channelID := query.Get("channelID") | ||||
|  | ||||
| 	userID := getUserID(r) | ||||
|  | ||||
| @@ -160,6 +166,9 @@ func (a *API) handleGetMe(w http.ResponseWriter, r *http.Request) { | ||||
| 	if a.permissions.HasPermissionTo(userID, model.PermissionManageSystem) { | ||||
| 		user.Permissions = append(user.Permissions, model.PermissionManageSystem.Id) | ||||
| 	} | ||||
| 	if channelID != "" && a.permissions.HasPermissionToChannel(userID, channelID, model.PermissionCreatePost) { | ||||
| 		user.Permissions = append(user.Permissions, model.PermissionCreatePost.Id) | ||||
| 	} | ||||
|  | ||||
| 	userData, err := json.Marshal(user) | ||||
| 	if err != nil { | ||||
|   | ||||
| @@ -63,7 +63,18 @@ func (a *App) CanSeeUser(seerUser string, seenUser string) (bool, error) { | ||||
| } | ||||
|  | ||||
| func (a *App) SearchUserChannels(teamID string, userID string, query string) ([]*mmModel.Channel, error) { | ||||
| 	return a.store.SearchUserChannels(teamID, userID, query) | ||||
| 	channels, err := a.store.SearchUserChannels(teamID, userID, query) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	var writeableChannels []*mmModel.Channel | ||||
| 	for _, channel := range channels { | ||||
| 		if a.permissions.HasPermissionToChannel(userID, channel.Id, model.PermissionCreatePost) { | ||||
| 			writeableChannels = append(writeableChannels, channel) | ||||
| 		} | ||||
| 	} | ||||
| 	return writeableChannels, nil | ||||
| } | ||||
|  | ||||
| func (a *App) GetChannel(teamID string, channelID string) (*mmModel.Channel, error) { | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	mmModel "github.com/mattermost/mattermost-server/v6/model" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| @@ -61,4 +62,24 @@ func TestSearchUsers(t *testing.T) { | ||||
| 		assert.Equal(t, users[0].Permissions[0], model.PermissionManageTeam.Id) | ||||
| 		assert.Equal(t, users[0].Permissions[1], model.PermissionManageSystem.Id) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("test user channels", func(t *testing.T) { | ||||
| 		channelID := "Channel1" | ||||
| 		th.Store.EXPECT().SearchUserChannels(teamID, userID, "").Return([]*mmModel.Channel{{Id: channelID}}, nil) | ||||
| 		th.API.EXPECT().HasPermissionToChannel(userID, channelID, model.PermissionCreatePost).Return(true).Times(1) | ||||
|  | ||||
| 		channels, err := th.App.SearchUserChannels(teamID, userID, "") | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, 1, len(channels)) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("test user channels- no permissions", func(t *testing.T) { | ||||
| 		channelID := "Channel1" | ||||
| 		th.Store.EXPECT().SearchUserChannels(teamID, userID, "").Return([]*mmModel.Channel{{Id: channelID}}, nil) | ||||
| 		th.API.EXPECT().HasPermissionToChannel(userID, channelID, model.PermissionCreatePost).Return(false).Times(1) | ||||
|  | ||||
| 		channels, err := th.App.SearchUserChannels(teamID, userID, "") | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, 0, len(channels)) | ||||
| 	}) | ||||
| } | ||||
|   | ||||
| @@ -91,7 +91,7 @@ func (*FakePermissionPluginAPI) HasPermissionToTeam(userID string, teamID string | ||||
| } | ||||
|  | ||||
| func (*FakePermissionPluginAPI) HasPermissionToChannel(userID string, channelID string, permission *mmModel.Permission) bool { | ||||
| 	return channelID == "valid-channel-id" | ||||
| 	return channelID == "valid-channel-id" || channelID == "valid-channel-id-2" | ||||
| } | ||||
|  | ||||
| func getTestConfig() (*config.Configuration, error) { | ||||
|   | ||||
| @@ -9,6 +9,7 @@ var ( | ||||
| 	PermissionManageTeam            = mmModel.PermissionManageTeam | ||||
| 	PermissionManageSystem          = mmModel.PermissionManageSystem | ||||
| 	PermissionReadChannel           = mmModel.PermissionReadChannel | ||||
| 	PermissionCreatePost            = mmModel.PermissionCreatePost | ||||
| 	PermissionViewMembers           = mmModel.PermissionViewMembers | ||||
| 	PermissionCreatePublicChannel   = mmModel.PermissionCreatePublicChannel | ||||
| 	PermissionCreatePrivateChannel  = mmModel.PermissionCreatePrivateChannel | ||||
|   | ||||
| @@ -107,6 +107,7 @@ const BoardsSwitcher = (props: Props): JSX.Element => { | ||||
|                         inverted={true} | ||||
|                         className='add-board-icon' | ||||
|                         icon={<AddIcon/>} | ||||
|                         title={'Add Board Dropdown'} | ||||
|                     /> | ||||
|                     <Menu> | ||||
|                         <Menu.Text | ||||
|   | ||||
| @@ -8,6 +8,7 @@ enum Permission { | ||||
|     DeleteBoard = 'delete_board', | ||||
|     ShareBoard = 'share_board', | ||||
|     ManageBoardRoles = 'manage_board_roles', | ||||
|     ChannelCreatePost = 'create_post', | ||||
|     ManageBoardCards = 'manage_board_cards', | ||||
|     ManageBoardProperties = 'manage_board_properties', | ||||
|     CommentBoardCards = 'comment_board_cards', | ||||
| @@ -196,6 +197,7 @@ class Constants { | ||||
|     } | ||||
|  | ||||
|     static readonly globalTeamId = '0' | ||||
|     static readonly noChannelID = '0' | ||||
|  | ||||
|     static readonly myInsights = 'MY' | ||||
|  | ||||
|   | ||||
| @@ -51,7 +51,7 @@ class OctoClient { | ||||
|         localStorage.setItem('focalboardSessionId', value) | ||||
|     } | ||||
|  | ||||
|     constructor(serverUrl?: string, public teamId = Constants.globalTeamId) { | ||||
|     constructor(serverUrl?: string, public teamId = Constants.globalTeamId, public channelId = Constants.noChannelID) { | ||||
|         this.serverUrl = serverUrl | ||||
|     } | ||||
|  | ||||
| @@ -161,8 +161,20 @@ class OctoClient { | ||||
|  | ||||
|     async getMe(): Promise<IUser | undefined> { | ||||
|         let path = '/api/v2/users/me' | ||||
|         let parameters = '' | ||||
|         if (this.teamId !== Constants.globalTeamId) { | ||||
|             path += `?teamID=${this.teamId}` | ||||
|             parameters = `teamID=${this.teamId}` | ||||
|         } | ||||
|         if (this.channelId !== Constants.noChannelID) { | ||||
|             const channelClause = `channelID=${this.channelId}` | ||||
|             if (parameters) { | ||||
|                 parameters += '&' + channelClause | ||||
|             } else { | ||||
|                 parameters = channelClause | ||||
|             } | ||||
|         } | ||||
|         if (parameters) { | ||||
|             path += '?' + parameters | ||||
|         } | ||||
|         const response = await fetch(this.getBaseURL() + path, {headers: this.headers()}) | ||||
|         if (response.status !== 200) { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user