Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions server/api/archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,9 @@ func (a *API) handleArchiveImport(w http.ResponseWriter, r *http.Request) {
opt := model.ImportArchiveOptions{
TeamID: teamID,
ModifiedBy: userID,
BoardValidator: func(board *model.Board) error {
return a.authorizeBoardCreate(userID, teamID, board.Type)
},
}

if err := a.app.ImportArchive(file, opt); err != nil {
Expand Down
36 changes: 26 additions & 10 deletions server/api/boards.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,24 @@ func (a *API) registerBoardsRoutes(r *mux.Router) {
r.HandleFunc("/boards/{boardID}/metadata", a.sessionRequired(a.handleGetBoardMetadata)).Methods("GET")
}

// authorizeBoardCreate enforces the per-type board creation policy: creating a
// public board requires the create_public_channel team permission, and creating
// any other board type requires create_private_channel. It is shared across all
// board creation paths so the policy is enforced consistently.
func (a *API) authorizeBoardCreate(userID, teamID string, boardType model.BoardType) error {
if boardType == model.BoardTypeOpen {
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionCreatePublicChannel) {
return model.NewErrPermission("access denied to create public boards")
}
} else {
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionCreatePrivateChannel) {
return model.NewErrPermission("access denied to create private boards")
}
}

return nil
}

