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
40 changes: 35 additions & 5 deletions backend/branches.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ type createBranchResponse struct {
func (p *githubPlugin) createBranch(req *plugin.Request, res *plugin.Response) {
projectID := req.Caller.ProjectID
taskID := req.PathParam("taskId")
if !p.taskBelongsToProject(taskID, projectID, res) {
return
}

type createBranchBody struct {
RepoID string `json:"repo_id"`
Expand Down Expand Up @@ -112,6 +115,9 @@ func (p *githubPlugin) createBranch(req *plugin.Request, res *plugin.Response) {
func (p *githubPlugin) linkBranchToTask(req *plugin.Request, res *plugin.Response) {
projectID := req.Caller.ProjectID
taskID := req.PathParam("taskId")
if !p.taskBelongsToProject(taskID, projectID, res) {
return
}

type linkBranchToTaskBody struct {
RepoID string `json:"repo_id"`
Expand Down Expand Up @@ -199,24 +205,48 @@ func (p *githubPlugin) linkBranchToTask(req *plugin.Request, res *plugin.Respons
// ─── GET /tasks/:taskId/github/branches ───────────────────────────────────────

func (p *githubPlugin) listTaskBranches(req *plugin.Request, res *plugin.Response) {
projectID := req.Caller.ProjectID
taskID := req.PathParam("taskId")
if !p.taskBelongsToProject(taskID, projectID, res) {
return
}

result, err := p.db.Query(`
SELECT id, task_id, repo_id, branch_name, created_at
FROM github_task_branches WHERE task_id = $1 ORDER BY created_at ASC
`, taskID)
result, err := p.db.Query(
`SELECT id, task_id, repo_id, branch_name, created_at FROM github_task_branches WHERE task_id = $1 ORDER BY created_at ASC`,
taskID,
)
if err != nil {
apiError(res, 500, "INTERNAL_ERROR", err.Error())
return
}

// github_task_branches has no project_id column of its own — only
// repo_id, which links to github_repositories (which does). Re-verify
// each branch's repo against the caller's project as defense-in-depth:
// taskBelongsToProject above already closes the main vector (a foreign
// taskId), but this also protects against any row a pre-fix caller
// might have already linked across projects. No SQL JOIN here (kept
// consistent with resolvePRForTask's style elsewhere in this plugin,
// which also resolves through separate single-table queries).
items := make([]taskBranchResponse, 0, len(result.Rows))
for _, row := range result.Rows {
sc := newRowScanner(result.Columns, row)
repoID := sc.str("repo_id")
repoResult, rErr := p.db.Query(
`SELECT id FROM github_repositories WHERE id = $1 AND project_id = $2`,
repoID, projectID,
)
if rErr != nil {
apiError(res, 500, "INTERNAL_ERROR", rErr.Error())
return
}
if len(repoResult.Rows) == 0 {
continue
}
items = append(items, taskBranchResponse{
ID: sc.str("id"),
TaskID: sc.str("task_id"),
RepoID: sc.str("repo_id"),
RepoID: repoID,
BranchName: sc.str("branch_name"),
CreatedAt: sc.str("created_at"),
})
Expand Down
24 changes: 24 additions & 0 deletions backend/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,30 @@ func apiError(res *plugin.Response, code int, errCode, message string) {
})
}

// taskBelongsToProject verifies taskID exists, is not deleted, and belongs
// to projectID — writing a 404 and returning false otherwise. Every handler
// that accepts a :taskId path param and uses it to read or write
// project-scoped GitHub data (PRs, branches) must call this first: the
// host's route-level permission check only verifies the caller belongs to
// the project in the URL, it has no way to also verify an arbitrary path
// param like :taskId belongs to that same project — that's this plugin's
// job, the same role resolvePRForTask plays for PR-specific resources.
func (p *githubPlugin) taskBelongsToProject(taskID, projectID string, res *plugin.Response) bool {
result, err := p.db.Query(
`SELECT id FROM tasks WHERE id = $1 AND project_id = $2 AND deleted_at IS NULL`,
taskID, projectID,
)
if err != nil {
apiError(res, 500, "INTERNAL_ERROR", err.Error())
return false
}
if len(result.Rows) == 0 {
apiError(res, 404, "TASK_NOT_FOUND", "Task not found")
return false
}
return true
}

// ─── event handlers ──────────────────────────────────────────────────────────

// handleTaskDeleted cleans up branches and PR links when a task is deleted.
Expand Down
Loading
Loading