// 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 . package models import ( "errors" "code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/utils" "code.vikunja.io/api/pkg/web" "xorm.io/xorm" ) // CanWrite return whether the user can write on that project or not func (p *Project) CanWrite(s *xorm.Session, a web.Auth) (bool, error) { // The favorite project can't be edited if p.ID == FavoritesPseudoProject.ID { return false, nil } // Get the project and check the right originalProject, err := GetProjectSimpleByID(s, p.ID) if err != nil { return false, err } // We put the result of the is archived check in a separate variable to be able to return it later without // needing to recheck it again errIsArchived := originalProject.CheckIsArchived(s) var canWrite bool // Check if we're dealing with a share auth shareAuth, ok := a.(*LinkSharing) if ok { return originalProject.ID == shareAuth.ProjectID && (shareAuth.Right == RightWrite || shareAuth.Right == RightAdmin), errIsArchived } u := &user.User{ID: a.GetID()} // Check if the user is either owner or can write to the project if originalProject.isOwner(u) { canWrite = true } if canWrite { return canWrite, errIsArchived } canWrite, _, err = originalProject.checkRight(s, u, RightWrite, RightAdmin) if err != nil { return false, err } return canWrite, errIsArchived } // CanRead checks if a user has read access to a project func (p *Project) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) { // The favorite project needs a special treatment if p.ID == FavoritesPseudoProject.ID { owner, err := user.GetFromAuth(a) if err != nil { return false, 0, err } *p = FavoritesPseudoProject p.Owner = owner return true, int(RightRead), nil } // Saved Filter Projects need a special case if GetSavedFilterIDFromProjectID(p.ID) > 0 { sf := &SavedFilter{ID: GetSavedFilterIDFromProjectID(p.ID)} return sf.CanRead(s, a) } // Check if the user is either owner or can read var err error originalProject, err := GetProjectSimpleByID(s, p.ID) if err != nil { return false, 0, err } *p = *originalProject // Check if we're dealing with a share auth shareAuth, ok := a.(*LinkSharing) if ok { return p.ID == shareAuth.ProjectID && (shareAuth.Right == RightRead || shareAuth.Right == RightWrite || shareAuth.Right == RightAdmin), int(shareAuth.Right), nil } return p.checkRight(s, &user.User{ID: a.GetID()}, RightRead, RightWrite, RightAdmin) } // CanUpdate checks if the user can update a project func (p *Project) CanUpdate(s *xorm.Session, a web.Auth) (canUpdate bool, err error) { // The favorite project can't be edited if p.ID == FavoritesPseudoProject.ID { return false, nil } fid := GetSavedFilterIDFromProjectID(p.ID) if fid > 0 { sf, err := GetSavedFilterSimpleByID(s, fid) if err != nil { return false, err } return sf.CanUpdate(s, a) } // Get the project ol, err := GetProjectSimpleByID(s, p.ID) if err != nil { return false, err } // Check if we're moving the project to a different parent project. // If that is the case, we need to verify permissions to do so. if p.ParentProjectID != 0 && p.ParentProjectID != ol.ParentProjectID { newProject := &Project{ID: p.ParentProjectID} can, err := newProject.CanWrite(s, a) if err != nil { return false, err } if !can { return false, ErrGenericForbidden{} } } canUpdate, err = p.CanWrite(s, a) // If the project is archived and the user tries to un-archive it, let the request through archivedErr := ErrProjectIsArchived{} is := errors.As(err, &archivedErr) if is && !p.IsArchived && archivedErr.ProjectID == p.ID { err = nil } return canUpdate, err } // CanDelete checks if the user can delete a project func (p *Project) CanDelete(s *xorm.Session, a web.Auth) (bool, error) { 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 p.ParentProjectID != 0 { parent := &Project{ID: p.ParentProjectID} return parent.CanWrite(s, a) } // Check if we're dealing with a share auth _, is := a.(*LinkSharing) if is { return false, nil } return true, nil } // IsAdmin returns whether the user has admin rights on the project or not func (p *Project) IsAdmin(s *xorm.Session, a web.Auth) (bool, error) { // The favorite project can't be edited if p.ID == FavoritesPseudoProject.ID { return false, nil } originalProject, err := GetProjectSimpleByID(s, p.ID) if err != nil { return false, err } // Check if we're dealing with a share auth shareAuth, ok := a.(*LinkSharing) if ok { return originalProject.ID == shareAuth.ProjectID && shareAuth.Right == RightAdmin, nil } u := &user.User{ID: a.GetID()} // Check all the things // Check if the user is either owner or can write to the project // Owners are always admins if originalProject.isOwner(u) { return true, nil } is, _, err := originalProject.checkRight(s, u, RightAdmin) return is, err } // Little helper function to check if a user is project owner func (p *Project) isOwner(u *user.User) bool { return p.OwnerID == u.ID } // Checks n different rights for any given user func (p *Project) checkRight(s *xorm.Session, u *user.User, rights ...Right) (bool, int, error) { projectRights, err := checkRightsForProjects(s, u, []int64{p.ID}) if err != nil { return false, 0, err } right, has := projectRights[p.ID] if !has { return false, 0, nil } for _, r := range rights { if r == right.MaxRight { return true, int(right.MaxRight), nil } } return false, 0, nil } type projectRight struct { ID int64 `xorm:"pk autoincr"` MaxRight Right } func checkRightsForProjects(s *xorm.Session, u *user.User, projectIDs []int64) (projectRightMap map[int64]*projectRight, err error) { projectRightMap = make(map[int64]*projectRight) if len(projectIDs) < 1 { return } args := []interface{}{ u.ID, u.ID, u.ID, u.ID, u.ID, u.ID, u.ID, } err = s.SQL(` WITH RECURSIVE project_hierarchy AS ( -- Base case: Start with the specified projects SELECT id, parent_project_id, 0 AS level, id AS original_project_id FROM projects WHERE id IN (`+utils.JoinInt64Slice(projectIDs, ", ")+`) UNION ALL -- Recursive case: Traverse up the hierarchy SELECT p.id, p.parent_project_id, ph.level + 1, ph.original_project_id FROM projects p INNER JOIN project_hierarchy ph ON p.id = ph.parent_project_id), project_permissions AS (SELECT ph.id, ph.original_project_id, CASE WHEN p.owner_id = ? THEN 2 WHEN COALESCE(ul.right, 0) > COALESCE(tl.right, 0) THEN ul.right ELSE COALESCE(tl.right, 0) END AS project_right, CASE WHEN p.owner_id = ? THEN 1 -- Direct project ownership ELSE ph.level + 1 -- Derived from parent project END AS priority FROM project_hierarchy ph LEFT JOIN projects p ON ph.id = p.id LEFT JOIN users_projects ul ON ul.project_id = ph.id AND ul.user_id = ? LEFT JOIN team_projects tl ON tl.project_id = ph.id LEFT JOIN team_members tm ON tm.team_id = tl.team_id AND tm.user_id = ? WHERE p.owner_id = ? OR ul.user_id = ? OR tm.user_id = ?) SELECT ph.original_project_id AS id, COALESCE(MAX(pp.project_right), -1) AS max_right FROM project_hierarchy ph LEFT JOIN (SELECT *, ROW_NUMBER() OVER (PARTITION BY original_project_id ORDER BY priority) AS rn FROM project_permissions) pp ON ph.id = pp.id AND pp.rn = 1 GROUP BY ph.original_project_id`, args...). Find(&projectRightMap) return }