func (a *API) authorizeBoardPatch(userID, boardID string, patch *model.BoardPatch) error {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardProperties) {
return model.NewErrPermission("access denied to modifying board properties")
Expand Down Expand Up @@ -163,16 +181,9 @@ func (a *API) handleCreateBoard(w http.ResponseWriter, r *http.Request) {
return
}

if newBoard.Type == model.BoardTypeOpen {
if !a.permissions.HasPermissionToTeam(userID, newBoard.TeamID, model.PermissionCreatePublicChannel) {
a.errorResponse(w, r, model.NewErrPermission("access denied to create public boards"))
return
}
} else {
if !a.permissions.HasPermissionToTeam(userID, newBoard.TeamID, model.PermissionCreatePrivateChannel) {
a.errorResponse(w, r, model.NewErrPermission("access denied to create private boards"))
return
}
if err = a.authorizeBoardCreate(userID, newBoard.TeamID, newBoard.Type); err != nil {
a.errorResponse(w, r, err)
return
}

isGuest, err := a.userIsGuest(userID)
Expand Down Expand Up @@ -538,6 +549,11 @@ func (a *API) handleDuplicateBoard(w http.ResponseWriter, r *http.Request) {
return
}

if err = a.authorizeBoardCreate(userID, toTeam, model.BoardTypePrivate); err != nil {
a.errorResponse(w, r, err)
return
}

auditRec := a.makeAuditRecord(r, "duplicateBoard", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("boardID", boardID)
Expand Down
7 changes: 7 additions & 0 deletions server/api/boards_and_blocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,13 @@ func (a *API) handleCreateBoardsAndBlocks(w http.ResponseWriter, r *http.Request
return
}

for _, board := range newBab.Boards {
if err = a.authorizeBoardCreate(userID, board.TeamID, board.Type); err != nil {
a.errorResponse(w, r, err)
return
}
}

for _, block := range newBab.Blocks {
// Error checking
if len(block.Type) < 1 {
Expand Down
124 changes: 105 additions & 19 deletions server/app/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ const (
)

var (
errBlockIsNotABoard = errors.New("block is not a board")
errSizeLimitExceeded = errors.New("size limit exceeded")
errBlockIsNotABoard = errors.New("block is not a board")
errSizeLimitExceeded = errors.New("size limit exceeded")
errArchiveNotSeekable = errors.New("cannot import archive: reader must support seeking to validate boards")
)

// ImportArchive imports an archive containing zero or more boards, plus all
Expand All @@ -39,6 +40,66 @@ var (
// Archives are ZIP files containing a `version.json` file and zero or more
// directories, each containing a `board.jsonl` and zero or more image files.
func (a *App) ImportArchive(r io.Reader, opt model.ImportArchiveOptions) error {
// When a board validator is provided, authorize every board in the archive
// before persisting anything. The archive is imported as a stream that
// commits board-by-board, so validation runs as a dedicated first pass and
// the reader is rewound before the real import.
if opt.BoardValidator != nil {
if err := a.validateArchiveBoards(r, opt); err != nil {
return err
}
seeker, ok := r.(io.Seeker)
if !ok {
return errArchiveNotSeekable
}
if _, err := seeker.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("cannot rewind archive after board validation: %w", err)
}
}

return a.importArchive(r, opt)
}

// validateArchiveBoards authorizes every board contained in the archive without
// persisting anything, so a single rejected board aborts the whole import
// before any board is created.
func (a *App) validateArchiveBoards(r io.Reader, opt model.ImportArchiveOptions) error {
br := bufio.NewReader(r)
peek, err := br.Peek(len(legacyFileBegin))
if err == nil && string(peek) == legacyFileBegin {
bab, _, parseErr := a.parseBoardsAndBlocks(br, opt)
if parseErr != nil {
return parseErr
}
return validateImportedBoards(bab.Boards, opt.BoardValidator)
}

zr := zipstream.NewReader(br)
for {
hdr, err := zr.Next()
if err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return err
}

dir, filename := filepath.Split(hdr.Name)
if filename != "board.jsonl" {
continue
}

bab, _, parseErr := a.parseBoardsAndBlocks(zr, opt)
if parseErr != nil {
return fmt.Errorf("cannot import board %s: %w", path.Clean(dir), parseErr)
}
if err := validateImportedBoards(bab.Boards, opt.BoardValidator); err != nil {
return err
}
}
}

func (a *App) importArchive(r io.Reader, opt model.ImportArchiveOptions) error {
// peek at the first bytes to see if this is a legacy archive format
br := bufio.NewReader(r)
peek, err := br.Peek(len(legacyFileBegin))
Expand Down Expand Up @@ -128,6 +189,20 @@ func (a *App) ImportArchive(r io.Reader, opt model.ImportArchiveOptions) error {
}
}

// validateImportedBoards runs the optional authorization callback against every
// board parsed from an archive, aborting the import if any board is rejected.
func validateImportedBoards(boards []*model.Board, validator model.BoardValidator) error {
if validator == nil {
return nil
}
for _, board := range boards {
if err := validator(board); err != nil {
return err
}
}
return nil
}

// Update image and attachment blocks.
func (a *App) fixImagesAttachments(boardMap map[string]*model.Board, fileMap map[string]string, teamID string, userID string) {
for _, board := range boardMap {
Expand Down Expand Up @@ -170,9 +245,9 @@ func (a *App) fixImagesAttachments(boardMap map[string]*model.Board, fileMap map
}
}

// ImportBoardJSONL imports a JSONL file containing blocks for one board. The resulting
// board id is returned.
func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (*model.Board, error) {
// parseBoardsAndBlocks parses a JSONL file describing one board into its boards,
// blocks, and members without persisting anything.
func (a *App) parseBoardsAndBlocks(r io.Reader, opt model.ImportArchiveOptions) (*model.BoardsAndBlocks, []*model.BoardMember, error) {
// TODO: Stream this once `model.GenerateBlockIDs` can take a stream of blocks.
// We don't want to load the whole file in memory, even though it's a single board.
boardsAndBlocks := &model.BoardsAndBlocks{
Expand All @@ -191,7 +266,7 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (*mo
firstLine := true
for scanner.Scan() {
if lineReader.N <= 0 {
return nil, fmt.Errorf("error parsing archive line %d: %w", lineNum, errSizeLimitExceeded)
return nil, nil, fmt.Errorf("error parsing archive line %d: %w", lineNum, errSizeLimitExceeded)
}

line := bytes.TrimSpace(scanner.Bytes())
Expand All @@ -207,7 +282,7 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (*mo
if !skip {
var archiveLine model.ArchiveLine
if err := json.Unmarshal(line, &archiveLine); err != nil {
return nil, fmt.Errorf("error parsing archive line %d: %w", lineNum, err)
return nil, nil, fmt.Errorf("error parsing archive line %d: %w", lineNum, err)
}

// first line must be a board
Expand All @@ -219,40 +294,40 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (*mo
case "board":
var board model.Board
if err2 := json.Unmarshal(archiveLine.Data, &board); err2 != nil {
return nil, fmt.Errorf("invalid board in archive line %d: %w", lineNum, err2)
return nil, nil, fmt.Errorf("invalid board in archive line %d: %w", lineNum, err2)
}
board.ModifiedBy = userID
board.UpdateAt = now
board.TeamID = opt.TeamID
if err := a.validateBoardForImport(userID, opt.TeamID, &board); err != nil {
return nil, err
return nil, nil, err
}
boardsAndBlocks.Boards = append(boardsAndBlocks.Boards, &board)
boardID = board.ID
case "board_block":
// legacy archives encoded boards as blocks; we need to convert them to real boards.
var block *model.Block
if err2 := json.Unmarshal(archiveLine.Data, &block); err2 != nil {
return nil, fmt.Errorf("invalid board block in archive line %d: %w", lineNum, err2)
return nil, nil, fmt.Errorf("invalid board block in archive line %d: %w", lineNum, err2)
}
block.ModifiedBy = userID
block.UpdateAt = now
board, err := a.blockToBoard(block, opt)
if err != nil {
return nil, fmt.Errorf("cannot convert archive line %d to block: %w", lineNum, err)
return nil, nil, fmt.Errorf("cannot convert archive line %d to board: %w", lineNum, err)
}
if err := a.validateBoardForImport(userID, opt.TeamID, board); err != nil {
return nil, err
return nil, nil, err
}
boardsAndBlocks.Boards = append(boardsAndBlocks.Boards, board)
boardID = board.ID
case "block":
var block *model.Block
if err2 := json.Unmarshal(archiveLine.Data, &block); err2 != nil {
return nil, fmt.Errorf("invalid block in archive line %d: %w", lineNum, err2)
return nil, nil, fmt.Errorf("invalid block in archive line %d: %w", lineNum, err2)
}
if err := block.IsValidForImport(); err != nil {
return nil, err
return nil, nil, err
}
block.ModifiedBy = userID
block.UpdateAt = now
Expand All @@ -261,31 +336,42 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (*mo
case "boardMember":
var boardMember *model.BoardMember
if err2 := json.Unmarshal(archiveLine.Data, &boardMember); err2 != nil {
return nil, fmt.Errorf("invalid board Member in archive line %d: %w", lineNum, err2)
return nil, nil, fmt.Errorf("invalid board Member in archive line %d: %w", lineNum, err2)
}
boardMembers = append(boardMembers, boardMember)
default:
return nil, model.NewErrUnsupportedArchiveLineType(lineNum, archiveLine.Type)
return nil, nil, model.NewErrUnsupportedArchiveLineType(lineNum, archiveLine.Type)
}
firstLine = false
}
}
}

if errRead := scanner.Err(); errRead != nil {
return nil, fmt.Errorf("error reading archive line %d: %w", lineNum, errRead)
return nil, nil, fmt.Errorf("error reading archive line %d: %w", lineNum, errRead)
}

return boardsAndBlocks, boardMembers, nil
}

// ImportBoardJSONL imports a JSONL file containing blocks for one board. The resulting
// board id is returned. Board authorization is expected to have already run via
// ImportArchive; this persists the parsed board without re-validating it.
func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (*model.Board, error) {
boardsAndBlocks, boardMembers, err := a.parseBoardsAndBlocks(r, opt)
if err != nil {
return nil, err
}

// loop to remove the people how are not part of the team and system
for i := len(boardMembers) - 1; i >= 0; i-- {
if _, err := a.GetUser(boardMembers[i].UserID); err != nil {
if _, getErr := a.GetUser(boardMembers[i].UserID); getErr != nil {
boardMembers = append(boardMembers[:i], boardMembers[i+1:]...)
}
}

a.fixBoardsandBlocks(boardsAndBlocks, opt)

var err error
boardsAndBlocks, err = model.GenerateBoardsAndBlocksIDs(boardsAndBlocks, a.logger)
if err != nil {
return nil, fmt.Errorf("error generating archive block IDs: %w", err)
Expand Down
Loading
Loading