// 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 Licensee 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 Licensee for more details. // // You should have received a copy of the GNU Affero General Public Licensee // along with this program. If not, see . package models import ( "errors" "code.vikunja.io/api/pkg/user" "code.vikunja.io/web" "xorm.io/builder" "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 } // Check if the user is either owner or can write to the project if originalProject.isOwner(&user.User{ID: a.GetID()}) { canWrite = true } if canWrite { return canWrite, errIsArchived } canWrite, _, err = originalProject.checkRight(s, a, 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 } if p.isOwner(&user.User{ID: a.GetID()}) { return true, int(RightAdmin), nil } return p.checkRight(s, a, 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 } // 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{} } } fid := getSavedFilterIDFromProjectID(p.ID) if fid > 0 { sf, err := getSavedFilterSimpleByID(s, fid) if err != nil { return false, err } return sf.CanUpdate(s, a) } 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 } // Check all the things // Check if the user is either owner or can write to the project // Owners are always admins if originalProject.isOwner(&user.User{ID: a.GetID()}) { return true, nil } is, _, err := originalProject.checkRight(s, a, 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, a web.Auth, rights ...Right) (bool, int, error) { var conds []builder.Cond for _, r := range rights { // User conditions // If the project was shared directly with the user and the user has the right conds = append(conds, builder.And( builder.Eq{"ul.user_id": a.GetID()}, builder.Eq{"ul.right": r}, )) // Team rights // If the project was shared directly with the team and the team has the right conds = append(conds, builder.And( builder.Eq{"tm2.user_id": a.GetID()}, builder.Eq{"tl.right": r}, )) } type allProjectRights struct { UserProject *ProjectUser `xorm:"extends"` TeamProject *TeamProject `xorm:"extends"` } r := &allProjectRights{} var maxRight = 0 exists, err := s. Select("p.*, ul.right, tl.right"). Table("projects"). Alias("p"). // User stuff Join("LEFT", []string{"users_projects", "ul"}, "ul.project_id = p.id"). // Team stuff Join("LEFT", []string{"team_projects", "tl"}, "p.id = tl.project_id"). Join("LEFT", []string{"team_members", "tm2"}, "tm2.team_id = tl.team_id"). // The actual condition Where(builder.And( builder.Or( conds..., ), builder.Eq{"p.id": p.ID}, )). Get(r) // If there's noting shared for this project, and it has a parent, go up the tree if !exists && p.ParentProjectID > 0 { parent, err := GetProjectSimpleByID(s, p.ParentProjectID) if err != nil { return false, 0, err } return parent.checkRight(s, a, rights...) } // Figure out the max right and return it if int(r.UserProject.Right) > maxRight { maxRight = int(r.UserProject.Right) } if int(r.TeamProject.Right) > maxRight { maxRight = int(r.TeamProject.Right) } return exists, maxRight, err }