1
0

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:
konrad
2021-08-11 19:08:10 +00:00
parent cd21c5fc6e
commit 27119ad6d4
28 changed files with 1402 additions and 41 deletions

View File

@ -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,

View File

@ -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
View 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
}

View 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)
}