* @copyright Copyright (c) 2017 Pellaeon Lin * @copyright Copyright (c) 2017 Lukas Reschke * * @author Julius Härtl * @author Pellaeon Lin * @author Lukas Reschke * * @license GNU AGPL version 3 or any later version * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License 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 License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ namespace OCA\Registration\Service; use InvalidArgumentException; use libphonenumber\NumberParseException; use libphonenumber\PhoneNumber; use libphonenumber\PhoneNumberUtil; use OC\Authentication\Exceptions\InvalidTokenException; use OC\Authentication\Exceptions\PasswordlessTokenException; use OC\Authentication\Token\IProvider; use OC\Authentication\Token\IToken; use OCA\Registration\AppInfo\Application; use OCA\Registration\Db\Registration; use OCA\Registration\Db\RegistrationMapper; use OCP\Accounts\IAccountManager; use OCP\AppFramework\Db\DoesNotExistException; use OCP\IRequest; use OCP\ISession; use OCP\IURLGenerator; use OCP\IUser; use OCP\Security\ICrypto; use OCP\Session\Exceptions\SessionNotAvailableException; use \OCP\IUserManager; use \OCP\IUserSession; use \OCP\IGroupManager; use \OCP\IL10N; use \OCP\IConfig; use \OCP\Security\ISecureRandom; use Psr\Log\LoggerInterface; class RegistrationService { /** @var string */ private $appName; /** @var MailService */ private $mailService; /** @var IL10N */ private $l10n; /** @var IURLGenerator */ private $urlGenerator; /** @var RegistrationMapper */ private $registrationMapper; /** @var IUserManager */ private $userManager; /** @var IAccountManager */ private $accountManager; /** @var IConfig */ private $config; /** @var IGroupManager */ private $groupManager; /** @var ISecureRandom */ private $random; /** @var IUserSession */ private $userSession; /** @var IRequest */ private $request; /** @var LoggerInterface */ private $logger; /** @var ISession */ private $session; /** @var IProvider */ private $tokenProvider; /** @var ICrypto */ private $crypto; public function __construct( string $appName, MailService $mailService, IL10N $l10n, IURLGenerator $urlGenerator, RegistrationMapper $registrationMapper, IUserManager $userManager, IAccountManager $accountManager, IConfig $config, IGroupManager $groupManager, ISecureRandom $random, IUserSession $userSession, IRequest $request, LoggerInterface $logger, ISession $session, IProvider $tokenProvider, ICrypto $crypto ) { $this->appName = $appName; $this->mailService = $mailService; $this->l10n = $l10n; $this->urlGenerator = $urlGenerator; $this->registrationMapper = $registrationMapper; $this->userManager = $userManager; $this->accountManager = $accountManager; $this->config = $config; $this->groupManager = $groupManager; $this->random = $random; $this->userSession = $userSession; $this->request = $request; $this->logger = $logger; $this->session = $session; $this->tokenProvider = $tokenProvider; $this->crypto = $crypto; } public function confirmEmail(Registration $registration): void { $registration->setEmailConfirmed(true); $this->registrationMapper->update($registration); } public function generateNewToken(Registration $registration): void { $this->registrationMapper->generateNewToken($registration); $this->registrationMapper->update($registration); } /** * Create registration request, used by both the API and form * @param string $email * @param string $username * @param string $password * @param string $displayname * @return Registration */ public function createRegistration(string $email, string $username = '', string $password = '', string $displayname = ''): Registration { $registration = new Registration(); $registration->setEmail($email); $registration->setUsername($username); $registration->setDisplayname($displayname); if ($password !== '') { $password = $this->crypto->encrypt($password); $registration->setPassword($password); } $this->registrationMapper->generateNewToken($registration); $this->registrationMapper->generateClientSecret($registration); $this->registrationMapper->insert($registration); return $registration; } /** * @param string $email * @throws RegistrationException */ public function validateEmail(string $email): void { $this->mailService->validateEmail($email); // check for pending registrations try { $this->registrationMapper->find($email);//if not found DB will throw a exception throw new RegistrationException( $this->l10n->t('A user has already taken this email, maybe you already have an account?') ); } catch (DoesNotExistException $e) { } if ($this->userManager->getByEmail($email)) { throw new RegistrationException( $this->l10n->t('A user has already taken this email, maybe you already have an account?'), $this->l10n->t('You can log in now.', [$this->urlGenerator->getAbsoluteURL('/')]) ); } if ($this->config->getAppValue($this->appName, 'allowed_domains', '') === '') { return; } $emailIsInDomainList = $this->checkAllowedDomains($email); $blockDomains = $this->config->getAppValue(Application::APP_ID, 'domains_is_blocklist', 'no') === 'yes'; $showDomains = $this->config->getAppValue(Application::APP_ID, 'show_domains', 'no') === 'yes'; if (!$blockDomains && !$emailIsInDomainList) { if ($showDomains) { throw new RegistrationException( $this->l10n->t( 'Registration is only allowed with the following domains:' ) . ' ' . implode(', ', explode(';', $this->config->getAppValue(Application::APP_ID, 'allowed_domains', '') )) ); } throw new RegistrationException( $this->l10n->t('Registration with this email domain is not allowed.') ); } if ($blockDomains && $emailIsInDomainList) { if ($showDomains) { throw new RegistrationException( $this->l10n->t( 'Registration is not allowed with the following domains:' ) . ' ' . implode(', ', explode(';', $this->config->getAppValue(Application::APP_ID, 'allowed_domains', '') )) ); } throw new RegistrationException( $this->l10n->t('Registration with this email domain is not allowed.') ); } } /** * @param string $displayname * @throws RegistrationException */ public function validateDisplayname(string $displayname): void { if ($displayname === '') { throw new RegistrationException($this->l10n->t('Please provide a valid display name.')); } } /** * @param string $username * @throws RegistrationException */ public function validateUsername(string $username): void { if ($username === '') { throw new RegistrationException($this->l10n->t('Please provide a valid login name.')); } $regex = $this->config->getAppValue($this->appName, 'username_policy_regex', ''); if ($regex && preg_match($regex, $username) === 0) { throw new RegistrationException($this->l10n->t('Please provide a valid login name.')); } if ($this->registrationMapper->usernameIsPending($username) || $this->userManager->get($username) !== null) { throw new RegistrationException($this->l10n->t('The login name you have chosen already exists.')); } } /** * @param string $phone * @throws RegistrationException */ public function validatePhoneNumber(string $phone): void { $defaultRegion = $this->config->getSystemValueString('default_phone_region', ''); if ($defaultRegion === '') { // When no default region is set, only +49… numbers are valid if (strpos($phone, '+') !== 0) { throw new RegistrationException($this->l10n->t('The phone number needs to contain the country code.')); } $defaultRegion = 'EN'; } $phoneUtil = PhoneNumberUtil::getInstance(); try { $phoneNumber = $phoneUtil->parse($phone, $defaultRegion); if (!$phoneNumber instanceof PhoneNumber || !$phoneUtil->isValidNumber($phoneNumber)) { throw new RegistrationException($this->l10n->t('The phone number is invalid.')); } } catch (NumberParseException $e) { throw new RegistrationException($this->l10n->t('The phone number is invalid.')); } } /** * check if email domain is allowed * * @param string $email * @return bool */ public function checkAllowedDomains(string $email): bool { $allowedDomains = $this->config->getAppValue($this->appName, 'allowed_domains', ''); if ($allowedDomains !== '') { [,$mailDomain] = explode('@', strtolower($email), 2); $allowedDomains = explode(';', strtolower($allowedDomains)); foreach ($allowedDomains as $domain) { // valid domain, everything's fine // Wildcards if (strpos($domain, '*') !== false) { // *.example.com // Make save for regex: // \*\.example\.com $regexDomain = preg_quote($domain, '\\'); // Replace "\*" with an actual regex wildcard and set start and end: // /^.+\.example\.com$/ $regexDomain = '/^' . str_replace('\\*', '.+', $regexDomain) . '$/'; if (preg_match($regexDomain, $mailDomain)) { return true; } } elseif ($mailDomain === $domain) { return true; } } return false; } return true; } /** * @return string[] */ public function getAllowedDomains(): array { $allowedDomains = $this->config->getAppValue($this->appName, 'allowed_domains', ''); $allowedDomains = explode(';', $allowedDomains); return $allowedDomains; } /** * @param Registration $registration * @param string|null $loginName * @param string|null $fullName * @param string|null $phone * @param string|null $password * @return IUser * @throws RegistrationException|InvalidArgumentException */ public function createAccount(Registration $registration, ?string $loginName = null, ?string $fullName = null, ?string $phone = null, ?string $password = null): IUser { if ($loginName === null) { $loginName = $registration->getUsername(); } if ($registration->getPassword() !== null) { $password = $this->crypto->decrypt($registration->getPassword()); } if (!$password) { throw new RegistrationException($this->l10n->t('Please provide a password.')); } $this->validateUsername($loginName); if ($this->config->getAppValue('registration', 'show_fullname', 'no') === 'yes' && $this->config->getAppValue('registration', 'enforce_fullname', 'no') === 'yes') { $this->validateDisplayname($fullName); } if (class_exists(PhoneNumberUtil::class) && $this->config->getAppValue('registration', 'show_phone', 'no') === 'yes') { if ($phone) { $this->validatePhoneNumber($phone); } elseif ($this->config->getAppValue('registration', 'enforce_phone', 'no') === 'yes') { throw new RegistrationException($this->l10n->t('Please provide a valid phone number.')); } } /* TODO * createUser tests username validity once, but validateUsername already checked it, * but createUser doesn't check if there is a pending registration with that name * * And validateUsername will throw RegistrationException while * createUser throws \InvalidArgumentException */ $user = $this->userManager->createUser($loginName, $password); if ($user === false) { throw new RegistrationException($this->l10n->t('Unable to create user, there are problems with the user backend.')); } $userId = $user->getUID(); // Set user email try { $user->setEMailAddress($registration->getEmail()); } catch (\Exception $e) { throw new RegistrationException($this->l10n->t('Unable to set user email: ' . $e->getMessage())); } // Set display name if ($fullName && $this->config->getAppValue('registration', 'show_fullname', 'no') === 'yes') { $user->setDisplayName($fullName); } // Set phone number in account data if (method_exists($this->accountManager, 'updateAccount') && $phone && $this->config->getAppValue('registration', 'show_phone', 'no') === 'yes') { $account = $this->accountManager->getAccount($user); $property = $account->getProperty(IAccountManager::PROPERTY_PHONE); $account->setProperty( IAccountManager::PROPERTY_PHONE, $phone, $property->getScope(), IAccountManager::NOT_VERIFIED ); $this->accountManager->updateAccount($account); } $groupId = ''; // Add user to group // This is not necessary for DA - here we want to create a new group for a new Agency! /* $registeredUserGroup = $this->config->getAppValue($this->appName, 'registered_user_group', 'none'); if ($registeredUserGroup !== 'none') { $group = $this->groupManager->get($registeredUserGroup); if ($group === null) { // This might happen if $registered_user_group is deleted after setting the value // Here I choose to log error instead of stopping the user to register $this->logger->error("You specified newly registered users be added to '$registeredUserGroup' group, but it does not exist."); $groupId = ''; } else { $group->addUser($user); $groupId = $group->getGID(); } } else { $groupId = ''; } */ // Create a new Group for the Agency and adding the new user $newgid = "agency_"; $newgid_counter = 1; $groupidfree = false; while($groupidfree == false){ // Find the next free Agency-ID if($this->groupManager->groupExists($newgid.$newgid_counter) == true){ $newgid_counter += 1; } else{ $groupidfree = true; } } $newgroup = $this->groupManager->createGroup($newgid.$newgid_counter); $newgroup->addUser($user); // disable user if this is requested by config $adminApprovalRequired = $this->config->getAppValue($this->appName, 'admin_approval_required', 'no'); if ($adminApprovalRequired === 'yes') { $user->setEnabled(false); } $this->mailService->notifyAdmins($userId, $user->isEnabled(), $groupId); return $user; } /** * @param string $email * @return Registration * @throws DoesNotExistException */ public function getRegistrationForEmail(string $email): Registration { return $this->registrationMapper->find($email); } /** * @param string $secret * @return Registration * @throws DoesNotExistException */ public function getRegistrationForSecret(string $secret): Registration { return $this->registrationMapper->findBySecret($secret); } public function deleteRegistration(Registration $registration): void { $this->registrationMapper->delete($registration); } /** * Return a 25 digit device password * * Example: AbCdE-fGhIj-KlMnO-pQrSt-12345 * * @return string */ private function generateRandomDeviceToken(): string { $groups = []; for ($i = 0; $i < 5; $i++) { $groups[] = $this->random->generate(5, ISecureRandom::CHAR_HUMAN_READABLE); } return implode('-', $groups); } /** * @param string $uid * @return string * @throws RegistrationException */ public function generateAppPassword(string $uid): string { $name = $this->l10n->t('Registration app auto setup'); try { $sessionId = $this->session->getId(); } catch (SessionNotAvailableException $ex) { throw new RegistrationException('Failed to generate an app token.'); } try { $sessionToken = $this->tokenProvider->getToken($sessionId); $loginName = $sessionToken->getLoginName(); try { $password = $this->tokenProvider->getPassword($sessionToken, $sessionId); } catch (PasswordlessTokenException $ex) { $password = null; } } catch (InvalidTokenException $ex) { throw new RegistrationException('Failed to generate an app token.'); } $token = $this->generateRandomDeviceToken(); $this->tokenProvider->generateToken($token, $uid, $loginName, $password, $name, IToken::PERMANENT_TOKEN); return $token; } /** * @param string $userId * @param string $username * @param string $password * @param bool $decrypt */ public function loginUser(string $userId, string $username, string $password, bool $decrypt = false): void { if ($decrypt) { $password = $this->crypto->decrypt($password); } $this->userSession->login($username, $password); $this->userSession->createSessionToken($this->request, $userId, $username, $password); } }