Skip to content

feat: convert regular accounts to guests#1619

Open
ernolf wants to merge 3 commits into
mainfrom
ernolf/feat/convert-to-guest
Open

feat: convert regular accounts to guests#1619
ernolf wants to merge 3 commits into
mainfrom
ernolf/feat/convert-to-guest

Conversation

@ernolf

@ernolf ernolf commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an admin action to convert a regular account into a guest account, and lets guests have a free-form login name instead of one derived from their email.

Closes #1153
Closes #333
Related #1479

Convert accounts to guests

Create a user (just for testing):

$ NC_PASS=karltest occ user:add --password-from-env --display-name="Karl Test" -- karltest
The account "karltest" was created successfully
Display name set to "Karl Test"
$ occ user:info karltest
  - user_id: karltest
  - display_name: Karl Test
  - email:
  - cloud_id: karltest@global-social.net
  - enabled: true
  - groups:
  - quota: none
  - storage:
    - free: 1184627609600
    - used: 0
    - total: 1184627609600
    - relative: 0
    - quota: -3
  - first_seen: never
  - last_seen: never
  - user_directory: /nc/dat/data/karltest
  - backend: Database

In Administration settings → Users, the account's menu gains Convert to guest account.

image

A confirmation dialog explains the effect: the account keeps its login name and password but becomes a limited guest, restricted to the apps allowed for guests, and receives the default guest quota. The conversion cannot be undone automatically.

image

It is only possible for accounts that use the database backend, are not already a guest, and have never logged in (enforced in the UI and in the OCS endpoint).

After conversion the account's backend is Guests:

image

and it logs in with the same password:

image

It then appears in the guests list:

image

Use case: self-registration

A typical use case is self-registration through the registration app with "Require administrator approval" enabled: those accounts are created disabled and never logged in, so an administrator can downgrade them to guests instead of enabling them. Once such an account has logged in, conversion is no longer possible.

If you would rather registered users keep their email address as login name, enable "Force email as login name" in the registration app settings.

Custom guest login names

occ guests:add gains --uid to set a free-form login name, instead of always deriving the user ID from the email address (or its hash):

$ occ guests:add --help
Description:
  Add a new guest account

Usage:
  guests:add [options] [--] <created-by> <email>

Arguments:
  created-by                         User ID who is set as creator
  email                              Email address

Options:
      --uid=UID                      Login name (user ID) for the guest. If omitted, the email address is used (hashed when the privacy setting is enabled).
      --generate-password            Do not set an initial password
      --password-from-env            Read password from environment variable OC_PASS
      --display-name[=DISPLAY-NAME]  User name used in the web UI (can contain any characters) [default: ""]
      --language[=LANGUAGE]          Language [default: ""]
  -h, --help                         Display help for the given command. When no command is given display help for the list command
  -q, --quiet                        Do not output any message
  -V, --version                      Display this application version
      --ansi|--no-ansi               Force (or disable --no-ansi) ANSI output
  -n, --no-interaction               Do not ask any interactive question
      --no-warnings                  Skip global warnings, show command output only
  -v|vv|vvv, --verbose               Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

Example — create a guest with a chosen login name and verify the user ID:

$ occ guests:add admin guest@example.com --uid=freddy
$ occ user:info freddy

The guests backend user-id guard is loosened accordingly to accept any non-empty id.

Implementation

  • ConversionService performs the transactional usersguests_users move (uid, display name, password hash and email preserved).
  • UsersController::convert() OCS endpoint (admin-only) with the eligibility checks above; route POST /api/v1/convert.
  • GuestManager::setGuestQuota() extracted so both newly created and converted guests get the configured default quota.
  • occ guests:add --uid for a free-form login name.
  • Unit tests for the backend guard, the --uid option, the controller endpoint and the conversion service.

Testing

Verified on a live instance (screenshots above): created a never-logged-in database account, converted it via the UI, confirmed the backend switched to Guests, the account logs in with the same password, and it appears in the guests list. The action is hidden for accounts that have logged in and for existing guests.

🤖 AI (if applicable)

  • The content of this PR was partly generated using AI
    Assisted-by: ClaudeCode:claude-opus-4-8

@github-actions

