Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e2e0c793 authored by Ronak Patel's avatar Ronak Patel Committed by AVINASH GUSAIN
Browse files

Map the 'active' and 'mailActive' attributes between LDAP and eCloud

parent 154c56b1
Loading
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -27,5 +27,6 @@
    <commands>
        <command>OCA\EcloudAccounts\Command\Migrate2FASecrets</command>
        <command>OCA\EcloudAccounts\Command\MigrateWebmailAddressbooks</command>
        <command>OCA\EcloudAccounts\Command\MapActiveAttributetoLDAP</command>
    </commands>
</info>
+63 −0
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace OCA\EcloudAccounts\Command;

use Exception;
use OCA\EcloudAccounts\AppInfo\Application;
use OCA\EcloudAccounts\Service\UserService;
use OCP\ILogger;
use OCP\IUser;
use OCP\IUserManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class MapActiveAttributetoLDAP extends Command {
	private OutputInterface $commandOutput;
	private IUserManager $userManager;
	private $userService;
	private $logger;

	public function __construct(IUserManager $userManager, ILogger $logger, UserService $userService) {
		$this->userManager = $userManager;
		$this->userService = $userService;
		$this->logger = $logger;
		parent::__construct();
	}

	protected function configure(): void {
		$this
			->setName(Application::APP_ID.':map-active-attribute-to-ldap')
			->setDescription('Map Active attribute to LDAP');
	}

	protected function execute(InputInterface $input, OutputInterface $output): int {
		$this->commandOutput = $output;
		$this->userManager->callForSeenUsers(function (IUser $user) {
			if ($this->isUserValid($user)) {
				$username = $user->getUID();
				$isEnabled = $user->isEnabled() ? true : false;
				try {
					$this->userService->mapActiveAttributesInLDAP($username, $isEnabled);
				} catch (Exception $e) {
					$this->logger->logException('Failed to update LDAP attributes for user: ' . $username, ['exception' => $e]);
				}
			}
		});
		$this->commandOutput->writeln('Active attributes mapped successfully.');
		return 0;
	}
	/**
	 * validate user
	 *
	 * @param IUser $user
	 */
	private function isUserValid(?IUser $user) : bool {
		if (!($user instanceof IUser)) {
			return false;
		}
		return true;
	}
}
+18 −15
Original line number Diff line number Diff line
@@ -7,6 +7,7 @@ namespace OCA\EcloudAccounts\Listeners;
use Exception;
use OCA\EcloudAccounts\Db\MailboxMapper;
use OCA\EcloudAccounts\Service\LDAPConnectionService;
use OCA\EcloudAccounts\Service\UserService;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\ILogger;
@@ -18,6 +19,8 @@ class UserChangedListener implements IEventListener {

	private const RECOVERY_EMAIL_FEATURE = 'recovery-email';

	private const ENABLED_FEATURE = 'enabled';

	private $util;

	private $logger;
@@ -26,11 +29,14 @@ class UserChangedListener implements IEventListener {

	private $mailboxMapper;

	public function __construct(Util $util, LDAPConnectionService $LDAPConnectionService, ILogger $logger, MailboxMapper $mailboxMapper) {
	private $userService;

	public function __construct(Util $util, LDAPConnectionService $LDAPConnectionService, ILogger $logger, MailboxMapper $mailboxMapper, UserService $userService) {
		$this->util = $util;
		$this->ldapConnectionService = $LDAPConnectionService;
		$this->mailboxMapper = $mailboxMapper;
		$this->logger = $logger;
		$this->userService = $userService;
	}

	public function handle(Event $event): void {
@@ -41,6 +47,7 @@ class UserChangedListener implements IEventListener {
		$feature = $event->getFeature();
		$user = $event->getUser();
		$username = $user->getUID();
		$newValue = $event->getValue();
		
		if ($feature === self::QUOTA_FEATURE) {
			$updatedQuota = $event->getValue();
@@ -56,7 +63,15 @@ class UserChangedListener implements IEventListener {
				'recoveryMailAddress' => $recoveryEmail
			];

			$this->updateAttributesInLDAP($username, $recoveryEmailAttribute);
			$this->userService->updateAttributesInLDAP($username, $recoveryEmailAttribute);
		}

		if ($feature === self::ENABLED_FEATURE) {
			try {
				$this->userService->mapActiveAttributesInLDAP($username, $newValue);
			} catch (Exception $e) {
				$this->logger->logException('Failed to update LDAP attributes for user: ' . $username, ['exception' => $e]);
			}
		}
	}

@@ -69,22 +84,10 @@ class UserChangedListener implements IEventListener {
				$quotaAttribute = [
					'quota' => $quotaInBytes
				];
				$this->updateAttributesInLDAP($username, $quotaAttribute);
				$this->userService->updateAttributesInLDAP($username, $quotaAttribute);
			}
		} catch (Exception $e) {
			$this->logger->error("Error setting quota for user $username " . $e->getMessage());
		}
	}
	
	private function updateAttributesInLDAP(string $username, array $attributes) {
		if ($this->ldapConnectionService->isLDAPEnabled()) {
			$conn = $this->ldapConnectionService->getLDAPConnection();
			$userDn = $this->ldapConnectionService->username2dn($username);
			
			if (!ldap_modify($conn, $userDn, $attributes)) {
				throw new Exception('Could not modify user entry at LDAP server!');
			}
			$this->ldapConnectionService->closeLDAPConnection($conn);
		}
	}
}
+350 −0
Original line number Diff line number Diff line
@@ -203,4 +203,354 @@ class UserService {
			throw new \Exception("SendGrid API error - Status Code: " . $response->statusCode());
		}
	}
	/**
	 * Register a new user.
	 *
	 * @param string $displayname The display name of the user.
	 * @param string $recoveryemail The recovery email address for the user.
	 * @param string $username The chosen username for the user.
	 * @param string $userEmail The email address of the user.
	 * @param string $password The password chosen by the user.
	 *
	 * @return void
	 * @throws Exception If the username or recovery email is already taken.
	 * @throws LDAPUserCreationException If there is an error adding new entry to LDAP store
	 */
	public function registerUser(string $displayname, string $recoveryEmail, string $username, string $userEmail, string $password): void {
		
		if ($this->userExists($username)) {
			throw new Exception("Username '$username' is already taken.");
		}
		if (!empty($recoveryEmail)) {
			$this->validateRecoveryEmail($recoveryEmail);
		}
		$this->addNewUserToLDAP($displayname, $recoveryEmail, $username, $userEmail, $password);
	}
	/**
	 * Validates the recovery email address.
	 *
	 * @param string $recoveryEmail The recovery email address to be validated.
	 * @throws Exception If the recovery email address has an incorrect format, is already taken, or if the domain is disallowed.
	 * @return void
	 */
	public function validateRecoveryEmail(string $recoveryEmail): void {
		if (!$this->isValidEmailFormat($recoveryEmail)) {
			throw new Exception('Recovery email address has an incorrect format.');
		}
		if ($this->checkRecoveryEmailAvailable($recoveryEmail)) {
			throw new Exception('Recovery email address is already taken.');
		}
		if ($this->isRecoveryEmailDomainDisallowed($recoveryEmail)) {
			throw new Exception('You cannot set an email address with a Murena domain as recovery email address.');
		}
		if ($this->isBlacklistedEmail($recoveryEmail)) {
			throw new BlacklistedEmailException('The domain of this email address is blacklisted. Please provide another recovery address.');
		}
	}
	/**
	 * Check if an email domain is blacklisted against a JSON list of disposable email domains.
	 *
	 * @param string $email The email address to check.
	 * @return bool True if the email domain is blacklisted, false otherwise.
	 */
	public function isBlacklistedEmail(string $email): bool {
		// Get the blacklisted domains from configuration
		$blacklistedDomainsInJson = $this->config->getAppValue(Application::APP_ID, 'blacklisted_domains');
		$blacklistedDomains = json_decode($blacklistedDomainsInJson, true);
		
		// Split the email address into parts using explode
		$emailParts = explode('@', $email);
		
		// Extract the domain part
		$emailDomain = strtolower(end($emailParts));
		
		// Check if the email domain is in the blacklisted domains array
		return in_array($emailDomain, $blacklistedDomains);
	}
	/**
	 * Add a new user to the LDAP directory.
	 *
	 * @param string $displayname The display name of the new user.
	 * @param string $recoveryEmail The recovery email address of the new user.
	 * @param string $username The username of the new user.
	 * @param string $userEmail The email address of the new user.
	 * @param string $password The password of the new user.
	 *
	 * @return void
	 * @throws LDAPUserCreationException If there is an error adding new entry to LDAP store
	 */
	private function addNewUserToLDAP(string $displayName, string $recoveryEmail, string $username, string $userEmail, string $password): void {
		$connection = $this->LDAPConnectionService->getLDAPConnection();
		$base = $this->LDAPConnectionService->getLDAPBaseUsers()[0];
		
		$newUserDN = "username=$username," . $base;
		
		$quota = $this->getDefaultQuota() * 1024 * 1024;
		
		$newUserEntry = [
			'mailAddress' => $userEmail,
			'username' => $username,
			'usernameWithoutDomain' => $username,
			'userPassword' => $password,
			'displayName' => $displayName,
			'quota' => $quota,
			'recoveryMailAddress' => $recoveryEmail,
			'active' => 'TRUE',
			'mailActive' => 'TRUE',
			'userClusterID' => $this->apiConfig['userCluserId'],
			'objectClass' => $this->apiConfig['objectClass']
		];
		
		$ret = ldap_add($connection, $newUserDN, $newUserEntry);
		
		if (!$ret) {
			throw new LDAPUserCreationException("Error while adding entry to LDAP for username: " .  $username . ' Error: ' . ldap_error($connection), ldap_errno($connection));
		}
	}
	/**
	 * Check if a recovery email address is available (not already taken by another user).
	 *
	 * @param string $recoveryEmail The recovery email address to check.
	 *
	 * @return bool True if the recovery email address is available, false otherwise.
	 */
	public function checkRecoveryEmailAvailable(string $recoveryEmail): bool {
		$recoveryEmail = strtolower($recoveryEmail);
		$users = $this->config->getUsersForUserValue('email-recovery', 'recovery-email', $recoveryEmail);
		if(count($users)) {
			return true;
		}
		$users = $this->config->getUsersForUserValue('email-recovery', 'unverified-recovery-email', $recoveryEmail);
		if(count($users)) {
			return true;
		}
		return false;
	}

	/**
	 * Check if a recovery email address domain is restricted for some domains
	 *
	 * @param string $recoveryEmail The recovery email address to check.
	 *
	 * @return bool True if the recovery email address is disallowed, false otherwise.
	 */
	public function isRecoveryEmailDomainDisallowed(string $recoveryEmail): bool {
		
		$recoveryEmail = strtolower($recoveryEmail);
		
		$emailParts = explode('@', $recoveryEmail);
		$domain = $emailParts[1] ?? '';
		
		$legacyDomain = $this->getLegacyDomain();
		$mainDomain = $this->getMainDomain();
		
		$restrictedDomains = [ $legacyDomain, $mainDomain ];

		return in_array($domain, $restrictedDomains);
	}

	/**
	 * Check if a recovery email address is in valid format
	 *
	 * @param string $recoveryEmail The recovery email address to check.
	 *
	 * @return bool True if the recovery email address is valid, false otherwise.
	 */
	public function isValidEmailFormat(string $recoveryEmail): bool {
		return filter_var($recoveryEmail, FILTER_VALIDATE_EMAIL) !== false;
	}

	/**
	 * Create a Hide My Email (HME) alias for a user.
	 *
	 * @param string $username The username for which to create the HME alias.
	 * @param string $resultmail The email address associated with the HME alias.
	 *
	 * @return void
	 */
	public function createHMEAlias(string $username, string $resultmail): void {
		$commonServicesURL = $this->apiConfig['commonServicesURL'];
		$aliasDomain = $this->apiConfig['aliasDomain'];
		$token = $this->apiConfig['commonServicesToken'];
		$commonApiVersion = $this->apiConfig['commonApiVersion'];
		
		$endpoint = $commonApiVersion . '/aliases/hide-my-email/';
		$url = $commonServicesURL . $endpoint . $resultmail;
		$data = array(
			"domain" => $aliasDomain
		);
		$headers = [
			"Authorization: Bearer $token"
		];
		$result = $this->curl->post($url, $data, $headers);
		$result = json_decode($result, true);

		$hmeAlias = isset($result['emailAlias']) ? $result['emailAlias'] : '';
		if($hmeAlias != '') {
			$hmeAliasAdded = $this->addHMEAliasInConfig($username, $hmeAlias);
			if (!$hmeAliasAdded) {
				$this->logger->error("Failed to add HME Alias '$hmeAlias' for username '$username' in config.");
			}
		} else {
			$this->logger->error("Failed to create HME Alias for username '$username'. Response: " . json_encode($result));
		}
	}
	/**
	 * Create a new domain alias for a user.
	 *
	 * @param string $username The username for which to create the domain alias.
	 * @param string $userEmail The email address associated with the domain alias.
	 *
	 * @return mixed The result of the domain alias creation request, decoded from JSON.
	 */
	public function createNewDomainAlias(string $username, string $userEmail): mixed {
		$commonServicesURL = $this->apiConfig['commonServicesURL'];
		$commonApiVersion = $this->config->getSystemValue('commonApiVersion', '');
		$domain = $this->apiConfig['mainDomain'];
		$token = $this->apiConfig['commonServicesToken'];
		$commonApiVersion = $this->apiConfig['commonApiVersion'];

		$endpoint = $commonApiVersion . '/aliases/';
		$url = $commonServicesURL . $endpoint . $userEmail;

		$data = array(
			"alias" => $username,
			"domain" => $domain
		);
		$headers = [
			"Authorization: Bearer $token"
		];
		
		$result = $this->curl->post($url, $data, $headers);
		$result = json_decode($result, true);
		if ($this->curl->getLastStatusCode() !== 200) {
			$this->logger->error("Failed to create new domain alias '$username' for email '$userEmail'.");
		}
		return $result;
	}
	/**
	 * Set account data locally for a user.
	 *
	 * @param string $uid The unique identifier of the user.
	 * @param string $mailAddress The email address to set for the user.
	 * @param string $quota The quota to set for the user (in megabytes).
	 *
	 * @return void
	 */
	public function setAccountDataLocally(string $uid, string $mailAddress): void {
		$user = $this->getUser($uid);
		if (is_null($user)) {
			throw new Exception("User with username '$uid' not found.");
		}
		// Set the email address for the user
		$user->setEMailAddress($mailAddress);
		$quota = $this->getDefaultQuota();
		// Format and set the quota for the user (in megabytes)
		$quota = strval($quota) . ' MB';
		$user->setQuota($quota);
	}

	public function isUsernameTaken(string $username) : bool {
		$commonServicesURL = $this->apiConfig['commonServicesURL'];
		$commonApiVersion = $this->apiConfig['commonApiVersion'];

		if (!isset($commonServicesURL) || empty($commonServicesURL)) {
			return false;
		}
		$endpoint = $commonApiVersion . '/users/';
		$url = $commonServicesURL . $endpoint . $username;

		$token = $this->apiConfig['commonServicesToken'];
		$headers = [
			"Authorization: Bearer $token"
		];

		$this->curl->get($url, [], $headers);

		$statusCode = $this->curl->getLastStatusCode();
		if ($statusCode === 404) {
			return false;
		}

		if ($statusCode === 200) {
			return true;
		}
		throw new Exception("Error checking if username '$username' is taken at common source, status code: " . (string) $statusCode);
	}
	/**
	 * Adds a username to the common data store.
	 *
	 * This method sends a POST request to the common data store API endpoint to add a username.
	 * If the operation is successful, the username will be added to the data store.
	 * If the operation fails, an exception will be thrown.
	 *
	 * @param string $username The username to add to the common data store.
	 *
	 * @throws AddUsernameToCommonStoreException If an error occurs while adding the username to the common data store.
	 */
	public function addUsernameToCommonDataStore(string $username) : void {
		$commonServicesURL = $this->apiConfig['commonServicesURL'];
		$commonApiVersion = $this->apiConfig['commonApiVersion'];

		if (!isset($commonServicesURL) || empty($commonServicesURL)) {
			return;
		}
		$endpoint = $commonApiVersion . '/users/';
		$url = $commonServicesURL . $endpoint ;
		
		$params = [
			'username' => $username
		];

		$token = $this->apiConfig['commonServicesToken'];
		$headers = [
			"Authorization: Bearer $token"
		];
		
		$this->curl->post($url, $params, $headers);

		if ($this->curl->getLastStatusCode() !== 200) {
			throw new AddUsernameToCommonStoreException("Error adding username '$username' to common data store.");
		}
	}

	public function mapActiveAttributesInLDAP(string $username, bool $isEnabled): void {
		$userActiveAttributes = $this->getActiveAttributes($isEnabled);
		$this->updateAttributesInLDAP($username, $userActiveAttributes);
	}

	private function getActiveAttributes(bool $isEnabled): array {
		return [
			'active' => $isEnabled ? 'TRUE' : 'FALSE',
			'mailActive' => $isEnabled ? 'TRUE' : 'FALSE',
		];
	}

	public function updateAttributesInLDAP(string $username, array $attributes): void {
		if (!$this->LDAPConnectionService->isLDAPEnabled()) {
			return;
		}
	
		$conn = $this->LDAPConnectionService->getLDAPConnection();
		$userDn = $this->LDAPConnectionService->username2dn($username);
	
		if ($userDn === false) {
			throw new Exception('Could not find DN for username: ' . $username);
		}
	
		if (!ldap_modify($conn, $userDn, $attributes)) {
			throw new Exception('Could not modify user ' . $username . ' entry at LDAP server. Attributes: ' . print_r($attributes, true));
		}
	
		$this->LDAPConnectionService->closeLDAPConnection($conn);
	}
	
	private function getDefaultQuota() {
		return $this->config->getSystemValueInt('default_quota_in_megabytes', 1024);
	}
	public function updateBlacklistedDomains() {
		$blacklisted_domain_url = 'https://raw.githubusercontent.com/disposable/disposable-email-domains/master/domains.json';
		$json_data = file_get_contents($blacklisted_domain_url);
		$this->config->setAppValue(Application::APP_ID, 'blacklisted_domains', $json_data);
	}
}