code.gitea.io/gitea@v1.22.3/services/user/user.go (about)

     1  // Copyright 2021 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package user
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"os"
    10  	"strings"
    11  	"time"
    12  
    13  	"code.gitea.io/gitea/models"
    14  	"code.gitea.io/gitea/models/db"
    15  	"code.gitea.io/gitea/models/organization"
    16  	packages_model "code.gitea.io/gitea/models/packages"
    17  	repo_model "code.gitea.io/gitea/models/repo"
    18  	system_model "code.gitea.io/gitea/models/system"
    19  	user_model "code.gitea.io/gitea/models/user"
    20  	"code.gitea.io/gitea/modules/eventsource"
    21  	"code.gitea.io/gitea/modules/log"
    22  	"code.gitea.io/gitea/modules/setting"
    23  	"code.gitea.io/gitea/modules/storage"
    24  	"code.gitea.io/gitea/modules/util"
    25  	"code.gitea.io/gitea/services/agit"
    26  	asymkey_service "code.gitea.io/gitea/services/asymkey"
    27  	org_service "code.gitea.io/gitea/services/org"
    28  	"code.gitea.io/gitea/services/packages"
    29  	container_service "code.gitea.io/gitea/services/packages/container"
    30  	repo_service "code.gitea.io/gitea/services/repository"
    31  )
    32  
    33  // RenameUser renames a user
    34  func RenameUser(ctx context.Context, u *user_model.User, newUserName string) error {
    35  	// Non-local users are not allowed to change their username.
    36  	if !u.IsOrganization() && !u.IsLocal() {
    37  		return user_model.ErrUserIsNotLocal{
    38  			UID:  u.ID,
    39  			Name: u.Name,
    40  		}
    41  	}
    42  
    43  	if newUserName == u.Name {
    44  		return nil
    45  	}
    46  
    47  	if err := user_model.IsUsableUsername(newUserName); err != nil {
    48  		return err
    49  	}
    50  
    51  	onlyCapitalization := strings.EqualFold(newUserName, u.Name)
    52  	oldUserName := u.Name
    53  
    54  	if onlyCapitalization {
    55  		u.Name = newUserName
    56  		if err := user_model.UpdateUserCols(ctx, u, "name"); err != nil {
    57  			u.Name = oldUserName
    58  			return err
    59  		}
    60  		return repo_model.UpdateRepositoryOwnerNames(ctx, u.ID, newUserName)
    61  	}
    62  
    63  	ctx, committer, err := db.TxContext(ctx)
    64  	if err != nil {
    65  		return err
    66  	}
    67  	defer committer.Close()
    68  
    69  	isExist, err := user_model.IsUserExist(ctx, u.ID, newUserName)
    70  	if err != nil {
    71  		return err
    72  	}
    73  	if isExist {
    74  		return user_model.ErrUserAlreadyExist{
    75  			Name: newUserName,
    76  		}
    77  	}
    78  
    79  	if err = repo_model.UpdateRepositoryOwnerName(ctx, oldUserName, newUserName); err != nil {
    80  		return err
    81  	}
    82  
    83  	if err = user_model.NewUserRedirect(ctx, u.ID, oldUserName, newUserName); err != nil {
    84  		return err
    85  	}
    86  
    87  	if err := agit.UserNameChanged(ctx, u, newUserName); err != nil {
    88  		return err
    89  	}
    90  	if err := container_service.UpdateRepositoryNames(ctx, u, newUserName); err != nil {
    91  		return err
    92  	}
    93  
    94  	u.Name = newUserName
    95  	u.LowerName = strings.ToLower(newUserName)
    96  	if err := user_model.UpdateUserCols(ctx, u, "name", "lower_name"); err != nil {
    97  		u.Name = oldUserName
    98  		u.LowerName = strings.ToLower(oldUserName)
    99  		return err
   100  	}
   101  
   102  	// Do not fail if directory does not exist
   103  	if err = util.Rename(user_model.UserPath(oldUserName), user_model.UserPath(newUserName)); err != nil && !os.IsNotExist(err) {
   104  		u.Name = oldUserName
   105  		u.LowerName = strings.ToLower(oldUserName)
   106  		return fmt.Errorf("rename user directory: %w", err)
   107  	}
   108  
   109  	if err = committer.Commit(); err != nil {
   110  		u.Name = oldUserName
   111  		u.LowerName = strings.ToLower(oldUserName)
   112  		if err2 := util.Rename(user_model.UserPath(newUserName), user_model.UserPath(oldUserName)); err2 != nil && !os.IsNotExist(err2) {
   113  			log.Critical("Unable to rollback directory change during failed username change from: %s to: %s. DB Error: %v. Filesystem Error: %v", oldUserName, newUserName, err, err2)
   114  			return fmt.Errorf("failed to rollback directory change during failed username change from: %s to: %s. DB Error: %w. Filesystem Error: %v", oldUserName, newUserName, err, err2)
   115  		}
   116  		return err
   117  	}
   118  	return nil
   119  }
   120  
   121  // DeleteUser completely and permanently deletes everything of a user,
   122  // but issues/comments/pulls will be kept and shown as someone has been deleted,
   123  // unless the user is younger than USER_DELETE_WITH_COMMENTS_MAX_DAYS.
   124  func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
   125  	if u.IsOrganization() {
   126  		return fmt.Errorf("%s is an organization not a user", u.Name)
   127  	}
   128  
   129  	if u.IsActive && user_model.IsLastAdminUser(ctx, u) {
   130  		return models.ErrDeleteLastAdminUser{UID: u.ID}
   131  	}
   132  
   133  	if purge {
   134  		// Disable the user first
   135  		// NOTE: This is deliberately not within a transaction as it must disable the user immediately to prevent any further action by the user to be purged.
   136  		if err := user_model.UpdateUserCols(ctx, &user_model.User{
   137  			ID:              u.ID,
   138  			IsActive:        false,
   139  			IsRestricted:    true,
   140  			IsAdmin:         false,
   141  			ProhibitLogin:   true,
   142  			Passwd:          "",
   143  			Salt:            "",
   144  			PasswdHashAlgo:  "",
   145  			MaxRepoCreation: 0,
   146  		}, "is_active", "is_restricted", "is_admin", "prohibit_login", "max_repo_creation", "passwd", "salt", "passwd_hash_algo"); err != nil {
   147  			return fmt.Errorf("unable to disable user: %s[%d] prior to purge. UpdateUserCols: %w", u.Name, u.ID, err)
   148  		}
   149  
   150  		// Force any logged in sessions to log out
   151  		// FIXME: We also need to tell the session manager to log them out too.
   152  		eventsource.GetManager().SendMessage(u.ID, &eventsource.Event{
   153  			Name: "logout",
   154  		})
   155  
   156  		// Delete all repos belonging to this user
   157  		// Now this is not within a transaction because there are internal transactions within the DeleteRepository
   158  		// BUT: the db will still be consistent even if a number of repos have already been deleted.
   159  		// And in fact we want to capture any repositories that are being created in other transactions in the meantime
   160  		//
   161  		// An alternative option here would be write a DeleteAllRepositoriesForUserID function which would delete all of the repos
   162  		// but such a function would likely get out of date
   163  		err := repo_service.DeleteOwnerRepositoriesDirectly(ctx, u)
   164  		if err != nil {
   165  			return err
   166  		}
   167  
   168  		// Remove from Organizations and delete last owner organizations
   169  		// Now this is not within a transaction because there are internal transactions within the DeleteOrganization
   170  		// BUT: the db will still be consistent even if a number of organizations memberships and organizations have already been deleted
   171  		// And in fact we want to capture any organization additions that are being created in other transactions in the meantime
   172  		//
   173  		// An alternative option here would be write a function which would delete all organizations but it seems
   174  		// but such a function would likely get out of date
   175  		for {
   176  			orgs, err := db.Find[organization.Organization](ctx, organization.FindOrgOptions{
   177  				ListOptions: db.ListOptions{
   178  					PageSize: repo_model.RepositoryListDefaultPageSize,
   179  					Page:     1,
   180  				},
   181  				UserID:         u.ID,
   182  				IncludePrivate: true,
   183  			})
   184  			if err != nil {
   185  				return fmt.Errorf("unable to find org list for %s[%d]. Error: %w", u.Name, u.ID, err)
   186  			}
   187  			if len(orgs) == 0 {
   188  				break
   189  			}
   190  			for _, org := range orgs {
   191  				if err := models.RemoveOrgUser(ctx, org, u); err != nil {
   192  					if organization.IsErrLastOrgOwner(err) {
   193  						err = org_service.DeleteOrganization(ctx, org, true)
   194  						if err != nil {
   195  							return fmt.Errorf("unable to delete organization %d: %w", org.ID, err)
   196  						}
   197  					}
   198  					if err != nil {
   199  						return fmt.Errorf("unable to remove user %s[%d] from org %s[%d]. Error: %w", u.Name, u.ID, org.Name, org.ID, err)
   200  					}
   201  				}
   202  			}
   203  		}
   204  
   205  		// Delete Packages
   206  		if setting.Packages.Enabled {
   207  			if _, err := packages.RemoveAllPackages(ctx, u.ID); err != nil {
   208  				return err
   209  			}
   210  		}
   211  	}
   212  
   213  	ctx, committer, err := db.TxContext(ctx)
   214  	if err != nil {
   215  		return err
   216  	}
   217  	defer committer.Close()
   218  
   219  	// Note: A user owns any repository or belongs to any organization
   220  	//	cannot perform delete operation. This causes a race with the purge above
   221  	//  however consistency requires that we ensure that this is the case
   222  
   223  	// Check ownership of repository.
   224  	count, err := repo_model.CountRepositories(ctx, repo_model.CountRepositoryOptions{OwnerID: u.ID})
   225  	if err != nil {
   226  		return fmt.Errorf("GetRepositoryCount: %w", err)
   227  	} else if count > 0 {
   228  		return models.ErrUserOwnRepos{UID: u.ID}
   229  	}
   230  
   231  	// Check membership of organization.
   232  	count, err = organization.GetOrganizationCount(ctx, u)
   233  	if err != nil {
   234  		return fmt.Errorf("GetOrganizationCount: %w", err)
   235  	} else if count > 0 {
   236  		return models.ErrUserHasOrgs{UID: u.ID}
   237  	}
   238  
   239  	// Check ownership of packages.
   240  	if ownsPackages, err := packages_model.HasOwnerPackages(ctx, u.ID); err != nil {
   241  		return fmt.Errorf("HasOwnerPackages: %w", err)
   242  	} else if ownsPackages {
   243  		return models.ErrUserOwnPackages{UID: u.ID}
   244  	}
   245  
   246  	if err := deleteUser(ctx, u, purge); err != nil {
   247  		return fmt.Errorf("DeleteUser: %w", err)
   248  	}
   249  
   250  	if err := committer.Commit(); err != nil {
   251  		return err
   252  	}
   253  	_ = committer.Close()
   254  
   255  	if err = asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
   256  		return err
   257  	}
   258  	if err = asymkey_service.RewriteAllPrincipalKeys(ctx); err != nil {
   259  		return err
   260  	}
   261  
   262  	// Note: There are something just cannot be roll back, so just keep error logs of those operations.
   263  	path := user_model.UserPath(u.Name)
   264  	if err = util.RemoveAll(path); err != nil {
   265  		err = fmt.Errorf("failed to RemoveAll %s: %w", path, err)
   266  		_ = system_model.CreateNotice(ctx, system_model.NoticeTask, fmt.Sprintf("delete user '%s': %v", u.Name, err))
   267  	}
   268  
   269  	if u.Avatar != "" {
   270  		avatarPath := u.CustomAvatarRelativePath()
   271  		if err = storage.Avatars.Delete(avatarPath); err != nil {
   272  			err = fmt.Errorf("failed to remove %s: %w", avatarPath, err)
   273  			_ = system_model.CreateNotice(ctx, system_model.NoticeTask, fmt.Sprintf("delete user '%s': %v", u.Name, err))
   274  		}
   275  	}
   276  
   277  	return nil
   278  }
   279  
   280  // DeleteInactiveUsers deletes all inactive users and their email addresses.
   281  func DeleteInactiveUsers(ctx context.Context, olderThan time.Duration) error {
   282  	inactiveUsers, err := user_model.GetInactiveUsers(ctx, olderThan)
   283  	if err != nil {
   284  		return err
   285  	}
   286  
   287  	// FIXME: should only update authorized_keys file once after all deletions.
   288  	for _, u := range inactiveUsers {
   289  		if err = DeleteUser(ctx, u, false); err != nil {
   290  			// Ignore inactive users that were ever active but then were set inactive by admin
   291  			if models.IsErrUserOwnRepos(err) || models.IsErrUserHasOrgs(err) || models.IsErrUserOwnPackages(err) {
   292  				continue
   293  			}
   294  			select {
   295  			case <-ctx.Done():
   296  				return db.ErrCancelledf("when deleting inactive user %q", u.Name)
   297  			default:
   298  				return err
   299  			}
   300  		}
   301  	}
   302  	return nil // TODO: there could be still inactive users left, and the number would increase gradually
   303  }