Merge pull request #2636 from mattermost/private-templates

Added support for access control on templates
This commit is contained in:
Scott Bishel 2022-03-31 07:42:58 -06:00 committed by GitHub
commit 36bf5704d0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 75 additions and 35 deletions

View file

@ -2188,7 +2188,7 @@ func (a *API) handleGetTemplates(w http.ResponseWriter, r *http.Request) {
auditRec.AddMeta("teamID", teamID)
// retrieve boards list
boards, err := a.app.GetTemplateBoards(teamID)
boards, err := a.app.GetTemplateBoards(teamID, userID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return

View file

@ -150,8 +150,8 @@ func (a *App) GetBoardsForUserAndTeam(userID, teamID string) ([]*model.Board, er
return a.store.GetBoardsForUserAndTeam(userID, teamID)
}
func (a *App) GetTemplateBoards(teamID string) ([]*model.Board, error) {
return a.store.GetTemplateBoards(teamID)
func (a *App) GetTemplateBoards(teamID, userID string) ([]*model.Board, error) {
return a.store.GetTemplateBoards(teamID, userID)
}
func (a *App) CreateBoard(board *model.Board, userID string, addMember bool) (*model.Board, error) {

View file

@ -46,14 +46,14 @@ func (a *App) PrepareOnboardingTour(userID string, teamID string) (string, strin
}
func (a *App) getOnboardingBoardID() (string, error) {
boards, err := a.store.GetTemplateBoards(globalTeamID)
boards, err := a.store.GetTemplateBoards(globalTeamID, "")
if err != nil {
return "", err
}
var onboardingBoardID string
for _, block := range boards {
if block.Title == WelcomeBoardTitle {
if block.Title == WelcomeBoardTitle && block.TeamID == globalTeamID {
onboardingBoardID = block.ID
break
}

View file

@ -25,7 +25,7 @@ func TestPrepareOnboardingTour(t *testing.T) {
IsTemplate: true,
}
th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{&welcomeBoard}, nil)
th.Store.EXPECT().GetTemplateBoards("0", "").Return([]*model.Board{&welcomeBoard}, nil)
th.Store.EXPECT().DuplicateBoard(welcomeBoard.ID, userID, teamID, false).Return(&model.BoardsAndBlocks{Boards: []*model.Board{&welcomeBoard}},
nil, nil)
th.Store.EXPECT().GetMembersForBoard(welcomeBoard.ID).Return([]*model.BoardMember{}, nil)
@ -60,7 +60,7 @@ func TestCreateWelcomeBoard(t *testing.T) {
TeamID: "0",
IsTemplate: true,
}
th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{&welcomeBoard}, nil)
th.Store.EXPECT().GetTemplateBoards("0", "").Return([]*model.Board{&welcomeBoard}, nil)
th.Store.EXPECT().DuplicateBoard(welcomeBoard.ID, userID, teamID, false).
Return(&model.BoardsAndBlocks{Boards: []*model.Board{&welcomeBoard}}, nil, nil)
th.Store.EXPECT().GetMembersForBoard(welcomeBoard.ID).Return([]*model.BoardMember{}, nil)
@ -72,7 +72,7 @@ func TestCreateWelcomeBoard(t *testing.T) {
t.Run("template doesn't contain a board", func(t *testing.T) {
teamID := testTeamID
th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{}, nil)
th.Store.EXPECT().GetTemplateBoards("0", "").Return([]*model.Board{}, nil)
boardID, err := th.App.createWelcomeBoard("user_id_1", teamID)
assert.Error(t, err)
assert.Empty(t, boardID)
@ -86,7 +86,7 @@ func TestCreateWelcomeBoard(t *testing.T) {
TeamID: teamID,
IsTemplate: true,
}
th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{&welcomeBoard}, nil)
th.Store.EXPECT().GetTemplateBoards("0", "").Return([]*model.Board{&welcomeBoard}, nil)
boardID, err := th.App.createWelcomeBoard("user_id_1", "workspace_id_1")
assert.Error(t, err)
assert.Empty(t, boardID)
@ -104,7 +104,7 @@ func TestGetOnboardingBoardID(t *testing.T) {
TeamID: "0",
IsTemplate: true,
}
th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{&welcomeBoard}, nil)
th.Store.EXPECT().GetTemplateBoards("0", "").Return([]*model.Board{&welcomeBoard}, nil)
onboardingBoardID, err := th.App.getOnboardingBoardID()
assert.NoError(t, err)
@ -112,7 +112,7 @@ func TestGetOnboardingBoardID(t *testing.T) {
})
t.Run("no blocks found", func(t *testing.T) {
th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{}, nil)
th.Store.EXPECT().GetTemplateBoards("0", "").Return([]*model.Board{}, nil)
onboardingBoardID, err := th.App.getOnboardingBoardID()
assert.Error(t, err)
@ -126,7 +126,7 @@ func TestGetOnboardingBoardID(t *testing.T) {
TeamID: "0",
IsTemplate: true,
}
th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{&welcomeBoard}, nil)
th.Store.EXPECT().GetTemplateBoards("0", "").Return([]*model.Board{&welcomeBoard}, nil)
onboardingBoardID, err := th.App.getOnboardingBoardID()
assert.Error(t, err)

View file

@ -23,7 +23,7 @@ func (a *App) InitTemplates() error {
// initializeTemplates imports default templates if the boards table is empty.
func (a *App) initializeTemplates() (bool, error) {
boards, err := a.store.GetTemplateBoards(globalTeamID)
boards, err := a.store.GetTemplateBoards(globalTeamID, "")
if err != nil {
return false, fmt.Errorf("cannot initialize templates: %w", err)
}

View file

@ -38,7 +38,7 @@ func TestApp_initializeTemplates(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
th.Store.EXPECT().GetTemplateBoards(globalTeamID).Return([]*model.Board{}, nil)
th.Store.EXPECT().GetTemplateBoards(globalTeamID, "").Return([]*model.Board{}, nil)
th.Store.EXPECT().RemoveDefaultTemplates([]*model.Board{}).Return(nil)
th.Store.EXPECT().CreateBoardsAndBlocks(gomock.Any(), gomock.Any()).AnyTimes().Return(boardsAndBlocks, nil)
th.Store.EXPECT().GetMembersForBoard(board.ID).AnyTimes().Return([]*model.BoardMember{}, nil)
@ -54,7 +54,7 @@ func TestApp_initializeTemplates(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
th.Store.EXPECT().GetTemplateBoards(globalTeamID).Return([]*model.Board{board}, nil)
th.Store.EXPECT().GetTemplateBoards(globalTeamID, "").Return([]*model.Board{board}, nil)
done, err := th.App.initializeTemplates()
require.NoError(t, err, "initializeTemplates should not error")

View file

@ -42,7 +42,7 @@ func setupTestHelper(t *testing.T) *TestHelper {
newAuth := New(&cfg, mockStore, localpermissions.New(mockPermissions, logger))
// called during default template setup for every test
mockStore.EXPECT().GetTemplateBoards(gomock.Any()).AnyTimes()
mockStore.EXPECT().GetTemplateBoards("0", "").AnyTimes()
mockStore.EXPECT().RemoveDefaultTemplates(gomock.Any()).AnyTimes()
mockStore.EXPECT().InsertBlock(gomock.Any(), gomock.Any()).AnyTimes()

View file

@ -881,18 +881,18 @@ func (mr *MockStoreMockRecorder) GetTeamsForUser(arg0 interface{}) *gomock.Call
}
// GetTemplateBoards mocks base method.
func (m *MockStore) GetTemplateBoards(arg0 string) ([]*model.Board, error) {
func (m *MockStore) GetTemplateBoards(arg0, arg1 string) ([]*model.Board, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetTemplateBoards", arg0)
ret := m.ctrl.Call(m, "GetTemplateBoards", arg0, arg1)
ret0, _ := ret[0].([]*model.Board)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetTemplateBoards indicates an expected call of GetTemplateBoards.
func (mr *MockStoreMockRecorder) GetTemplateBoards(arg0 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) GetTemplateBoards(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateBoards", reflect.TypeOf((*MockStore)(nil).GetTemplateBoards), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateBoards", reflect.TypeOf((*MockStore)(nil).GetTemplateBoards), arg0, arg1)
}
// GetUserByEmail mocks base method.

View file

@ -435,8 +435,8 @@ func (s *SQLStore) GetTeamsForUser(userID string) ([]*model.Team, error) {
}
func (s *SQLStore) GetTemplateBoards(teamID string) ([]*model.Board, error) {
return s.getTemplateBoards(s.db, teamID)
func (s *SQLStore) GetTemplateBoards(teamID string, userID string) ([]*model.Board, error) {
return s.getTemplateBoards(s.db, teamID, userID)
}

View file

@ -54,13 +54,24 @@ func (s *SQLStore) removeDefaultTemplates(db sq.BaseRunner, boards []*model.Boar
return nil
}
// getDefaultTemplateBoards fetches all template blocks .
func (s *SQLStore) getTemplateBoards(db sq.BaseRunner, teamID string) ([]*model.Board, error) {
// getTemplateBoards fetches all template boards .
func (s *SQLStore) getTemplateBoards(db sq.BaseRunner, teamID, userID string) ([]*model.Board, error) {
query := s.getQueryBuilder(db).
Select(boardFields("")...).
From(s.tablePrefix + "boards").
Where(sq.Eq{"coalesce(team_id, '0')": teamID}).
Where(sq.Eq{"is_template": true})
From(s.tablePrefix+"boards as b").
LeftJoin(s.tablePrefix+"board_members as bm on b.id = bm.board_id and bm.user_id = ?", userID).
Where(sq.Eq{"is_template": true}).
Where(sq.Eq{"b.team_id": teamID}).
Where(sq.Or{
// this is to include public templates even if there is not board_member entry
sq.And{
sq.Eq{"bm.board_id": nil},
sq.Eq{"b.type": model.BoardTypeOpen},
},
sq.And{
sq.NotEq{"bm.board_id": nil},
},
})
rows, err := query.Query()
if err != nil {
@ -69,5 +80,10 @@ func (s *SQLStore) getTemplateBoards(db sq.BaseRunner, teamID string) ([]*model.
}
defer s.CloseRows(rows)
return s.boardsFromRows(rows)
userTemplates, err := s.boardsFromRows(rows)
if err != nil {
return nil, err
}
return userTemplates, nil
}

View file

@ -129,7 +129,7 @@ type Store interface {
GetNextNotificationHint(remove bool) (*model.NotificationHint, error)
RemoveDefaultTemplates(boards []*model.Board) error
GetTemplateBoards(teamID string) ([]*model.Board, error)
GetTemplateBoards(teamID, userID string) ([]*model.Board, error)
DBType() string

View file

@ -181,6 +181,7 @@
"ShareBoard.tokenRegenrated": "Token regenerated",
"ShareBoard.userPermissionsRemoveMemberText": "Remove member",
"ShareBoard.userPermissionsYouText": "(You)",
"ShareTemplate.Title": "Share Template",
"Sidebar.about": "About Focalboard",
"Sidebar.add-board": "+ Add board",
"Sidebar.changePassword": "Change password",

View file

@ -212,5 +212,16 @@
text-decoration: underline;
}
}
.Menu {
position: fixed;
left: 55%;
right: calc(45% - 240px);
.menu-contents {
min-width: 240px;
max-width: 240px;
}
}
}
}

View file

@ -9,7 +9,7 @@ import Select from 'react-select/async'
import {CSSObject} from '@emotion/serialize'
import {useAppSelector} from '../../store/hooks'
import {getCurrentBoardId, getCurrentBoardMembers} from '../../store/boards'
import {getCurrentBoard, getCurrentBoardMembers} from '../../store/boards'
import {getMe, getBoardUsersList} from '../../store/users'
import {Utils, IDType} from '../../utils'
@ -95,7 +95,8 @@ export default function ShareBoardDialog(props: Props): JSX.Element {
// members of the current board
const members = useAppSelector<{[key: string]: BoardMember}>(getCurrentBoardMembers)
const boardId = useAppSelector(getCurrentBoardId)
const board = useAppSelector(getCurrentBoard)
const boardId = board.id
const boardUsers = useAppSelector<IUser[]>(getBoardUsersList)
const me = useAppSelector<IUser|null>(getMe)
@ -239,7 +240,7 @@ export default function ShareBoardDialog(props: Props): JSX.Element {
))
}
const toolbar = (
const shareBoardTitle = (
<span className='text-heading5'>
<FormattedMessage
id={'ShareBoard.Title'}
@ -248,6 +249,17 @@ export default function ShareBoardDialog(props: Props): JSX.Element {
</span>
)
const shareTemplateTitle = (
<span className='text-heading5'>
<FormattedMessage
id={'ShareTemplate.Title'}
defaultMessage={'Share Template'}
/>
</span>
)
const toolbar = board.isTemplate ? shareTemplateTitle : shareBoardTitle
return (
<Dialog
onClose={props.onClose}
@ -299,7 +311,7 @@ export default function ShareBoardDialog(props: Props): JSX.Element {
})}
</div>
{props.enableSharedBoards && (
{props.enableSharedBoards && !board.isTemplate && (
<div className='tabs-container'>
<button
onClick={() => setPublish(false)}
@ -323,7 +335,7 @@ export default function ShareBoardDialog(props: Props): JSX.Element {
</BoardPermissionGate>
</div>
)}
{(props.enableSharedBoards && publish) &&
{(props.enableSharedBoards && publish && !board.isTemplate) &&
(<BoardPermissionGate permissions={[Permission.ShareBoard]}>
<div className='tabs-content'>
<div>
@ -401,7 +413,7 @@ export default function ShareBoardDialog(props: Props): JSX.Element {
</BoardPermissionGate>
)}
{!publish && (
{!publish && !board.isTemplate && (
<div className='tabs-content'>
<div>
<div className='d-flex justify-content-between'>