[v16.0/forgejo] fix: make assignment of projects to issues consistent (#13504)

**Backport:** https://codeberg.org/forgejo/forgejo/pulls/13066

A partial backport of the `tests/forgery` changes from `feat: make delete repository transcation READ COMMITTED (#12856)` is included and makes it a clean cherry-pick.

- Move the permissions and consistency logic to services/context/project.go
- When an invalid `project=id` is provided in the query string to display the form to create a new issue, it will now fail instead of silently discard the invalid id

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/13504
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
This commit is contained in:
limiting-factor 2026-07-17 23:29:06 +02:00 committed by limiting-factor
commit d3db2674b9
6 changed files with 516 additions and 125 deletions

View file

@ -995,17 +995,10 @@ func NewIssue(ctx *context.Context) {
}
projectID := ctx.FormInt64("project")
if projectID > 0 && (isProjectsEnabled || isOwnerProjectsEnabled) {
project, err := project_model.GetProjectByID(ctx, projectID)
if err != nil {
log.Error("GetProjectByID: %d: %v", projectID, err)
} else if !project.CanBeAccessedByOwnerRepo(ctx.Repo.Repository.OwnerID, ctx.Repo.Repository) {
log.Error("GetProjectByID: %d: %v", projectID,
fmt.Errorf("project[%d] neither in repo[%d] nor has the same owner (project: [%d] ./. repo: [%d])",
project.ID, ctx.Repo.Repository.ID, project.OwnerID, ctx.Repo.Repository.OwnerID))
} else {
ctx.Data["project_id"] = projectID
ctx.Data["Project"] = project
if projectID > 0 {
context.ReqProjectIDAssignableToIssueAndSetData(ctx, projectID)
if ctx.Written() {
return
}
if len(ctx.Req.URL.Query().Get("project")) > 0 {
@ -1168,18 +1161,10 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull
}
if form.ProjectID > 0 {
p, err := project_model.GetProjectByID(ctx, form.ProjectID)
if err != nil {
ctx.ServerError("GetProjectByID", err)
context.ReqProjectIDAssignableToIssueAndSetData(ctx, form.ProjectID)
if ctx.Written() {
return nil, nil, 0, 0
}
if p.RepoID != ctx.Repo.Repository.ID && p.OwnerID != ctx.Repo.Repository.OwnerID {
ctx.NotFound("", nil)
return nil, nil, 0, 0
}
ctx.Data["Project"] = p
ctx.Data["project_id"] = form.ProjectID
}
// Check assignees
@ -1219,6 +1204,17 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull
return labelIDs, assigneeIDs, milestoneID, form.ProjectID
}
func updateIssueProject(ctx *context.Context, issue *issues_model.Issue, projectID int64) {
context.ReqProjectIDAssignableToIssue(ctx, projectID)
if ctx.Written() {
return
}
if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, projectID, 0); err != nil {
ctx.ServerError("IssueAssignOrRemoveProject", err)
return
}
}
// NewIssuePost response for creating new issue
func NewIssuePost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateIssueForm)
@ -1288,13 +1284,8 @@ func NewIssuePost(ctx *context.Context) {
}
if projectID > 0 {
if !ctx.Repo.CanRead(unit.TypeProjects) || (ctx.ContextUser.IsOrganization() && !ctx.Org.CanReadUnit(ctx, unit.TypeProjects)) {
// User must also be able to see the project.
ctx.Error(http.StatusForbidden, "user doesn't have permissions to read projects")
return
}
if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, projectID, 0); err != nil {
ctx.ServerError("IssueAssignOrRemoveProject", err)
updateIssueProject(ctx, issue, projectID)
if ctx.Written() {
return
}
}

View file

@ -1680,12 +1680,10 @@ func CompareAndPullRequestPost(ctx *context.Context) {
return
}
if projectID > 0 && ctx.Repo.CanWrite(unit.TypeProjects) {
if err := issues_model.IssueAssignOrRemoveProject(ctx, pullIssue, ctx.Doer, projectID, 0); err != nil {
if !errors.Is(err, util.ErrPermissionDenied) {
ctx.ServerError("IssueAssignOrRemoveProject", err)
return
}
if projectID > 0 {
updateIssueProject(ctx, pullIssue, projectID)
if ctx.Written() {
return
}
}

View file

@ -1005,26 +1005,9 @@ func registerRoutes(m *web.Route) {
}
reqRepoOrOwnerProjectReader := func(ctx *context.Context) {
unitType := unit.TypeProjects
if ctx.ContextUser == nil || ctx.Doer == nil {
ctx.NotFound(unitType.String(), nil)
return
if projectID := ctx.FormInt64("id"); projectID > 0 {
context.ReqProjectIDAssignableToIssue(ctx, projectID)
}
switch {
case ctx.ContextUser.IsIndividual():
if ctx.Doer.ID == ctx.ContextUser.ID || ctx.Doer.IsAdmin {
return
}
case ctx.ContextUser.IsOrganization():
if ctx.Org.Organization.UnitPermission(ctx, ctx.Doer, unitType) >= perm.AccessModeRead {
return
}
default:
ctx.NotFound(unitType.String(), nil)
return
}
reqRepoProjectsReader(ctx)
}
individualPermsChecker := func(ctx *context.Context) {

View file

@ -0,0 +1,96 @@
// Copyright 2026 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later
package context
import (
"fmt"
"net/http"
project_model "forgejo.org/models/project"
"forgejo.org/models/unit"
"forgejo.org/modules/log"
)
func ReqProjectIDAssignableToIssueAndSetData(ctx *Context, projectID int64) {
project := getProjectID(ctx, projectID)
if ctx.Written() {
return
}
reqProjectAssignableToIssue(ctx, project)
if ctx.Written() {
return
}
ctx.Data["Project"] = project
ctx.Data["project_id"] = project.ID
}
func ReqProjectIDAssignableToIssue(ctx *Context, projectID int64) {
project := getProjectID(ctx, projectID)
if ctx.Written() {
return
}
reqProjectAssignableToIssue(ctx, project)
}
func getProjectID(ctx *Context, projectID int64) *project_model.Project {
project, err := project_model.GetProjectByID(ctx, projectID)
if err != nil {
if project_model.IsErrProjectNotExist(err) {
ctx.NotFound(fmt.Sprintf("project %d is not found", projectID), nil)
return nil
}
log.Error("project_model.GetProjectByID(%d): %v", projectID, err)
ctx.ServerError(fmt.Sprintf("project_model.GetProjectByID(%d)", projectID), err)
return nil
}
return project
}
func reqProjectAssignableToIssue(ctx *Context, project *project_model.Project) {
reqValidAndConsistentProject(ctx, project)
if ctx.Written() {
return
}
reqPermissionToAssignProjectToIssue(ctx, project.Type)
}
func reqValidAndConsistentProject(ctx *Context, project *project_model.Project) {
if project.RepoID != ctx.Repo.Repository.ID && project.OwnerID != ctx.Repo.Repository.OwnerID {
ctx.NotFound(fmt.Sprintf("project %d does not belong", project.ID), nil)
}
}
func reqPermissionToAssignProjectToIssue(ctx *Context, projectType project_model.Type) {
switch projectType {
case project_model.TypeRepository:
if !ctx.Repo.Repository.UnitEnabled(ctx, unit.TypeProjects) {
ctx.NotFound("repository projects are disabled", nil)
return
}
if !ctx.Repo.CanRead(unit.TypeProjects) {
ctx.Error(http.StatusForbidden, "doesn't have permissions to read repository projects")
return
}
if !ctx.Repo.CanWrite(unit.TypeIssues) {
ctx.Error(http.StatusForbidden, "doesn't have permissions to write repository issues")
return
}
case project_model.TypeOrganization:
if !ctx.Org.CanReadUnit(ctx, unit.TypeProjects) {
ctx.Error(http.StatusForbidden, "doesn't have permissions to read the owner projects")
return
}
if !ctx.Org.CanWriteUnit(ctx, unit.TypeIssues) {
ctx.Error(http.StatusForbidden, "doesn't have permissions to write the repository issues and set the project")
return
}
case project_model.TypeIndividual:
if !ctx.Repo.CanWrite(unit.TypeIssues) {
ctx.Error(http.StatusForbidden, "doesn't have permissions to write the repository issues and set the project")
return
}
default:
ctx.ServerError(fmt.Sprintf("unexpected project type %v", projectType), nil)
}
}

View file

@ -8,10 +8,13 @@ import (
"io/fs"
"testing"
"forgejo.org/models/db"
"forgejo.org/models/perm"
repo_model "forgejo.org/models/repo"
unit_model "forgejo.org/models/unit"
user_model "forgejo.org/models/user"
"forgejo.org/modules/git"
repo_module "forgejo.org/modules/repository"
repo_service "forgejo.org/services/repository"
wiki_service "forgejo.org/services/wiki"
@ -33,6 +36,8 @@ type CreateRepositoryOptions struct {
LatestSha *string // if not nil, the commit sha after initializing the repo with the Files will be written to this ref
SkipCleanup bool // if true the repo will not be deleted at the end of the test (can be useful to debug locally)
Collaborators map[*user_model.User]perm.AccessMode
}
// FilesInit specifies the templates to use upon repository initialization.
@ -88,7 +93,7 @@ func CreateRepository(t testing.TB, owner *user_model.User, opts *CreateReposito
require.NoError(t, err)
if !opts.SkipCleanup {
t.Cleanup(func() {
_ = repo_service.DeleteRepository(t.Context(), owner, repo, false)
_ = repo_service.DeleteRepository(db.DefaultContext, owner, repo, false)
})
}
require.NotEmpty(t, repo)
@ -106,6 +111,11 @@ func CreateRepository(t testing.TB, owner *user_model.User, opts *CreateReposito
}
repo.Owner = owner
for user, mode := range opts.Collaborators {
require.NoError(t, repo_module.AddCollaborator(t.Context(), repo, user))
require.NoError(t, repo_model.ChangeCollaborationAccessMode(t.Context(), repo, user.ID, mode))
}
return repo
}

View file

@ -7,6 +7,7 @@ package integration
import (
"fmt"
"net/http"
"net/http/httptest"
"path"
"strconv"
"strings"
@ -14,14 +15,15 @@ import (
"forgejo.org/models/db"
issues_model "forgejo.org/models/issues"
org_model "forgejo.org/models/organization"
"forgejo.org/models/perm"
project_model "forgejo.org/models/project"
repo_model "forgejo.org/models/repo"
unit_model "forgejo.org/models/unit"
"forgejo.org/models/unittest"
user_model "forgejo.org/models/user"
repo_service "forgejo.org/services/repository"
"forgejo.org/modules/test"
"forgejo.org/tests"
"forgejo.org/tests/forgery"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -169,16 +171,40 @@ func TestChangeStatusProject(t *testing.T) {
})
}
func TestAssignProject(t *testing.T) {
defer unittest.OverrideFixtures("tests/integration/fixtures/TestAssignProject/")()
func TestProjectPermissionsAndConsistency(t *testing.T) {
defer tests.PrepareTestEnv(t)()
ctx := t.Context()
newTestIssue := func(t *testing.T, session *TestSession, owner *user_model.User, repo *repo_model.Repository) (*issues_model.Issue, string, string) {
newTestIssue := func(t *testing.T, session *TestSession, repo *repo_model.Repository, project *project_model.Project, expectedStatus int) *httptest.ResponseRecorder {
t.Helper()
issueURL := testNewIssue(t, session, owner.Name, repo.Name, "Hello", "World")
req := NewRequest(t, "GET", path.Join(repo.FullName(), "issues", "new"))
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
link, exists := htmlDoc.doc.Find("#new-issue").Attr("action")
require.True(t, exists, "The template has changed")
payload := map[string]string{
"title": "Hello",
"content": "World",
}
if project != nil {
payload["project_id"] = strconv.FormatInt(project.ID, 10)
}
req = NewRequestWithValues(t, "POST", link, payload)
return session.MakeRequest(t, req, expectedStatus)
}
newTestIssueSuccess := func(t *testing.T, session *TestSession, repo *repo_model.Repository, project *project_model.Project) *issues_model.Issue {
t.Helper()
resp := newTestIssue(t, session, repo, project, http.StatusOK)
issueURL := test.RedirectURL(resp)
indexStr := issueURL[strings.LastIndexByte(issueURL, '/')+1:]
index, err := strconv.Atoi(indexStr)
require.NoError(t, err, "Invalid issue href: %s", issueURL)
@ -186,99 +212,386 @@ func TestAssignProject(t *testing.T) {
issue := &issues_model.Issue{RepoID: repo.ID, Index: int64(index)}
unittest.AssertExistsAndLoadBean(t, issue)
issueID := strconv.FormatInt(issue.ID, 10)
return issue, indexStr, issueID
if project != nil {
require.NoError(t, issue.LoadProject(ctx))
require.NotNil(t, issue.Project)
require.Equal(t, project.ID, issue.Project.ID)
}
return issue
}
updateIssueProject := func(t *testing.T, session *TestSession, projectID, issueID, owner, repo string, expectedStatus int) {
updateIssueProject := func(t *testing.T, session *TestSession, repo *repo_model.Repository, project *project_model.Project, issue *issues_model.Issue, expectedStatus int) {
t.Helper()
req := NewRequestWithValues(t, "POST", path.Join(owner, repo, "issues", "projects"), map[string]string{
"issue_ids": issueID,
"id": projectID,
req := NewRequestWithValues(t, "POST", path.Join(repo.FullName(), "issues", "projects"), map[string]string{
"issue_ids": strconv.FormatInt(issue.ID, 10),
"id": strconv.FormatInt(project.ID, 10),
})
session.MakeRequest(t, req, expectedStatus)
if expectedStatus == http.StatusOK {
issue := &issues_model.Issue{ID: issue.ID}
unittest.AssertExistsAndLoadBean(t, issue)
issue.LoadProject(ctx)
require.Equal(t, project.ID, issue.Project.ID)
}
}
// User
t.Run("UserProjectOn+RepoProjectOff", func(tt *testing.T) {
defer tests.PrintCurrentTest(tt)()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
clearIssueProject := func(t *testing.T, session *TestSession, repo *repo_model.Repository, issue *issues_model.Issue) {
t.Helper()
session := loginUser(tt, user.Name)
issue, _, issueID := newTestIssue(tt, session, user, repo)
req := NewRequestWithValues(t, "POST", path.Join(repo.FullName(), "issues", "projects"), map[string]string{
"issue_ids": strconv.FormatInt(issue.ID, 10),
})
session.MakeRequest(t, req, http.StatusOK)
}
updateIssueProject(tt, session, "1003", issueID, user.Name, repo.Name, http.StatusOK)
require.NoError(tt, issue.LoadProject(db.DefaultContext))
require.NotNil(tt, issue.Project)
require.Equal(tt, int64(1003), issue.Project.ID)
t.Run("New issue with project ID in query string", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
unittest.LoadFixtures()
getNewIssue := func(t *testing.T, session *TestSession, repo *repo_model.Repository, projectID int64, expectedStatus int) *httptest.ResponseRecorder {
t.Helper()
req := NewRequest(t, "GET", fmt.Sprintf("%s?project=%d", path.Join(repo.FullName(), "issues", "new"), projectID))
return session.MakeRequest(t, req, expectedStatus)
}
t.Run("does not exist anywhere", func(t *testing.T) {
owner := forgery.CreateUser(t, nil)
doer := owner
repo := forgery.CreateRepository(t, owner, nil)
invalidProjectID := int64(4234243)
session := loginUser(t, doer.Name)
resp := getNewIssue(t, session, repo, invalidProjectID, http.StatusNotFound)
assert.Contains(t, resp.Body.String(), "Not found.")
})
t.Run("is a valid repository project", func(t *testing.T) {
owner := forgery.CreateUser(t, nil)
doer := owner
repo := forgery.CreateRepository(t, owner, nil)
project := forgery.CreateProject(t, repo, nil)
session := loginUser(t, doer.Name)
getNewIssue(t, session, repo, project.ID, http.StatusOK)
})
t.Run("is invalid because it is a repository project that belongs to a different repository", func(t *testing.T) {
owner := forgery.CreateUser(t, nil)
doer := owner
repo := forgery.CreateRepository(t, owner, nil)
otherRepo := forgery.CreateRepository(t, owner, nil)
projectFromOtherRepo := forgery.CreateProject(t, otherRepo, nil)
session := loginUser(t, doer.Name)
resp := getNewIssue(t, session, repo, projectFromOtherRepo.ID, http.StatusNotFound)
assert.Contains(t, resp.Body.String(), "Not found.")
})
t.Run("is a valid owner project", func(t *testing.T) {
user := forgery.CreateUser(t, nil)
repo := forgery.CreateRepository(t, user, nil)
doer := user
project := forgery.CreateProject(t, user, nil)
forgery.DisableRepoUnits(t, repo, unit_model.TypeProjects)
session := loginUser(t, doer.Name)
getNewIssue(t, session, repo, project.ID, http.StatusOK)
})
t.Run("is invalid because it is an owner project that belongs to a different owner", func(t *testing.T) {
user := forgery.CreateUser(t, nil)
repo := forgery.CreateRepository(t, user, nil)
doer := user
otherUser := forgery.CreateUser(t, nil)
projectFromOtherUser := forgery.CreateProject(t, otherUser, nil)
session := loginUser(t, doer.Name)
resp := getNewIssue(t, session, repo, projectFromOtherUser.ID, http.StatusNotFound)
assert.Contains(t, resp.Body.String(), "Not found.")
})
})
// Team 1001 - enabled project unit
team := unittest.AssertExistsAndLoadBean(t, &org_model.Team{ID: 1001})
require.NoError(t, team.LoadMembers(ctx))
require.NoError(t, team.LoadRepositories(ctx))
t.Run("Project ID", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
unittest.LoadFixtures()
user := team.Members[0]
repo := team.Repos[0]
org := team.GetOrg(ctx)
t.Run("does not exist anywhere", func(t *testing.T) {
owner := forgery.CreateUser(t, nil)
doer := owner
session := loginUser(t, user.Name)
repo := forgery.CreateRepository(t, owner, nil)
invalidProject := &project_model.Project{ID: 4234243}
session := loginUser(t, doer.Name)
newTestIssue(t, session, repo, invalidProject, http.StatusNotFound)
issue := newTestIssueSuccess(t, session, repo, nil)
t.Run("OrgProjectOn+RepoProjectOn", func(tt *testing.T) {
defer tests.PrintCurrentTest(tt)()
issue, _, issueID := newTestIssue(tt, session, org.AsUser(), repo)
updateIssueProject(t, session, repo, invalidProject, issue, http.StatusNotFound)
})
updateIssueProject(tt, session, "1001", issueID, org.Name, repo.Name, http.StatusOK)
t.Run("is a valid repository project", func(t *testing.T) {
owner := forgery.CreateUser(t, nil)
doer := owner
require.NoError(tt, issue.LoadProject(db.DefaultContext))
require.NotNil(tt, issue.Project)
require.Equal(tt, int64(1001), issue.Project.ID)
repo := forgery.CreateRepository(t, owner, nil)
projectA := forgery.CreateProject(t, repo, nil)
session := loginUser(t, doer.Name)
issue := newTestIssueSuccess(t, session, repo, projectA)
projectB := forgery.CreateProject(t, repo, nil)
updateIssueProject(t, session, repo, projectB, issue, http.StatusOK)
clearIssueProject(t, session, repo, issue)
})
t.Run("is invalid because it is a repository project that belongs to a different repository", func(t *testing.T) {
owner := forgery.CreateUser(t, nil)
doer := owner
repo := forgery.CreateRepository(t, owner, nil)
otherRepo := forgery.CreateRepository(t, owner, nil)
projectFromOtherRepo := forgery.CreateProject(t, otherRepo, nil)
session := loginUser(t, doer.Name)
newTestIssue(t, session, repo, projectFromOtherRepo, http.StatusNotFound)
issue := newTestIssueSuccess(t, session, repo, nil)
updateIssueProject(t, session, repo, projectFromOtherRepo, issue, http.StatusNotFound)
})
t.Run("is a valid owner project", func(t *testing.T) {
user := forgery.CreateUser(t, nil)
repo := forgery.CreateRepository(t, user, nil)
doer := user
projectA := forgery.CreateProject(t, user, nil)
forgery.DisableRepoUnits(t, repo, unit_model.TypeProjects)
session := loginUser(t, doer.Name)
issue := newTestIssueSuccess(t, session, repo, projectA)
projectB := forgery.CreateProject(t, user, nil)
updateIssueProject(t, session, repo, projectB, issue, http.StatusOK)
clearIssueProject(t, session, repo, issue)
})
t.Run("is invalid because it is an owner project that belongs to a different owner", func(t *testing.T) {
user := forgery.CreateUser(t, nil)
repo := forgery.CreateRepository(t, user, nil)
doer := user
otherUser := forgery.CreateUser(t, nil)
projectFromOtherUser := forgery.CreateProject(t, otherUser, nil)
session := loginUser(t, doer.Name)
newTestIssue(t, session, repo, projectFromOtherUser, http.StatusNotFound)
issue := newTestIssueSuccess(t, session, repo, nil)
updateIssueProject(t, session, repo, projectFromOtherUser, issue, http.StatusNotFound)
})
})
// Disable repository project unit
require.NoError(t, repo_service.UpdateRepositoryUnits(ctx, repo, nil, []unit_model.Type{unit_model.TypeProjects}))
t.Run("OrgProjectOn+RepoProjectOff", func(tt *testing.T) {
defer tests.PrintCurrentTest(tt)()
issue, _, issueID := newTestIssue(tt, session, org.AsUser(), repo)
t.Run("Repository project", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
unittest.LoadFixtures()
updateIssueProject(tt, session, "1001", issueID, org.Name, repo.Name, http.StatusOK)
require.NoError(tt, issue.LoadProject(db.DefaultContext))
require.NotNil(tt, issue.Project)
require.Equal(tt, int64(1001), issue.Project.ID)
t.Run("doer is owner", func(t *testing.T) {
owner := forgery.CreateUser(t, nil)
doer := owner
repo := forgery.CreateRepository(t, owner, nil)
projectA := forgery.CreateProject(t, repo, nil)
session := loginUser(t, doer.Name)
issue := newTestIssueSuccess(t, session, repo, projectA)
projectB := forgery.CreateProject(t, repo, nil)
updateIssueProject(t, session, repo, projectB, issue, http.StatusOK)
clearIssueProject(t, session, repo, issue)
})
t.Run("doer is owner but the projects unit is disabled", func(t *testing.T) {
owner := forgery.CreateUser(t, nil)
doer := owner
repo := forgery.CreateRepository(t, owner, nil)
project := forgery.CreateProject(t, repo, nil)
forgery.DisableRepoUnits(t, repo, unit_model.TypeProjects)
session := loginUser(t, doer.Name)
newTestIssue(t, session, repo, project, http.StatusNotFound)
issue := newTestIssueSuccess(t, session, repo, nil)
updateIssueProject(t, session, repo, project, issue, http.StatusNotFound)
})
t.Run("doer is collaborator with write permissions", func(t *testing.T) {
doer := forgery.CreateUser(t, nil)
user := forgery.CreateUser(t, nil)
repo := forgery.CreateRepository(t, user, &forgery.CreateRepositoryOptions{
Collaborators: map[*user_model.User]perm.AccessMode{doer: perm.AccessModeWrite},
})
projectA := forgery.CreateProject(t, repo, nil)
session := loginUser(t, doer.Name)
issue := newTestIssueSuccess(t, session, repo, projectA)
projectB := forgery.CreateProject(t, repo, nil)
updateIssueProject(t, session, repo, projectB, issue, http.StatusOK)
clearIssueProject(t, session, repo, issue)
})
t.Run("doer is collaborator with read permissions", func(t *testing.T) {
doer := forgery.CreateUser(t, nil)
user := forgery.CreateUser(t, nil)
repo := forgery.CreateRepository(t, user, &forgery.CreateRepositoryOptions{
Collaborators: map[*user_model.User]perm.AccessMode{doer: perm.AccessModeRead},
})
project := forgery.CreateProject(t, repo, nil)
session := loginUser(t, doer.Name)
newTestIssue(t, session, repo, project, http.StatusForbidden)
issue := newTestIssueSuccess(t, session, repo, nil)
updateIssueProject(t, session, repo, project, issue, http.StatusNotFound)
})
})
// Team 1002 - disabled project unit
team = unittest.AssertExistsAndLoadBean(t, &org_model.Team{ID: 1002})
require.NoError(t, team.LoadMembers(ctx))
require.NoError(t, team.LoadRepositories(ctx))
t.Run("Organization project", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
unittest.LoadFixtures()
user = team.Members[0]
repo = team.Repos[0]
org = team.GetOrg(ctx)
t.Run("doer is the organization owner", func(t *testing.T) {
owner := forgery.CreateUser(t, nil)
doer := owner
org := forgery.CreateOrganisation(t, owner)
session = loginUser(t, user.Name)
repo := forgery.CreateRepository(t, org.AsUser(), nil)
projectA := forgery.CreateProject(t, org, nil)
forgery.DisableRepoUnits(t, repo, unit_model.TypeProjects)
session := loginUser(t, doer.Name)
issue := newTestIssueSuccess(t, session, repo, projectA)
t.Run("OrgProjectOff+RepoProjectOn", func(tt *testing.T) {
defer tests.PrintCurrentTest(tt)()
issue, _, issueID := newTestIssue(tt, session, org.AsUser(), repo)
projectB := forgery.CreateProject(t, org, nil)
updateIssueProject(t, session, repo, projectB, issue, http.StatusOK)
clearIssueProject(t, session, repo, issue)
})
updateIssueProject(tt, session, "1002", issueID, org.Name, repo.Name, http.StatusOK)
require.NoError(tt, issue.LoadProject(db.DefaultContext))
require.NotNil(tt, issue.Project)
require.Equal(tt, int64(1002), issue.Project.ID)
t.Run("doer in team with write permissions", func(t *testing.T) {
doer := forgery.CreateUser(t, nil)
owner := forgery.CreateUser(t, nil)
org := forgery.CreateOrganisation(t, owner)
forgery.CreateTeam(t, org, &forgery.CreateTeamOptions{
Mode: perm.AccessModeWrite,
Members: []*user_model.User{doer},
})
repo := forgery.CreateRepository(t, org.AsUser(), nil)
projectA := forgery.CreateProject(t, org, nil)
forgery.DisableRepoUnits(t, repo, unit_model.TypeProjects)
session := loginUser(t, doer.Name)
issue := newTestIssueSuccess(t, session, repo, projectA)
projectB := forgery.CreateProject(t, org, nil)
updateIssueProject(t, session, repo, projectB, issue, http.StatusOK)
clearIssueProject(t, session, repo, issue)
})
t.Run("doer in a team with read permissions", func(t *testing.T) {
doer := forgery.CreateUser(t, nil)
org := forgery.CreateOrganisation(t, nil)
forgery.CreateTeam(t, org, &forgery.CreateTeamOptions{
Mode: perm.AccessModeRead,
Members: []*user_model.User{doer},
})
repo := forgery.CreateRepository(t, org.AsUser(), nil)
project := forgery.CreateProject(t, org, nil)
forgery.DisableRepoUnits(t, repo, unit_model.TypeProjects)
session := loginUser(t, doer.Name)
newTestIssue(t, session, repo, project, http.StatusForbidden)
issue := newTestIssueSuccess(t, session, repo, nil)
updateIssueProject(t, session, repo, project, issue, http.StatusNotFound)
})
t.Run("doer not in any team", func(t *testing.T) {
doer := forgery.CreateUser(t, nil)
org := forgery.CreateOrganisation(t, nil)
repo := forgery.CreateRepository(t, org.AsUser(), nil)
project := forgery.CreateProject(t, org, nil)
forgery.DisableRepoUnits(t, repo, unit_model.TypeProjects)
session := loginUser(t, doer.Name)
newTestIssue(t, session, repo, project, http.StatusForbidden)
issue := newTestIssueSuccess(t, session, repo, nil)
updateIssueProject(t, session, repo, project, issue, http.StatusNotFound)
})
})
// Disable repository project unit
require.NoError(t, repo_service.UpdateRepositoryUnits(ctx, repo, nil, []unit_model.Type{unit_model.TypeProjects}))
t.Run("OrgProjectOff+RepoProjectOff", func(tt *testing.T) {
defer tests.PrintCurrentTest(tt)()
issue, _, issueID := newTestIssue(tt, session, org.AsUser(), repo)
t.Run("User project", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
unittest.LoadFixtures()
updateIssueProject(tt, session, "1002", issueID, org.Name, repo.Name, http.StatusOK)
require.NoError(tt, issue.LoadProject(db.DefaultContext))
require.NotNil(tt, issue.Project)
require.Equal(tt, int64(1002), issue.Project.ID)
t.Run("doer is owner", func(t *testing.T) {
user := forgery.CreateUser(t, nil)
repo := forgery.CreateRepository(t, user, nil)
doer := user
projectA := forgery.CreateProject(t, user, nil)
forgery.DisableRepoUnits(t, repo, unit_model.TypeProjects)
session := loginUser(t, doer.Name)
issue := newTestIssueSuccess(t, session, repo, projectA)
projectB := forgery.CreateProject(t, user, nil)
updateIssueProject(t, session, repo, projectB, issue, http.StatusOK)
clearIssueProject(t, session, repo, issue)
})
t.Run("doer is collaborator with write permissions", func(t *testing.T) {
doer := forgery.CreateUser(t, nil)
user := forgery.CreateUser(t, nil)
repo := forgery.CreateRepository(t, user, &forgery.CreateRepositoryOptions{
Collaborators: map[*user_model.User]perm.AccessMode{doer: perm.AccessModeWrite},
})
projectA := forgery.CreateProject(t, user, nil)
forgery.DisableRepoUnits(t, repo, unit_model.TypeProjects)
session := loginUser(t, doer.Name)
issue := newTestIssueSuccess(t, session, repo, projectA)
projectB := forgery.CreateProject(t, user, nil)
updateIssueProject(t, session, repo, projectB, issue, http.StatusOK)
clearIssueProject(t, session, repo, issue)
})
t.Run("doer is collaborator with read permissions", func(t *testing.T) {
doer := forgery.CreateUser(t, nil)
user := forgery.CreateUser(t, nil)
repo := forgery.CreateRepository(t, user, &forgery.CreateRepositoryOptions{
Collaborators: map[*user_model.User]perm.AccessMode{doer: perm.AccessModeRead},
})
project := forgery.CreateProject(t, user, nil)
forgery.DisableRepoUnits(t, repo, unit_model.TypeProjects)
session := loginUser(t, doer.Name)
newTestIssue(t, session, repo, project, http.StatusForbidden)
issue := newTestIssueSuccess(t, session, repo, nil)
updateIssueProject(t, session, repo, project, issue, http.StatusNotFound)
})
t.Run("doer is not a collaborator or owner", func(t *testing.T) {
doer := forgery.CreateUser(t, nil)
user := forgery.CreateUser(t, nil)
repo := forgery.CreateRepository(t, user, nil)
project := forgery.CreateProject(t, user, nil)
forgery.DisableRepoUnits(t, repo, unit_model.TypeProjects)
session := loginUser(t, doer.Name)
newTestIssue(t, session, repo, project, http.StatusForbidden)
issue := newTestIssueSuccess(t, session, repo, nil)
updateIssueProject(t, session, repo, project, issue, http.StatusNotFound)
})
})
}