diff --git a/lib/Listeners/UserChangedListener.php b/lib/Listeners/UserChangedListener.php index f720dc38ef4ede75d6f3f2e208f2a64ce1ffbae3..5d68bf0d729fd6999a0451ffbfb121d90edd5941 100644 --- a/lib/Listeners/UserChangedListener.php +++ b/lib/Listeners/UserChangedListener.php @@ -7,6 +7,7 @@ namespace OCA\EcloudAccounts\Listeners; use Exception; use OCA\EcloudAccounts\Db\MailboxMapper; use OCA\EcloudAccounts\Service\LDAPConnectionService; +use OCA\EcloudAccounts\Service\SSOService; use OCA\EcloudAccounts\Service\UserService; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; @@ -26,13 +27,15 @@ class UserChangedListener implements IEventListener { private $userService; private $LDAPConnectionService; + private $ssoService; - public function __construct(Util $util, LoggerInterface $logger, MailboxMapper $mailboxMapper, UserService $userService, LDAPConnectionService $LDAPConnectionService) { + public function __construct(Util $util, LoggerInterface $logger, MailboxMapper $mailboxMapper, UserService $userService, LDAPConnectionService $LDAPConnectionService, SSOService $ssoService) { $this->util = $util; $this->mailboxMapper = $mailboxMapper; $this->logger = $logger; $this->userService = $userService; $this->LDAPConnectionService = $LDAPConnectionService; + $this->ssoService = $ssoService; } public function handle(Event $event): void { @@ -44,37 +47,99 @@ class UserChangedListener implements IEventListener { $user = $event->getUser(); $username = $user->getUID(); $newValue = $event->getValue(); + $backend = $user->getBackend()->getBackendName(); + + $this->logger->error("[UserChangedListener] Event received - User: $username, Feature: $feature, Backend: $backend, Value type: " . gettype($newValue) . ", Value: " . var_export($newValue, true)); if ($feature === self::QUOTA_FEATURE) { + $this->logger->error("[UserChangedListener] Processing QUOTA_FEATURE for user: $username"); $updatedQuota = $event->getValue(); $quotaInBytes = (int) $this->util->computerFileSize($updatedQuota); - $backend = $user->getBackend()->getBackendName(); + $this->logger->error("[UserChangedListener] Quota update - User: $username, Quota: $updatedQuota, Bytes: $quotaInBytes, Backend: $backend"); $this->updateQuota($username, $backend, $quotaInBytes); } if ($feature === self::ENABLED_FEATURE) { + $this->logger->error("[UserChangedListener] Processing ENABLED_FEATURE for user: $username"); + $this->logger->error("[UserChangedListener] Event value - Type: " . gettype($newValue) . ", Value: " . var_export($newValue, true)); + + // Convert $newValue to boolean - handle various types that might be returned + $isEnabled = false; + $conversionMethod = 'unknown'; + + if ($newValue !== null) { + if (is_bool($newValue)) { + $isEnabled = $newValue; + $conversionMethod = 'direct_bool'; + } elseif (is_string($newValue)) { + $isEnabled = filter_var($newValue, FILTER_VALIDATE_BOOLEAN); + $conversionMethod = 'filter_var_string'; + } elseif (is_numeric($newValue)) { + $isEnabled = (bool) $newValue; + $conversionMethod = 'cast_numeric'; + } else { + // Fallback to user's actual enabled state + $isEnabled = $user->isEnabled(); + $conversionMethod = 'fallback_user_state'; + $this->logger->error("[UserChangedListener] Unexpected value type (" . gettype($newValue) . ") for user: $username, using user->isEnabled() instead"); + } + } else { + // If value is null, use user's actual enabled state + $isEnabled = $user->isEnabled(); + $conversionMethod = 'null_fallback_user_state'; + $this->logger->error("[UserChangedListener] Event value is null for user: $username, using user->isEnabled() instead"); + } + + // Get user's current enabled state for comparison + $userCurrentEnabled = $user->isEnabled(); + $this->logger->error("[UserChangedListener] Conversion result - Method: $conversionMethod, Final enabled value: " . ($isEnabled ? 'true' : 'false') . ", user->isEnabled(): " . ($userCurrentEnabled ? 'true' : 'false')); + + // Step 1: Update LDAP attributes (active, mailActive) + $this->logger->error("[UserChangedListener] Step 1: Updating LDAP attributes for user: $username, enabled: " . ($isEnabled ? 'true' : 'false')); + try { + $this->userService->mapActiveAttributesInLDAP($username, $isEnabled); + $this->logger->error("[UserChangedListener] Step 1: Successfully updated LDAP attributes for user: $username, enabled: " . ($isEnabled ? 'true' : 'false')); + } catch (Exception $e) { + $this->logger->error("[UserChangedListener] Step 1: Failed to update LDAP attributes for user: $username", ['exception' => $e]); + } + + // Step 2: Update Keycloak Admin UI enabled status (this is the ONLY way to update Keycloak's internal enabled flag) + $this->logger->error("[UserChangedListener] Step 2: Updating Keycloak enabled status for user: $username, enabled: " . ($isEnabled ? 'true' : 'false')); try { - $this->userService->mapActiveAttributesInLDAP($username, $newValue); + $keycloakUpdateSuccess = $this->ssoService->updateUserEnabledStatus($username, $isEnabled); + if ($keycloakUpdateSuccess) { + $this->logger->error("[UserChangedListener] Step 2: Successfully updated Keycloak enabled status for user: $username"); + } else { + $this->logger->error("[UserChangedListener] Step 2: Keycloak update failed for user: $username (check SSOService logs for details)"); + } } catch (Exception $e) { - $this->logger->error('Failed to update LDAP attributes for user: ' . $username, ['exception' => $e]); + $this->logger->error("[UserChangedListener] Step 2: Exception caught while updating Keycloak enabled status for user: $username", ['exception' => $e]); + // Don't throw - LDAP update is more critical, Keycloak update failure shouldn't break the flow } + + $this->logger->error("[UserChangedListener] Completed ENABLED_FEATURE processing for user: $username"); } } private function updateQuota(string $username, string $backend, int $quotaInBytes) { + $this->logger->error("[UserChangedListener] updateQuota called - User: $username, Backend: $backend, Quota bytes: $quotaInBytes"); try { if ($backend === 'SQL raw') { + $this->logger->error("[UserChangedListener] Updating quota in SQL raw backend for user: $username"); $this->mailboxMapper->updateMailboxQuota($username, $quotaInBytes); + $this->logger->error("[UserChangedListener] Successfully updated quota in SQL raw for user: $username"); } if ($backend === 'LDAP') { + $this->logger->error("[UserChangedListener] Updating quota in LDAP backend for user: $username"); $quotaAttribute = [ 'quota' => $quotaInBytes ]; $this->LDAPConnectionService->updateAttributesInLDAP($username, $quotaAttribute); + $this->logger->error("[UserChangedListener] Successfully updated quota in LDAP for user: $username"); } } catch (Exception $e) { - $this->logger->error("Error setting quota for user $username " . $e->getMessage()); + $this->logger->error("[UserChangedListener] Error setting quota for user $username: " . $e->getMessage(), ['exception' => $e]); } } } diff --git a/lib/Service/SSOService.php b/lib/Service/SSOService.php index 89d49067a8e0816494b3ff0ddd74e446f7472e1b..05193439daf34c7c11a5e21b48029b508a0ed8ca 100644 --- a/lib/Service/SSOService.php +++ b/lib/Service/SSOService.php @@ -85,7 +85,7 @@ class SSOService { 'credentials' => [$credentialEntry] ]; - $this->logger->debug('migrateCredential calling SSO API with url: '. $url . ' and data: ' . print_r($data, true)); + $this->logger->error('migrateCredential calling SSO API with url: '. $url . ' and data: ' . print_r($data, true)); $this->callSSOAPI($url, 'PUT', $data, 204); } @@ -100,7 +100,7 @@ class SSOService { $url = $this->ssoConfig['admin_rest_api_url'] . self::CREDENTIALS_ENDPOINT; $url = str_replace('{USER_ID}', $this->currentUserId, $url); $url .= '/' . $credentialId; - $this->logger->debug('deleteCredentials calling SSO API with url: '. $url); + $this->logger->error('deleteCredentials calling SSO API with url: '. $url); $this->callSSOAPI($url, 'DELETE', [], 204); } } @@ -113,7 +113,7 @@ class SSOService { $url = $this->ssoConfig['admin_rest_api_url'] . self::USERS_ENDPOINT . '/' . $this->currentUserId . '/logout'; - $this->logger->debug('logout calling SSO API with url: '. $url); + $this->logger->error('logout calling SSO API with url: '. $url); $this->callSSOAPI($url, 'POST', [], 204); // Revoke offline sessions for all clients @@ -159,7 +159,7 @@ class SSOService { $clientsCache = $this->cacheFactory->createDistributed('ecloud_accounts_realm_clients'); $cachedClients = $clientsCache->get($cacheKey); if ($cachedClients !== null) { - $this->logger->debug('Returning cached clients from distributed cache'); + $this->logger->error('Returning cached clients from distributed cache'); return $cachedClients; } @@ -193,7 +193,7 @@ class SSOService { // Get offline sessions for this client and user $offlineSessions = $this->getOfflineSessionsForClient($clientUUID); if (empty($offlineSessions)) { - $this->logger->debug('No offline sessions found for client: ' . $clientUUID); + $this->logger->error('No offline sessions found for client: ' . $clientUUID); return; } @@ -253,10 +253,43 @@ class SSOService { $this->migrateCredential($username, $secret); } + /** + * Update user's enabled status in Keycloak Admin UI + * This is the ONLY way to update Keycloak's internal "enabled" flag + * + * @param string $username The Nextcloud username + * @param bool $enabled Whether the user should be enabled (true) or disabled (false) + * @return bool True if successful, false otherwise + */ + public function updateUserEnabledStatus(string $username, bool $enabled): bool { + try { + if ($this->isNotCurrentUser($username)) { + $this->setupUserId($username); + } + + $url = $this->ssoConfig['admin_rest_api_url'] . self::USERS_ENDPOINT . '/' . $this->currentUserId; + + // Only send what we need + $this->logger->error("[SSOService] Updating Keycloak enabled status for user: $username, enabled: " . ($enabled ? 'true' : 'false') . ", Keycloak User ID: " . $this->currentUserId); + $this->callSSOAPI($url, 'PUT', [ + 'enabled' => $enabled, + ], 204); + + $this->logger->error("[SSOService] Keycloak user {$username} enabled=" . ($enabled ? 'true' : 'false')); + return true; + + } catch (\Exception $e) { + $this->logger->error("[SSOService] Failed to update Keycloak enabled status for user: $username", [ + 'exception' => $e + ]); + return false; + } + } + private function getCredentialIds() : array { $url = $this->ssoConfig['admin_rest_api_url'] . self::CREDENTIALS_ENDPOINT; $url = str_replace('{USER_ID}', $this->currentUserId, $url); - $this->logger->debug('getCredentialIds calling SSO API with url: '. $url); + $this->logger->error('getCredentialIds calling SSO API with url: '. $url); $credentials = $this->callSSOAPI($url, 'GET'); @@ -326,7 +359,7 @@ class SSOService { } $url = $this->ssoConfig['admin_rest_api_url'] . self::USERS_ENDPOINT . '?exact=true&email=' . $email; - $this->logger->debug('getUserId calling SSO API with url: '. $url); + $this->logger->error('getUserId calling SSO API with url: '. $url); $users = $this->callSSOAPI($url, 'GET'); if (empty($users) || !is_array($users) || !isset($users[0])) { throw new SSOAdminAPIException('Error: no user found for search with url: ' . $url); @@ -378,7 +411,7 @@ class SSOService { $headers = [ 'Content-Type: application/x-www-form-urlencoded' ]; - $this->logger->debug('getAdminAccessToken calling SSO API with url: '. $adminAccessTokenRoute . ' and headers: ' . print_r($headers, true) . ' and body: ' . print_r($requestBody, true)); + $this->logger->error('getAdminAccessToken calling SSO API with url: '. $adminAccessTokenRoute . ' and headers: ' . print_r($headers, true) . ' and body: ' . print_r($requestBody, true)); $response = $this->curl->post($adminAccessTokenRoute, $requestBody, $headers); if ($this->curl->getLastStatusCode() !== 200) { @@ -424,7 +457,31 @@ class SSOService { $statusCode = $this->curl->getLastStatusCode(); if ($statusCode !== $expectedStatusCode) { - throw new SSOAdminAPIException('Error calling SSO API with url ' . $url . ' status code: ' . $statusCode); + // Try to parse error response from Keycloak + $errorMessage = 'Error calling SSO API with url ' . $url . ' status code: ' . $statusCode; + if (!empty($answer)) { + $errorResponse = json_decode($answer, true); + if (is_array($errorResponse)) { + if (isset($errorResponse['errorMessage'])) { + $errorMessage .= ' - ' . $errorResponse['errorMessage']; + } elseif (isset($errorResponse['error'])) { + $errorMessage .= ' - ' . $errorResponse['error']; + } elseif (isset($errorResponse['message'])) { + $errorMessage .= ' - ' . $errorResponse['message']; + } + // Log full response for debugging + $this->logger->error("[SSOService] Keycloak API error response: " . json_encode($errorResponse)); + } else { + // Log raw response if not JSON + $this->logger->error("[SSOService] Keycloak API error response (raw): " . substr($answer, 0, 500)); + } + } + throw new SSOAdminAPIException($errorMessage); + } + + // For 204 No Content responses, return null instead of trying to decode JSON + if ($statusCode === 204 || empty($answer)) { + return null; } $answer = json_decode($answer, true);