Files
vikunja/pkg/models/project_permissions_multiple_teams_test.go
kolaente a7086e5e49 fix: prevent session leaks and visibility issues in model tests
Two categories of fixes:

1. Use defer s.Close() instead of explicit s.Close() to prevent session
   leaks when require.FailNow() triggers runtime.Goexit(), which skips
   explicit close calls but runs deferred functions. Leaked sessions
   hold SQLite write locks that block all subsequent fixture loading.

2. Add s.Commit() before db.AssertExists/db.AssertMissing calls. These
   assertion helpers query via the global engine (not the test session),
   so they cannot see uncommitted data from the session's transaction.

For block-scoped sessions (kanban_task_bucket_test.go), wrap each block
in an anonymous function so defer runs at block boundary rather than
deferring to the enclosing test function.
2026-02-25 11:03:02 +01:00

703 lines
20 KiB
Go

// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-present Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package models
import (
"testing"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/user"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"xorm.io/xorm"
)
// TestProjectPermissions_OwnerInMultipleTeamsWithDifferentPermissions tests the scenario where:
// - User A creates a project (becomes owner)
// - User A is part of two teams Y and Z
// - Project is shared with team Y with admin permissions
// - Project is shared with team Z with read-only permissions
// - User A should still have owner/admin permissions (highest permission should apply)
func TestProjectPermissions_OwnerInMultipleTeamsWithDifferentPermissions(t *testing.T) {
// Setup: Load fixtures and create a database session
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
// User A (we'll use user 1 from fixtures - needs Username for GetFromAuth)
userA := &user.User{
ID: 1,
Username: "user1",
}
// Create a new project owned by User A
project := &Project{
Title: "Test Project for Multiple Teams",
Description: "Testing permissions when owner is in multiple teams",
}
err := project.Create(s, userA)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
// Verify User A has admin permissions as the owner
t.Run("owner has admin permissions before sharing", func(t *testing.T) {
s = db.NewSession()
defer s.Close()
canRead, maxPerm, err := project.CanRead(s, userA)
require.NoError(t, err)
assert.True(t, canRead, "Owner should be able to read their project")
assert.Equal(t, int(PermissionAdmin), maxPerm, "Owner should have admin permission")
canWrite, err := project.CanWrite(s, userA)
require.NoError(t, err)
assert.True(t, canWrite, "Owner should be able to write to their project")
isAdmin, err := project.IsAdmin(s, userA)
require.NoError(t, err)
assert.True(t, isAdmin, "Owner should be admin of their project")
})
prepareSharingY := func(sess *xorm.Session) {
// Create Team Y - User A will be automatically added as a member when creating the team
teamY := &Team{
Name: "Team Y",
Description: "Team with admin permissions",
}
err = teamY.Create(sess, userA)
require.NoError(t, err)
require.NoError(t, sess.Commit())
// Share project with Team Y (admin permissions)
teamProjectY := &TeamProject{
TeamID: teamY.ID,
ProjectID: project.ID,
Permission: PermissionAdmin,
}
err = teamProjectY.Create(sess, userA)
require.NoError(t, err)
require.NoError(t, sess.Commit())
}
// Setup sharing with Team Y
s = db.NewSession()
defer s.Close()
prepareSharingY(s)
// Verify User A still has admin permissions after sharing with Team Y
t.Run("owner has admin permissions after sharing with team Y (admin)", func(t *testing.T) {
s = db.NewSession()
defer s.Close()
canRead, maxPerm, err := project.CanRead(s, userA)
require.NoError(t, err)
assert.True(t, canRead, "Owner should be able to read their project after sharing with Team Y")
assert.Equal(t, int(PermissionAdmin), maxPerm, "Owner should still have admin permission after sharing with Team Y")
canWrite, err := project.CanWrite(s, userA)
require.NoError(t, err)
assert.True(t, canWrite, "Owner should be able to write to their project after sharing with Team Y")
isAdmin, err := project.IsAdmin(s, userA)
require.NoError(t, err)
assert.True(t, isAdmin, "Owner should still be admin after sharing with Team Y")
})
prepareTeamSharedMultiple := func(sess *xorm.Session) {
// Create Team Z - User A will be automatically added as a member when creating the team
teamZ := &Team{
Name: "Team Z",
Description: "Team with read-only permissions",
}
err = teamZ.Create(sess, userA)
require.NoError(t, err)
require.NoError(t, sess.Commit())
// Share project with Team Z (read-only permissions)
teamProjectZ := &TeamProject{
TeamID: teamZ.ID,
ProjectID: project.ID,
Permission: PermissionRead,
}
err = teamProjectZ.Create(sess, userA)
require.NoError(t, err)
require.NoError(t, sess.Commit())
}
// Setup sharing with Team Z
s = db.NewSession()
defer s.Close()
prepareTeamSharedMultiple(s)
// Verify User A STILL has admin permissions after sharing with Team Z
// user should retain highest permission (owner/admin), not be downgraded to read-only
t.Run("owner has admin permissions after sharing with team Z (read-only)", func(t *testing.T) {
s = db.NewSession()
defer s.Close()
canRead, maxPerm, err := project.CanRead(s, userA)
require.NoError(t, err)
assert.True(t, canRead, "Owner should be able to read their project after sharing with Team Z")
assert.Equal(t, int(PermissionAdmin), maxPerm, "Owner should STILL have admin permission (not downgraded to read-only)")
canWrite, err := project.CanWrite(s, userA)
require.NoError(t, err)
assert.True(t, canWrite, "Owner should STILL be able to write to their project (not downgraded to read-only)")
isAdmin, err := project.IsAdmin(s, userA)
require.NoError(t, err)
assert.True(t, isAdmin, "Owner should STILL be admin (not downgraded to read-only)")
})
}
// TestProjectPermissions_OwnerInMultipleTeamsAdminThenWrite tests the variant where:
// - User A creates a project
// - Project is shared with team Y with admin permissions
// - Project is shared with team Z with write permissions
// - User A should still have admin permissions (not downgraded to write)
func TestProjectPermissions_OwnerInMultipleTeamsAdminThenWrite(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
userA := &user.User{
ID: 1,
Username: "user1",
}
// Create a new project owned by User A
project := &Project{
Title: "Test Project Admin Then Write",
Description: "Testing permissions when owner is in teams with admin then write",
}
err := project.Create(s, userA)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
// Create Team Y with admin permissions - User A will be automatically added as a member
teamY := &Team{
Name: "Team Y Admin",
}
err = teamY.Create(s, userA)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
teamProjectY := &TeamProject{
TeamID: teamY.ID,
ProjectID: project.ID,
Permission: PermissionAdmin,
}
err = teamProjectY.Create(s, userA)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
// Create Team Z with write permissions - User A will be automatically added as a member
teamZ := &Team{
Name: "Team Z Write",
}
err = teamZ.Create(s, userA)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
teamProjectZ := &TeamProject{
TeamID: teamZ.ID,
ProjectID: project.ID,
Permission: PermissionWrite,
}
err = teamProjectZ.Create(s, userA)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
// Test: User A should still have admin permissions (not downgraded to write)
t.Run("owner has admin permissions after sharing with admin team then write team", func(t *testing.T) {
s = db.NewSession()
defer s.Close()
canRead, maxPerm, err := project.CanRead(s, userA)
require.NoError(t, err)
assert.True(t, canRead)
assert.Equal(t, int(PermissionAdmin), maxPerm, "Owner should have admin permission, not downgraded to write")
isAdmin, err := project.IsAdmin(s, userA)
require.NoError(t, err)
assert.True(t, isAdmin, "Owner should still be admin, not downgraded to write")
})
}
// TestProjectPermissions_NonOwnerInMultipleTeams tests the scenario where:
// - User B (not the owner) is part of two teams
// - Team Y has admin permissions on a project
// - Team Z has read-only permissions on the same project
// - User B should have admin permissions (highest of the two)
func TestProjectPermissions_NonOwnerInMultipleTeams(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
// User 1 is the owner
owner := &user.User{
ID: 1,
Username: "user1",
}
// User 2 will be in multiple teams
userB := &user.User{
ID: 2,
Username: "user2",
}
// Create a new project owned by User 1
project := &Project{
Title: "Test Project Non-Owner Multiple Teams",
Description: "Testing permissions for non-owner in multiple teams",
}
err := project.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
// Create Team Y with admin permissions
teamY := &Team{
Name: "Team Y Admin for Non-Owner",
}
err = teamY.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
// Add User B to Team Y
teamMemberY := &TeamMember{
TeamID: teamY.ID,
Username: "user2",
}
err = teamMemberY.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
// Share project with Team Y (admin)
teamProjectY := &TeamProject{
TeamID: teamY.ID,
ProjectID: project.ID,
Permission: PermissionAdmin,
}
err = teamProjectY.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
// Verify User B has admin permissions through Team Y
t.Run("non-owner has admin through team Y", func(t *testing.T) {
s = db.NewSession()
defer s.Close()
canRead, maxPerm, err := project.CanRead(s, userB)
require.NoError(t, err)
assert.True(t, canRead)
assert.Equal(t, int(PermissionAdmin), maxPerm)
isAdmin, err := project.IsAdmin(s, userB)
require.NoError(t, err)
assert.True(t, isAdmin)
})
// Create Team Z with read-only permissions
teamZ := &Team{
Name: "Team Z Read for Non-Owner",
}
err = teamZ.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
// Add User B to Team Z
teamMemberZ := &TeamMember{
TeamID: teamZ.ID,
Username: "user2",
}
err = teamMemberZ.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
// Share project with Team Z (read-only)
teamProjectZ := &TeamProject{
TeamID: teamZ.ID,
ProjectID: project.ID,
Permission: PermissionRead,
}
err = teamProjectZ.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
// Test: User B should still have admin permissions (highest of Team Y and Team Z)
t.Run("non-owner retains admin after adding to read-only team Z", func(t *testing.T) {
s = db.NewSession()
defer s.Close()
canRead, maxPerm, err := project.CanRead(s, userB)
require.NoError(t, err)
assert.True(t, canRead)
assert.Equal(t, int(PermissionAdmin), maxPerm, "Non-owner should have admin (highest permission), not downgraded to read")
isAdmin, err := project.IsAdmin(s, userB)
require.NoError(t, err)
assert.True(t, isAdmin, "Non-owner should still have admin permission, not downgraded to read")
})
}
// TestProjectPermissions_OrderOfSharingDoesNotMatter tests that the order in which
// teams are granted access doesn't affect the final permission level
func TestProjectPermissions_OrderOfSharingDoesNotMatter(t *testing.T) {
db.LoadAndAssertFixtures(t)
owner := &user.User{
ID: 1,
Username: "user1",
}
userB := &user.User{
ID: 2,
Username: "user2",
}
// Test 1: Share read-only first, then admin
t.Run("share read-only first then admin", func(t *testing.T) {
s := db.NewSession()
defer s.Close()
project := &Project{
Title: "Test Order: Read then Admin",
}
err := project.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
// Create and share with read-only team first
teamRead := &Team{Name: "Read First Team"}
err = teamRead.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
teamMemberRead := &TeamMember{TeamID: teamRead.ID, Username: "user2"}
err = teamMemberRead.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
teamProjectRead := &TeamProject{
TeamID: teamRead.ID,
ProjectID: project.ID,
Permission: PermissionRead,
}
err = teamProjectRead.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
// Now share with admin team
teamAdmin := &Team{Name: "Admin Second Team"}
err = teamAdmin.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
teamMemberAdmin := &TeamMember{TeamID: teamAdmin.ID, Username: "user2"}
err = teamMemberAdmin.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
teamProjectAdmin := &TeamProject{
TeamID: teamAdmin.ID,
ProjectID: project.ID,
Permission: PermissionAdmin,
}
err = teamProjectAdmin.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
// Verify user has admin permission
s = db.NewSession()
defer s.Close()
canRead, maxPerm, err := project.CanRead(s, userB)
require.NoError(t, err)
assert.True(t, canRead)
assert.Equal(t, int(PermissionAdmin), maxPerm, "User should have admin even though read-only was shared first")
isAdmin, err := project.IsAdmin(s, userB)
require.NoError(t, err)
assert.True(t, isAdmin)
})
// Test 2: Share admin first, then read-only (the reported bug scenario)
t.Run("share admin first then read-only - REPORTED BUG", func(t *testing.T) {
s := db.NewSession()
defer s.Close()
project := &Project{
Title: "Test Order: Admin then Read",
}
err := project.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
// Create and share with admin team first
teamAdmin := &Team{Name: "Admin First Team"}
err = teamAdmin.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
teamMemberAdmin := &TeamMember{TeamID: teamAdmin.ID, Username: "user2"}
err = teamMemberAdmin.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
teamProjectAdmin := &TeamProject{
TeamID: teamAdmin.ID,
ProjectID: project.ID,
Permission: PermissionAdmin,
}
err = teamProjectAdmin.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
// Now share with read-only team
teamRead := &Team{Name: "Read Second Team"}
err = teamRead.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
teamMemberRead := &TeamMember{TeamID: teamRead.ID, Username: "user2"}
err = teamMemberRead.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
teamProjectRead := &TeamProject{
TeamID: teamRead.ID,
ProjectID: project.ID,
Permission: PermissionRead,
}
err = teamProjectRead.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
// Verify user STILL has admin permission (not downgraded to read)
s = db.NewSession()
defer s.Close()
canRead, maxPerm, err := project.CanRead(s, userB)
require.NoError(t, err)
assert.True(t, canRead)
assert.Equal(t, int(PermissionAdmin), maxPerm, "User should STILL have admin even though read-only was shared last")
isAdmin, err := project.IsAdmin(s, userB)
require.NoError(t, err)
assert.True(t, isAdmin, "User should STILL be admin even though read-only was shared last")
})
}
// TestProjectPermissions_UserInMultipleTeamsWithSamePermission tests the edge case where:
// - A user belongs to multiple teams that all grant the same permission level
// - The effective permission should equal that permission level (not more, not less)
// - This verifies the MAX function works correctly when all permissions are equal
func TestProjectPermissions_UserInMultipleTeamsWithSamePermission(t *testing.T) {
db.LoadAndAssertFixtures(t)
owner := &user.User{
ID: 1,
Username: "user1",
}
userB := &user.User{
ID: 2,
Username: "user2",
}
// Test: User in two teams, both with read-only permission
t.Run("user in multiple teams with same read-only permission", func(t *testing.T) {
s := db.NewSession()
defer s.Close()
// Create project
project := &Project{
Title: "Test Multiple Teams Same Permission",
}
err := project.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
// Create first team with read-only permission
team1 := &Team{Name: "Team 1 Read"}
err = team1.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
teamMember1 := &TeamMember{TeamID: team1.ID, Username: "user2"}
err = teamMember1.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
teamProject1 := &TeamProject{
TeamID: team1.ID,
ProjectID: project.ID,
Permission: PermissionRead,
}
err = teamProject1.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
// Create second team with read-only permission
team2 := &Team{Name: "Team 2 Read"}
err = team2.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
teamMember2 := &TeamMember{TeamID: team2.ID, Username: "user2"}
err = teamMember2.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
teamProject2 := &TeamProject{
TeamID: team2.ID,
ProjectID: project.ID,
Permission: PermissionRead,
}
err = teamProject2.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
// Verify user has read permission (not more, not less)
s = db.NewSession()
defer s.Close()
canRead, maxPerm, err := project.CanRead(s, userB)
require.NoError(t, err)
assert.True(t, canRead, "User should be able to read")
assert.Equal(t, int(PermissionRead), maxPerm, "User should have exactly read permission when all teams grant read-only")
canWrite, err := project.CanWrite(s, userB)
require.NoError(t, err)
assert.False(t, canWrite, "User should NOT be able to write with only read permissions")
isAdmin, err := project.IsAdmin(s, userB)
require.NoError(t, err)
assert.False(t, isAdmin, "User should NOT be admin with only read permissions")
})
// Test: User in two teams, both with write permission
t.Run("user in multiple teams with same write permission", func(t *testing.T) {
s := db.NewSession()
defer s.Close()
// Create project
project := &Project{
Title: "Test Multiple Teams Same Write Permission",
}
err := project.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
// Create first team with write permission
team1 := &Team{Name: "Team 1 Write"}
err = team1.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
teamMember1 := &TeamMember{TeamID: team1.ID, Username: "user2"}
err = teamMember1.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
teamProject1 := &TeamProject{
TeamID: team1.ID,
ProjectID: project.ID,
Permission: PermissionWrite,
}
err = teamProject1.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
// Create second team with write permission
team2 := &Team{Name: "Team 2 Write"}
err = team2.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
teamMember2 := &TeamMember{TeamID: team2.ID, Username: "user2"}
err = teamMember2.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
teamProject2 := &TeamProject{
TeamID: team2.ID,
ProjectID: project.ID,
Permission: PermissionWrite,
}
err = teamProject2.Create(s, owner)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
// Verify user has write permission
s = db.NewSession()
defer s.Close()
canRead, maxPerm, err := project.CanRead(s, userB)
require.NoError(t, err)
assert.True(t, canRead, "User should be able to read")
assert.Equal(t, int(PermissionWrite), maxPerm, "User should have exactly write permission when all teams grant write")
canWrite, err := project.CanWrite(s, userB)
require.NoError(t, err)
assert.True(t, canWrite, "User should be able to write")
isAdmin, err := project.IsAdmin(s, userB)
require.NoError(t, err)
assert.False(t, isAdmin, "User should NOT be admin with only write permissions")
})
}