User account deletion (#937)
Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/api/pulls/937 Co-authored-by: konrad <konrad@kola-entertainments.de> Co-committed-by: konrad <konrad@kola-entertainments.de>
This commit is contained in:
@ -762,11 +762,19 @@ func (l *List) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
}
|
||||
|
||||
// Delete all tasks on that list
|
||||
_, err = s.Where("list_id = ?", l.ID).Delete(&Task{})
|
||||
// Using the loop to make sure all related entities to all tasks are properly deleted as well.
|
||||
tasks, _, _, err := getRawTasksForLists(s, []*List{l}, a, &taskOptions{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, task := range tasks {
|
||||
err = task.Delete(s, a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return events.Dispatch(&ListDeletedEvent{
|
||||
List: l,
|
||||
Doer: a,
|
||||
|
@ -650,7 +650,10 @@ func CreateNewNamespaceForUser(s *xorm.Session, user *user.User) (err error) {
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{id} [delete]
|
||||
func (n *Namespace) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
return deleteNamespace(s, n, a, true)
|
||||
}
|
||||
|
||||
func deleteNamespace(s *xorm.Session, n *Namespace, a web.Auth, withLists bool) (err error) {
|
||||
// Check if the namespace exists
|
||||
_, err = GetNamespaceByID(s, n.ID)
|
||||
if err != nil {
|
||||
@ -663,6 +666,15 @@ func (n *Namespace) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
namespaceDeleted := &NamespaceDeletedEvent{
|
||||
Namespace: n,
|
||||
Doer: a,
|
||||
}
|
||||
|
||||
if !withLists {
|
||||
return events.Dispatch(namespaceDeleted)
|
||||
}
|
||||
|
||||
// Delete all lists with their tasks
|
||||
lists, err := GetListsByNamespaceID(s, n.ID, &user.User{})
|
||||
if err != nil {
|
||||
@ -670,43 +682,18 @@ func (n *Namespace) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
}
|
||||
|
||||
if len(lists) == 0 {
|
||||
return events.Dispatch(&NamespaceDeletedEvent{
|
||||
Namespace: n,
|
||||
Doer: a,
|
||||
})
|
||||
return events.Dispatch(namespaceDeleted)
|
||||
}
|
||||
|
||||
var listIDs []int64
|
||||
// We need to do that for here because we need the list ids to delete two times:
|
||||
// 1) to delete the lists itself
|
||||
// 2) to delete the list tasks
|
||||
for _, l := range lists {
|
||||
listIDs = append(listIDs, l.ID)
|
||||
// Looping over all lists to let the list handle properly cleaning up the tasks and everything else associated with it.
|
||||
for _, list := range lists {
|
||||
err = list.Delete(s, a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(listIDs) == 0 {
|
||||
return events.Dispatch(&NamespaceDeletedEvent{
|
||||
Namespace: n,
|
||||
Doer: a,
|
||||
})
|
||||
}
|
||||
|
||||
// Delete tasks
|
||||
_, err = s.In("list_id", listIDs).Delete(&Task{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the lists
|
||||
_, err = s.In("id", listIDs).Delete(&List{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return events.Dispatch(&NamespaceDeletedEvent{
|
||||
Namespace: n,
|
||||
Doer: a,
|
||||
})
|
||||
return events.Dispatch(namespaceDeleted)
|
||||
}
|
||||
|
||||
// Update implements the update method via the interface
|
||||
|
275
pkg/models/user_delete.go
Normal file
275
pkg/models/user_delete.go
Normal file
@ -0,0 +1,275 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/cron"
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/notifications"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// User deletion must happen here in this packaage because we want to delete everything associated to this user.
|
||||
// Because most of these things are managed in the models package, using them has to happen here.
|
||||
|
||||
// RegisterUserDeletionCron registers the cron job that actually removes users who are scheduled to delete.
|
||||
func RegisterUserDeletionCron() {
|
||||
err := cron.Schedule("0 * * * *", deleteUsers)
|
||||
if err != nil {
|
||||
log.Errorf("Could not register deletion cron: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func deleteUsers() {
|
||||
s := db.NewSession()
|
||||
users := []*user.User{}
|
||||
err := s.Where(builder.Lt{"deletion_scheduled_at": time.Now()}).
|
||||
Find(&users)
|
||||
if err != nil {
|
||||
log.Errorf("Could not get users scheduled for deletion: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, u := range users {
|
||||
if u.DeletionScheduledAt.Before(time.Now()) {
|
||||
log.Debugf("User %d is not yet scheduled for deletion.", u.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
err = s.Begin()
|
||||
if err != nil {
|
||||
log.Errorf("Could not start transaction: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = DeleteUser(s, u)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
log.Errorf("Could not delete u %d: %s", u.ID, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = s.Commit()
|
||||
if err != nil {
|
||||
log.Errorf("Could not commit transaction: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteUser completely removes a user and all their associated lists, namespaces and tasks.
|
||||
// This action is irrevocable.
|
||||
// Public to allow deletion from the CLI.
|
||||
func DeleteUser(s *xorm.Session, u *user.User) (err error) {
|
||||
namespacesToDelete := []*Namespace{}
|
||||
// Get all namespaces and lists this u has access to
|
||||
nm := &Namespace{IsArchived: true}
|
||||
res, _, _, err := nm.ReadAll(s, u, "", 1, -1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
namespaces := res.([]*NamespaceWithLists)
|
||||
for _, n := range namespaces {
|
||||
if n.ID < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
hadUsers, err := ensureNamespaceAdminUser(s, &n.Namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hadUsers {
|
||||
continue
|
||||
}
|
||||
hadTeams, err := ensureNamespaceAdminTeam(s, &n.Namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hadTeams {
|
||||
continue
|
||||
}
|
||||
|
||||
namespacesToDelete = append(namespacesToDelete, &n.Namespace)
|
||||
}
|
||||
|
||||
// Get all lists to delete
|
||||
listsToDelete := []*List{}
|
||||
lm := &List{IsArchived: true}
|
||||
res, _, _, err = lm.ReadAll(s, u, "", 0, -1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lists := res.([]*List)
|
||||
for _, l := range lists {
|
||||
if l.ID < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
hadUsers, err := ensureListAdminUser(s, l)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hadUsers {
|
||||
continue
|
||||
}
|
||||
hadTeams, err := ensureListAdminTeam(s, l)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hadTeams {
|
||||
continue
|
||||
}
|
||||
|
||||
listsToDelete = append(listsToDelete, l)
|
||||
}
|
||||
|
||||
// Delete everything not shared with anybody else
|
||||
for _, n := range namespacesToDelete {
|
||||
err = deleteNamespace(s, n, u, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, l := range listsToDelete {
|
||||
err = l.Delete(s, u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, err = s.Where("id = ?", u.ID).Delete(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return notifications.Notify(u, &user.AccountDeletedNotification{})
|
||||
}
|
||||
|
||||
func ensureNamespaceAdminUser(s *xorm.Session, n *Namespace) (hadUsers bool, err error) {
|
||||
namespaceUsers := []*NamespaceUser{}
|
||||
err = s.Where("namespace_id = ?", n.ID).Find(&namespaceUsers)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(namespaceUsers) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
for _, lu := range namespaceUsers {
|
||||
if lu.Right == RightAdmin {
|
||||
// List already has more than one admin, no need to do anything
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
firstUser := namespaceUsers[0]
|
||||
firstUser.Right = RightAdmin
|
||||
_, err = s.Where("id = ?", firstUser.ID).
|
||||
Cols("right").
|
||||
Update(firstUser)
|
||||
return true, err
|
||||
}
|
||||
|
||||
func ensureNamespaceAdminTeam(s *xorm.Session, n *Namespace) (hadTeams bool, err error) {
|
||||
namespaceTeams := []*TeamNamespace{}
|
||||
err = s.Where("namespace_id = ?", n.ID).Find(&namespaceTeams)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(namespaceTeams) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
for _, lu := range namespaceTeams {
|
||||
if lu.Right == RightAdmin {
|
||||
// List already has more than one admin, no need to do anything
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
firstTeam := namespaceTeams[0]
|
||||
firstTeam.Right = RightAdmin
|
||||
_, err = s.Where("id = ?", firstTeam.ID).
|
||||
Cols("right").
|
||||
Update(firstTeam)
|
||||
return true, err
|
||||
}
|
||||
|
||||
func ensureListAdminUser(s *xorm.Session, l *List) (hadUsers bool, err error) {
|
||||
listUsers := []*ListUser{}
|
||||
err = s.Where("list_id = ?", l.ID).Find(&listUsers)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(listUsers) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
for _, lu := range listUsers {
|
||||
if lu.Right == RightAdmin {
|
||||
// List already has more than one admin, no need to do anything
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
firstUser := listUsers[0]
|
||||
firstUser.Right = RightAdmin
|
||||
_, err = s.Where("id = ?", firstUser.ID).
|
||||
Cols("right").
|
||||
Update(firstUser)
|
||||
return true, err
|
||||
}
|
||||
|
||||
func ensureListAdminTeam(s *xorm.Session, l *List) (hadTeams bool, err error) {
|
||||
listTeams := []*TeamList{}
|
||||
err = s.Where("list_id = ?", l.ID).Find(&listTeams)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(listTeams) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
for _, lu := range listTeams {
|
||||
if lu.Right == RightAdmin {
|
||||
// List already has more than one admin, no need to do anything
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
firstTeam := listTeams[0]
|
||||
firstTeam.Right = RightAdmin
|
||||
_, err = s.Where("id = ?", firstTeam.ID).
|
||||
Cols("right").
|
||||
Update(firstTeam)
|
||||
return true, err
|
||||
}
|
47
pkg/models/user_delete_test.go
Normal file
47
pkg/models/user_delete_test.go
Normal file
@ -0,0 +1,47 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/notifications"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDeleteUser(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
notifications.Fake()
|
||||
|
||||
u := &user.User{ID: 6}
|
||||
err := DeleteUser(s, u)
|
||||
|
||||
assert.NoError(t, err)
|
||||
db.AssertMissing(t, "users", map[string]interface{}{"id": u.ID})
|
||||
db.AssertMissing(t, "lists", map[string]interface{}{"id": 24}) // only user6 had access to this list
|
||||
db.AssertExists(t, "lists", map[string]interface{}{"id": 6}, false)
|
||||
db.AssertExists(t, "lists", map[string]interface{}{"id": 7}, false)
|
||||
db.AssertExists(t, "lists", map[string]interface{}{"id": 8}, false)
|
||||
db.AssertExists(t, "lists", map[string]interface{}{"id": 9}, false)
|
||||
db.AssertExists(t, "lists", map[string]interface{}{"id": 10}, false)
|
||||
db.AssertExists(t, "lists", map[string]interface{}{"id": 11}, false)
|
||||
}
|
Reference in New Issue
Block a user