Copy link
Copy Markdown
Contributor

Hello there,
Thank you so much for taking the time and effort to create a pull request to our Nextcloud project.

We hope that the review process is going smooth and is helpful for you. We want to ensure your pull request is reviewed to your satisfaction. If you have a moment, our community management team would very much appreciate your feedback on your experience with this PR review process.

Your feedback is valuable to us as we continuously strive to improve our community developer experience. Please take a moment to complete our short survey by clicking on the following link: https://cloud.nextcloud.com/apps/forms/s/i9Ago4EQRZ7TWxjfmeEpPkf6

Thank you for contributing to Nextcloud and we hope to hear from you soon!

(If you believe you should not receive this message, you can add yourself to the blocklist.)

Comment thread lib/Controller/UsersController.php
ernolf added 3 commits July 22, 2026 12:29
- add "Convert to guest account" for regular, never-logged-in database accounts (admin … menu action + OCS endpoint), keeping the login name and password
- add ConversionService for the transactional users -> guests_users move; extract GuestManager::setGuestQuota so converted accounts get the default guest quota
- allow a free-form guest login name via `occ guests:add --uid` instead of deriving the user ID from the email address
- loosen the guests backend user-id guard to accept any non-empty id

Closes #1153

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: ernolf <raphael.gradenwitz@googlemail.com>
Signed-off-by: ernolf <raphael.gradenwitz@googlemail.com>
- refuse converting members of the admin group and subadmins, both return 409
- add controller tests covering both rejection paths

Assisted-by: ClaudeCode:claude-fable-5
Signed-off-by: ernolf <raphael.gradenwitz@googlemail.com>
@ernolf
ernolf force-pushed the ernolf/feat/convert-to-guest branch from dc3dcde to 77e31b8 Compare July 22, 2026 10:38
Comment thread lib/UserBackend.php
* resolved against the guests_users table.
*/
protected function potentialGuestUserId(string $userId): bool {
return str_contains($userId, '@') || preg_match('/^[a-f0-9]{64}$/', $userId);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer if the userId when converting to a guest user is changed instead of changing this method

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @CarlSchwan , I considered exactly that route and decided against it deliberately, so let me defend the design.

Changing the uid would turn this PR into the opposite of what it closes. #333 (open since 2020, never declined) asks for guests whose login name is not their email address. The registration approval flow is the concrete case: a person signs up with a username the admin deliberately allowed, the admin approves the account and converts it to a guest. Renaming it to a sha256 hash at that moment silently discards the name the person just registered with, and they would have to log in with their email instead. Admins who want uniform email logins already have that choice today, the registration app has a "Force email as login name" setting and even a login name policy regex. I would not want the guests app to override a decision the admin already made there.

There is also a technical reason the uid is preserved: core deliberately offers no uid rename, because the uid is a foreign key in core and app tables alike, group memberships, incoming shares, preferences provisioned before first login, and any other app's tables keyed by uid. An app-side rename can only cover the tables it knows about, everything else keeps pointing at a uid that no longer exists. Guillaume ruled out the direct DB edit in #333 for exactly this reason. Keeping the uid is what makes the conversion a safe, atomic operation, one insert, one delete, one transaction.

On potentialGuestUserId(): the email-or-hash shape was never a correctness boundary, guests_users is. Any regular account with an @ in its uid already passes the check today and falls through to the table lookup, and the email convention stays untouched for invited guests, where the email is the only identity that exists at creation time. The loosened check costs one indexed SELECT per unknown uid, and the miss is cached per request.

So keeping the uid stable is not an implementation shortcut, it is the point of the feature, and I would like to keep it that way.

// shares) stays valid.
$this->connection->beginTransaction();
try {
$insert = $this->connection->getQueryBuilder();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use:

		if ($this->config->useHashedEmailAsUserID()) {
			$email = strtolower($email);
			$username = hash('sha256', $email);
		} else {
			$username = $email;
		}

to create uid, same as in lib/Controller/UsersController.php and lib/Command/AddCommand.php

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Answered in the thread on UserBackend.php, keeping the uid is deliberate, see my reasoning there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Convert existing users to guest users Assign a specific username at guest creation

3 participants