1
0
mirror of https://github.com/mattermost/focalboard.git synced 2024-12-24 13:43:12 +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:
Scott Bishel 2023-02-22 14:55:13 -07:00 committed by GitHub
parent 230e519352
commit f88ee1ece3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 259 additions and 29 deletions

View File

@ -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": ""
}
]
}

View File

@ -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>
`;

View File

@ -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()
})
})

View File

@ -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) => (

View File

@ -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))
}

View File

@ -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 {

View File

@ -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) {

View File

@ -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))
})
}

View File

@ -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) {

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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) {