-
Notifications
You must be signed in to change notification settings - Fork 33
feat: convert regular accounts to guests #1619
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| /** | ||
| * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors | ||
| * SPDX-License-Identifier: AGPL-3.0-or-later | ||
| */ | ||
|
|
||
| namespace OCA\Guests\Service; | ||
|
|
||
| use OCA\Guests\AppInfo\Application; | ||
| use OCA\Guests\ConfigLexicon; | ||
| use OCP\Config\IUserConfig; | ||
| use OCP\IDBConnection; | ||
| use OCP\IUser; | ||
|
|
||
| /** | ||
| * Converts a regular account into a guest account by moving it from the core | ||
| * "Database" user backend into the guests backend. | ||
| * | ||
| * The user ID is preserved, so all existing account data, home storage and | ||
| * mounts stay valid, and the password hash is carried over unchanged. The | ||
| * caller is responsible for checking eligibility (database backend, never | ||
| * logged in, not already a guest). | ||
| */ | ||
| class ConversionService { | ||
| public function __construct( | ||
| private readonly IDBConnection $connection, | ||
| private readonly IUserConfig $userConfig, | ||
| ) { | ||
| } | ||
|
|
||
| public function convertToGuest(IUser $user, IUser $createdBy): void { | ||
| $uid = $user->getUID(); | ||
| $uidLower = mb_strtolower($uid); | ||
| $displayName = $user->getDisplayName(); | ||
| $email = $user->getSystemEMailAddress() ?? ''; | ||
|
|
||
| // Carry over the existing password hash from the database backend. | ||
| $query = $this->connection->getQueryBuilder(); | ||
| $query->select('password') | ||
| ->from('users') | ||
| ->where($query->expr()->eq('uid_lower', $query->createNamedParameter($uidLower))); | ||
| $result = $query->executeQuery(); | ||
| $passwordHash = $result->fetchOne(); | ||
| $result->closeCursor(); | ||
| if ($passwordHash === false) { | ||
| throw new \RuntimeException('No password hash found for "' . $uid . '"'); | ||
| } | ||
|
|
||
| // Move the account between backends in a single transaction. The user ID | ||
| // is unchanged, so all data keyed by it (account, home storage, mounts, | ||
| // shares) stays valid. | ||
| $this->connection->beginTransaction(); | ||
| try { | ||
| $insert = $this->connection->getQueryBuilder(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use: to create uid, same as in lib/Controller/UsersController.php and lib/Command/AddCommand.php
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| $insert->insert('guests_users') | ||
| ->values([ | ||
| 'uid' => $insert->createNamedParameter($uid), | ||
| 'uid_lower' => $insert->createNamedParameter($uidLower), | ||
| 'displayname' => $insert->createNamedParameter($displayName), | ||
| 'password' => $insert->createNamedParameter($passwordHash), | ||
| 'email' => $insert->createNamedParameter($email), | ||
| ]); | ||
| $insert->executeStatement(); | ||
|
|
||
| $delete = $this->connection->getQueryBuilder(); | ||
| $delete->delete('users') | ||
| ->where($delete->expr()->eq('uid_lower', $delete->createNamedParameter($uidLower))); | ||
| $delete->executeStatement(); | ||
|
|
||
| $this->connection->commit(); | ||
| } catch (\Throwable $e) { | ||
| $this->connection->rollBack(); | ||
| throw $e; | ||
| } | ||
|
|
||
| $this->userConfig->setValueString($uid, Application::APP_ID, ConfigLexicon::USER_CREATED_BY, $createdBy->getUID()); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -330,8 +330,7 @@ private function loadUser($uid): bool { | |
| return false; | ||
| } | ||
|
|
||
| // guests $uid could be NULL or '' | ||
| // or is not an email anyway | ||
| // Skip empty IDs; any non-empty ID is resolved against the guests_users table. | ||
| if (!$this->potentialGuestUserId($uid)) { | ||
| $this->cache[$uid] = false; | ||
| return false; | ||
|
|
@@ -473,14 +472,10 @@ public function getRealUID(string $uid): string { | |
| } | ||
|
|
||
| /** | ||
| * Guest app user ids are: | ||
| * - either email addresses so they need to contain an @ | ||
| * - lowercase sha256 hashes of email addresses, 64 characters of a-f and 0-9 | ||
| * | ||
| * @param string $userId | ||
| * @return bool | ||
| * Guard against empty IDs only. Any non-empty ID may belong to a guest and is | ||
| * resolved against the guests_users table. | ||
| */ | ||
| protected function potentialGuestUserId(string $userId): bool { | ||
| return str_contains($userId, '@') || preg_match('/^[a-f0-9]{64}$/', $userId); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @CarlSchwan I think this is a very good explanation for @ernolf's decision, wdyt?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This means now all usage of IUserManager::getSeenUsers will query all the times the guest backend and various other methods in this manager will do the same |
||
| return $userId !== ''; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.