feat(permissions): site admins bypass all Can* checks (license-gated)
This commit is contained in:
parent
7c7e060d16
commit
87a06d6cb9
|
|
@ -0,0 +1,44 @@
|
|||
// 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 (
|
||||
"code.vikunja.io/api/pkg/license"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/api/pkg/web"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// isInstanceAdmin gates cross-user access on both is_admin and the admin-panel
|
||||
// license so flipping is_admin on a free instance cannot recover the paid bypass.
|
||||
// is_admin is re-read from the DB because the auth's flag is claim-derived and
|
||||
// stale until the JWT expires.
|
||||
func isInstanceAdmin(s *xorm.Session, a web.Auth) bool {
|
||||
if !license.IsFeatureEnabled(license.FeatureAdminPanel) {
|
||||
return false
|
||||
}
|
||||
u, ok := a.(*user.User)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
fresh, err := user.GetUserByID(s, u.ID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return fresh.IsAdmin
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
// 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/license"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAdminBypass_Project(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
license.SetForTests([]license.Feature{license.FeatureAdminPanel})
|
||||
defer license.ResetForTests()
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
_, err := s.ID(int64(2)).Cols("is_admin").Update(&user.User{IsAdmin: true})
|
||||
require.NoError(t, err)
|
||||
|
||||
admin := &user.User{ID: 2, IsAdmin: true}
|
||||
p := &Project{ID: 1}
|
||||
|
||||
t.Run("CanRead", func(t *testing.T) {
|
||||
can, _, err := p.CanRead(s, admin)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, can, "admin must be able to read any project")
|
||||
})
|
||||
|
||||
t.Run("CanWrite", func(t *testing.T) {
|
||||
can, err := p.CanWrite(s, admin)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, can)
|
||||
})
|
||||
|
||||
t.Run("CanUpdate", func(t *testing.T) {
|
||||
can, err := p.CanUpdate(s, admin)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, can)
|
||||
})
|
||||
|
||||
t.Run("CanDelete", func(t *testing.T) {
|
||||
can, err := p.CanDelete(s, admin)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, can)
|
||||
})
|
||||
}
|
||||
|
||||
// Without the admin-panel license, flipping is_admin must not recover the paid bypass.
|
||||
func TestAdminBypass_Project_LicenseInactive(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
license.ResetForTests()
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
_, err := s.ID(int64(2)).Cols("is_admin").Update(&user.User{IsAdmin: true})
|
||||
require.NoError(t, err)
|
||||
|
||||
admin := &user.User{ID: 2, IsAdmin: true}
|
||||
p := &Project{ID: 1}
|
||||
|
||||
t.Run("CanRead", func(t *testing.T) {
|
||||
can, _, err := p.CanRead(s, admin)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, can, "unlicensed admin must not read another user's project")
|
||||
})
|
||||
|
||||
t.Run("CanWrite", func(t *testing.T) {
|
||||
can, err := p.CanWrite(s, admin)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, can)
|
||||
})
|
||||
|
||||
t.Run("CanDelete", func(t *testing.T) {
|
||||
can, err := p.CanDelete(s, admin)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, can)
|
||||
})
|
||||
}
|
||||
|
||||
// A stale JWT admin claim must not grant the bypass after DB demotion.
|
||||
func TestAdminBypass_StaleJWT_Demoted(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
license.SetForTests([]license.Feature{license.FeatureAdminPanel})
|
||||
defer license.ResetForTests()
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
stale := &user.User{ID: 3, IsAdmin: true}
|
||||
p := &Project{ID: 1}
|
||||
|
||||
t.Run("CanRead", func(t *testing.T) {
|
||||
can, _, err := p.CanRead(s, stale)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, can, "stale admin claim must not grant project read without DB confirmation")
|
||||
})
|
||||
|
||||
t.Run("CanWrite", func(t *testing.T) {
|
||||
can, err := p.CanWrite(s, stale)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, can)
|
||||
})
|
||||
|
||||
t.Run("CanDelete", func(t *testing.T) {
|
||||
can, err := p.CanDelete(s, stale)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, can)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAdminBypass_StaleJWT_DeletedUser(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
license.SetForTests([]license.Feature{license.FeatureAdminPanel})
|
||||
defer license.ResetForTests()
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
stale := &user.User{ID: 99999, IsAdmin: true}
|
||||
p := &Project{ID: 1}
|
||||
|
||||
t.Run("CanRead", func(t *testing.T) {
|
||||
can, _, err := p.CanRead(s, stale)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, can, "deleted admin must not grant bypass")
|
||||
})
|
||||
}
|
||||
|
|
@ -34,6 +34,10 @@ func (p *Project) CanWrite(s *xorm.Session, a web.Auth) (bool, error) {
|
|||
return false, nil
|
||||
}
|
||||
|
||||
if isInstanceAdmin(s, a) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Get the project and check the permission
|
||||
originalProject, err := GetProjectSimpleByID(s, p.ID)
|
||||
if err != nil {
|
||||
|
|
@ -74,6 +78,15 @@ func (p *Project) CanWrite(s *xorm.Session, a web.Auth) (bool, error) {
|
|||
// CanRead checks if a user has read access to a project
|
||||
func (p *Project) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
|
||||
|
||||
if isInstanceAdmin(s, a) {
|
||||
originalProject, err := GetProjectSimpleByID(s, p.ID)
|
||||
if err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
*p = *originalProject
|
||||
return true, int(PermissionAdmin), nil
|
||||
}
|
||||
|
||||
// The favorite project needs a special treatment
|
||||
if p.ID == FavoritesPseudoProject.ID {
|
||||
owner, err := user.GetFromAuth(a)
|
||||
|
|
@ -118,6 +131,10 @@ func (p *Project) CanUpdate(s *xorm.Session, a web.Auth) (canUpdate bool, err er
|
|||
return false, nil
|
||||
}
|
||||
|
||||
if isInstanceAdmin(s, a) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
fid := GetSavedFilterIDFromProjectID(p.ID)
|
||||
if fid > 0 {
|
||||
sf, err := GetSavedFilterSimpleByID(s, fid)
|
||||
|
|
@ -165,11 +182,17 @@ func (p *Project) CanUpdate(s *xorm.Session, a web.Auth) (canUpdate bool, err er
|
|||
|
||||
// CanDelete checks if the user can delete a project
|
||||
func (p *Project) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
if isInstanceAdmin(s, a) {
|
||||
return true, nil
|
||||
}
|
||||
return p.IsAdmin(s, a)
|
||||
}
|
||||
|
||||
// CanCreate checks if the user can create a project
|
||||
func (p *Project) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
if isInstanceAdmin(s, a) {
|
||||
return true, nil
|
||||
}
|
||||
if p.ParentProjectID != 0 {
|
||||
parent := &Project{ID: p.ParentProjectID}
|
||||
return parent.CanWrite(s, a)
|
||||
|
|
@ -189,6 +212,10 @@ func (p *Project) IsAdmin(s *xorm.Session, a web.Auth) (bool, error) {
|
|||
return false, nil
|
||||
}
|
||||
|
||||
if isInstanceAdmin(s, a) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
originalProject, err := GetProjectSimpleByID(s, p.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
|
|
|||
|
|
@ -42,6 +42,10 @@ func (tl *TeamProject) canDoTeamProject(s *xorm.Session, a web.Auth) (bool, erro
|
|||
return false, nil
|
||||
}
|
||||
|
||||
if isInstanceAdmin(s, a) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
l := Project{ID: tl.ProjectID}
|
||||
return l.IsAdmin(s, a)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,10 @@ func (lu *ProjectUser) canDoProjectUser(s *xorm.Session, a web.Auth) (bool, erro
|
|||
return false, nil
|
||||
}
|
||||
|
||||
if isInstanceAdmin(s, a) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Get the project and check if the user has write access on it
|
||||
l := Project{ID: lu.ProjectID}
|
||||
return l.IsAdmin(s, a)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ import (
|
|||
)
|
||||
|
||||
func (pv *ProjectView) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
|
||||
if isInstanceAdmin(s, a) {
|
||||
return true, int(PermissionAdmin), nil
|
||||
}
|
||||
filterID := GetSavedFilterIDFromProjectID(pv.ProjectID)
|
||||
if filterID > 0 {
|
||||
sf := &SavedFilter{ID: filterID}
|
||||
|
|
@ -33,6 +36,9 @@ func (pv *ProjectView) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
|
|||
}
|
||||
|
||||
func (pv *ProjectView) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
if isInstanceAdmin(s, a) {
|
||||
return true, nil
|
||||
}
|
||||
filterID := GetSavedFilterIDFromProjectID(pv.ProjectID)
|
||||
if filterID > 0 {
|
||||
sf := &SavedFilter{ID: filterID}
|
||||
|
|
@ -44,6 +50,9 @@ func (pv *ProjectView) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
|
|||
}
|
||||
|
||||
func (pv *ProjectView) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
if isInstanceAdmin(s, a) {
|
||||
return true, nil
|
||||
}
|
||||
filterID := GetSavedFilterIDFromProjectID(pv.ProjectID)
|
||||
if filterID > 0 {
|
||||
sf := &SavedFilter{ID: filterID}
|
||||
|
|
@ -55,6 +64,9 @@ func (pv *ProjectView) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
|
|||
}
|
||||
|
||||
func (pv *ProjectView) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
if isInstanceAdmin(s, a) {
|
||||
return true, nil
|
||||
}
|
||||
filterID := GetSavedFilterIDFromProjectID(pv.ProjectID)
|
||||
if filterID > 0 {
|
||||
sf := &SavedFilter{ID: filterID}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,10 @@ func (tm *TeamMember) IsAdmin(s *xorm.Session, a web.Auth) (bool, error) {
|
|||
return false, nil
|
||||
}
|
||||
|
||||
if isInstanceAdmin(s, a) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// A user can add a member to a team if he is admin of that team
|
||||
exists, err := s.
|
||||
Where("user_id = ? AND team_id = ? AND admin = ?", a.GetID(), tm.TeamID, true).
|
||||
|
|
|
|||
|
|
@ -54,6 +54,10 @@ func (t *Team) IsAdmin(s *xorm.Session, a web.Auth) (bool, error) {
|
|||
return false, err
|
||||
}
|
||||
|
||||
if isInstanceAdmin(s, a) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return s.Where("team_id = ?", t.ID).
|
||||
And("user_id = ?", a.GetID()).
|
||||
And("admin = ?", true).
|
||||
|
|
@ -62,6 +66,9 @@ func (t *Team) IsAdmin(s *xorm.Session, a web.Auth) (bool, error) {
|
|||
|
||||
// CanRead returns true if the user has read access to the team
|
||||
func (t *Team) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
|
||||
if isInstanceAdmin(s, a) {
|
||||
return true, int(PermissionAdmin), nil
|
||||
}
|
||||
// Check if the user is in the team
|
||||
tm := &TeamMember{}
|
||||
can, err := s.
|
||||
|
|
|
|||
Loading…
Reference in New Issue