Merge pull request #2636 from mattermost/private-templates
Added support for access control on templates
This commit is contained in:
commit
36bf5704d0
14 changed files with 75 additions and 35 deletions
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -212,5 +212,16 @@
|
|||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.Menu {
|
||||
position: fixed;
|
||||
left: 55%;
|
||||
right: calc(45% - 240px);
|
||||
|
||||
.menu-contents {
|
||||
min-width: 240px;
|
||||
max-width: 240px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'>
|
||||
|
|
Loading…
Reference in a new issue