diff --git a/lib/Controller/ContactpersonenController.php b/lib/Controller/ContactpersonenController.php index 91ce0220..31f8dd17 100644 --- a/lib/Controller/ContactpersonenController.php +++ b/lib/Controller/ContactpersonenController.php @@ -264,7 +264,6 @@ function ($group) { * * @return JSONResponse Result of user creation. * - * @NoAdminRequired * @NoCSRFRequired * * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -274,21 +273,30 @@ function ($group) { */ public function convertToUser(string $contactpersoonId): JSONResponse { - if ($this->userSession->getUser() === null) { + $currentUser = $this->userSession->getUser(); + if ($currentUser === null) { return new JSONResponse(['message' => 'Not authenticated'], Http::STATUS_UNAUTHORIZED); } + // Only admins and org-admins may create user accounts. + $isAdmin = $this->groupManager->isAdmin($currentUser->getUID()); + $isOrgAdmin = $this->groupManager->isInGroup($currentUser->getUID(), 'gebruik-beheerder') + || $this->groupManager->isInGroup($currentUser->getUID(), 'aanbod-beheerder'); + if ($isAdmin === false && $isOrgAdmin === false) { + return new JSONResponse(['message' => 'Insufficient permissions'], Http::STATUS_FORBIDDEN); + } + try { // Get object service. $objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService'); - // Find the contactpersoon object. + // Find the contactpersoon object — bind to current tenant. $contactpersoonObject = $objectService->find( id: $contactpersoonId, register: 'voorzieningen', schema: 'contactpersoon', - _rbac: false, - _multitenancy: false + _rbac: true, + _multitenancy: true ); if ($contactpersoonObject === null) { @@ -493,8 +501,12 @@ public function convertToUser(string $contactpersoonId): JSONResponse /** * Change user password. * - * @param string $username The username. - * @param string $newPassword The new password. + * Admins may change any user's password. Regular users may only change their + * own password, and must supply the current password for confirmation. + * + * @param string $username The username. + * @param string $newPassword The new password. + * @param string $currentPassword The current password (required for self-service resets). * * @return JSONResponse Result of password change. * @@ -502,12 +514,46 @@ public function convertToUser(string $contactpersoonId): JSONResponse * @NoCSRFRequired * @spec openspec/changes/retrofit-2026-05-26-contactpersonen-api/tasks.md#task-3 */ - public function changePassword(string $username, string $newPassword): JSONResponse + public function changePassword(string $username, string $newPassword, string $currentPassword=''): JSONResponse { - if ($this->userSession->getUser() === null) { + $currentUser = $this->userSession->getUser(); + if ($currentUser === null) { return new JSONResponse(['message' => 'Not authenticated'], Http::STATUS_UNAUTHORIZED); } + $isAdmin = $this->groupManager->isAdmin($currentUser->getUID()); + $isSelfReset = $currentUser->getUID() === $username; + + // Non-admins may only change their own password. + if ($isAdmin === false && $isSelfReset === false) { + return new JSONResponse(['message' => 'Insufficient permissions'], Http::STATUS_FORBIDDEN); + } + + // Self-service resets require current-password confirmation. + if ($isSelfReset === true && $isAdmin === false) { + if (empty($currentPassword) === true) { + return new JSONResponse( + [ + 'success' => false, + 'message' => 'Current password is required for self-service password reset', + ], + 400 + ); + } + + // Verify current password by checking the user's backend. + $authUser = $this->userManager->checkPassword($username, $currentPassword); + if ($authUser === false) { + return new JSONResponse( + [ + 'success' => false, + 'message' => 'Current password is incorrect', + ], + 403 + ); + } + }//end if + try { $user = $this->userManager->get($username); @@ -1011,20 +1057,29 @@ public function getAvailableGroups(): JSONResponse /** * Disable a user account. * + * Requires admin or organisation-admin (gebruik-beheerder / aanbod-beheerder) role. + * * @param string $contactpersoonId The contactpersoon ID. * * @return JSONResponse Result of the disable operation. * - * @NoAdminRequired * @NoCSRFRequired - * @spec openspec/changes/retrofit-2026-05-26-contactpersonen-api/tasks.md#task-3 + * @spec openspec/changes/retrofit-2026-05-26-contactpersonen-api/tasks.md#task-3 */ public function disableUser(string $contactpersoonId): JSONResponse { - if ($this->userSession->getUser() === null) { + $currentUser = $this->userSession->getUser(); + if ($currentUser === null) { return new JSONResponse(['message' => 'Not authenticated'], Http::STATUS_UNAUTHORIZED); } + $isAdmin = $this->groupManager->isAdmin($currentUser->getUID()); + $isOrgAdmin = $this->groupManager->isInGroup($currentUser->getUID(), 'gebruik-beheerder') + || $this->groupManager->isInGroup($currentUser->getUID(), 'aanbod-beheerder'); + if ($isAdmin === false && $isOrgAdmin === false) { + return new JSONResponse(['message' => 'Insufficient permissions'], Http::STATUS_FORBIDDEN); + } + try { // Delegate to service. $this->contactSvc->disableUserForContactpersoon($contactpersoonId); @@ -1063,20 +1118,29 @@ public function disableUser(string $contactpersoonId): JSONResponse /** * Enable a user account. * + * Requires admin or organisation-admin (gebruik-beheerder / aanbod-beheerder) role. + * * @param string $contactpersoonId The contactpersoon ID. * * @return JSONResponse Result of the enable operation. * - * @NoAdminRequired * @NoCSRFRequired - * @spec openspec/changes/retrofit-2026-05-26-contactpersonen-api/tasks.md#task-3 + * @spec openspec/changes/retrofit-2026-05-26-contactpersonen-api/tasks.md#task-3 */ public function enableUser(string $contactpersoonId): JSONResponse { - if ($this->userSession->getUser() === null) { + $currentUser = $this->userSession->getUser(); + if ($currentUser === null) { return new JSONResponse(['message' => 'Not authenticated'], Http::STATUS_UNAUTHORIZED); } + $isAdmin = $this->groupManager->isAdmin($currentUser->getUID()); + $isOrgAdmin = $this->groupManager->isInGroup($currentUser->getUID(), 'gebruik-beheerder') + || $this->groupManager->isInGroup($currentUser->getUID(), 'aanbod-beheerder'); + if ($isAdmin === false && $isOrgAdmin === false) { + return new JSONResponse(['message' => 'Insufficient permissions'], Http::STATUS_FORBIDDEN); + } + try { // Delegate to service. $this->contactSvc->enableUserForContactpersoon($contactpersoonId); diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index 00687ba9..57fdab67 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -1344,9 +1344,13 @@ public function render(): string /** * Import ArchiMate file. * + * Only accepts multipart file uploads — the `file_path` JSON body parameter is + * rejected to prevent local filesystem read (path traversal / SSRF). + * The `ini_set('memory_limit')` call has been removed; configure PHP memory via + * php.ini / pool config instead. + * * @return JSONResponse Result of the import operation with progress tracking. * - * @NoAdminRequired * @NoCSRFRequired * * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -1356,20 +1360,16 @@ public function render(): string */ public function importArchiMate(): JSONResponse { - if ($this->userSession->getUser() === null) { + $currentUser = $this->userSession->getUser(); + if ($currentUser === null) { return new JSONResponse(['message' => 'Not authenticated'], Http::STATUS_UNAUTHORIZED); } + if ($this->groupManager->isAdmin($currentUser->getUID()) === false) { + return new JSONResponse(['message' => 'Admin privileges required'], Http::STATUS_FORBIDDEN); + } + try { - // Increase memory limit for large imports. - ini_set('memory_limit', '4096M'); - $this->logger->info( - 'Memory limit increased for import', - [ - 'old_limit' => ini_get('memory_limit'), - 'new_limit' => '4096M', - ] - ); // Get JSON data from request body. $rawInput = file_get_contents('php://input'); $data = json_decode($rawInput, true); @@ -1377,13 +1377,21 @@ public function importArchiMate(): JSONResponse $contentType = $this->request->getHeader('Content-Type'); $isMultipart = strpos(haystack: $contentType, needle: 'multipart/form-data') !== false; - // Enhanced debug logging. + // Reject file_path from JSON body — only uploaded files are accepted. + if ($data !== null && isset($data['file_path']) === true) { + return new JSONResponse( + [ + 'success' => false, + 'message' => 'The file_path parameter is not accepted. Please upload a file via multipart/form-data.', + 'error' => 'FILE_PATH_NOT_ALLOWED', + ], + 400 + ); + } + $this->logger->info( 'ArchiMate import request received', [ - 'rawInput' => $rawInput, - 'decodedData' => $data, - 'jsonError' => json_last_error_msg(), 'contentType' => $contentType, 'isMultipart' => $isMultipart, 'requestMethod' => $this->request->getMethod(), @@ -1407,8 +1415,6 @@ public function importArchiMate(): JSONResponse $this->logger->info( 'File upload detection detailed', [ - 'uploadedFiles' => $uploadedFiles, - 'filesArray' => $filesArray, 'requestMethod' => $this->request->getMethod(), 'contentType' => $contentType, 'hasUploadedFiles' => $hasUploadedFiles, @@ -1438,42 +1444,15 @@ public function importArchiMate(): JSONResponse ]; $this->logger->info('File upload detected.', ['options' => $options]); - } else if ($data !== null && isset($data['file_path']) === true) { - // Handle file path from JSON payload. - $fileSize = 0; - if (file_exists($data['file_path']) === true) { - } - - $options = [ - 'updateExisting' => $data['updateExisting'] ?? true, - 'deleteOrphaned' => $data['deleteOrphaned'] ?? false, - 'preserveIds' => $data['preserveIds'] ?? true, - 'processingMode' => $data['processingMode'] ?? 'speed', - 'filePath' => $data['file_path'], - 'fileName' => $data['fileName'] ?? basename($data['file_path']), - 'fileSize' => $data['fileSize'] ?? $fileSize, - 'mimeType' => $data['mimeType'] ?? 'text/xml', - ]; - - $this->logger->info('JSON payload detected.', ['options' => $options]); }//end if if (isset($options) === false) { $this->logger->error( - 'No file uploaded or file path provided — DETAILED DEBUG', + 'No ArchiMate file uploaded', [ - 'uploadedFiles' => $uploadedFiles, - 'filesArray' => $filesArray, - 'data' => $data, - 'rawInput' => $rawInput, - 'contentType' => $contentType, - 'isMultipart' => $isMultipart, - 'requestMethod' => $this->request->getMethod(), - '_FILES_DEBUG' => $_FILES, - '_POST_DEBUG' => $_POST, - 'requestParams' => $this->request->getParams(), - 'userAgent' => $this->request->getHeader('User-Agent'), - 'xRequestedWith' => $this->request->getHeader('X-Requested-With'), + 'contentType' => $contentType, + 'isMultipart' => $isMultipart, + 'requestMethod' => $this->request->getMethod(), ] ); @@ -1928,21 +1907,38 @@ public function testEmailConnection(): JSONResponse /** * Get email settings * - * @NoAdminRequired + * Secret fields (passwords / API keys) are redacted in the response; only + * a boolean presence indicator is returned for each secret. + * * @NoCSRFRequired * - * @return JSONResponse Current email settings + * @return JSONResponse Current email settings (secrets redacted) * @spec openspec/changes/retrofit-2026-05-26-settings-admin-controller/tasks.md#task-4 */ public function getEmailSettings(): JSONResponse { - if ($this->userSession->getUser() === null) { + $currentUser = $this->userSession->getUser(); + if ($currentUser === null) { return new JSONResponse(['message' => 'Not authenticated'], Http::STATUS_UNAUTHORIZED); } + if ($this->groupManager->isAdmin($currentUser->getUID()) === false) { + return new JSONResponse(['message' => 'Admin privileges required'], Http::STATUS_FORBIDDEN); + } + try { $emailSettings = $this->settingsService->getEmailSettings(); + // Redact secret values — return only a masked placeholder when set. + $secretFields = ['smtpPassword', 'sendgridApiKey', 'mailgunApiKey', 'postmarkApiKey', 'sesSecretKey', 'mailjetSecretKey']; + foreach ($secretFields as $field) { + if (empty($emailSettings[$field]) === false) { + $emailSettings[$field] = '••••••••'; + } else { + $emailSettings[$field] = ''; + }//end if + } + return new JSONResponse( [ 'success' => true, @@ -1969,7 +1965,6 @@ public function getEmailSettings(): JSONResponse /** * Update email settings * - * @NoAdminRequired * @NoCSRFRequired * * @return JSONResponse Update result @@ -1977,10 +1972,15 @@ public function getEmailSettings(): JSONResponse */ public function updateEmailSettings(): JSONResponse { - if ($this->userSession->getUser() === null) { + $currentUser = $this->userSession->getUser(); + if ($currentUser === null) { return new JSONResponse(['message' => 'Not authenticated'], Http::STATUS_UNAUTHORIZED); } + if ($this->groupManager->isAdmin($currentUser->getUID()) === false) { + return new JSONResponse(['message' => 'Admin privileges required'], Http::STATUS_FORBIDDEN); + } + try { $data = $this->request->getParams(); $emailSettings = $data['emailSettings'] ?? $data; @@ -2302,7 +2302,6 @@ public function getGenericUserGroups(): JSONResponse /** * Set generic user groups * - * @NoAdminRequired * @NoCSRFRequired * * @return JSONResponse Update result @@ -2310,10 +2309,15 @@ public function getGenericUserGroups(): JSONResponse */ public function setGenericUserGroups(): JSONResponse { - if ($this->userSession->getUser() === null) { + $currentUser = $this->userSession->getUser(); + if ($currentUser === null) { return new JSONResponse(['message' => 'Not authenticated'], Http::STATUS_UNAUTHORIZED); } + if ($this->groupManager->isAdmin($currentUser->getUID()) === false) { + return new JSONResponse(['message' => 'Admin privileges required'], Http::STATUS_FORBIDDEN); + } + try { $data = $this->request->getParams(); $groups = $data['groups'] ?? []; @@ -2390,7 +2394,6 @@ public function getOrganizationAdminGroups(): JSONResponse /** * Set organization admin groups * - * @NoAdminRequired * @NoCSRFRequired * * @return JSONResponse Update result @@ -2398,10 +2401,15 @@ public function getOrganizationAdminGroups(): JSONResponse */ public function setOrganizationAdminGroups(): JSONResponse { - if ($this->userSession->getUser() === null) { + $currentUser = $this->userSession->getUser(); + if ($currentUser === null) { return new JSONResponse(['message' => 'Not authenticated'], Http::STATUS_UNAUTHORIZED); } + if ($this->groupManager->isAdmin($currentUser->getUID()) === false) { + return new JSONResponse(['message' => 'Admin privileges required'], Http::STATUS_FORBIDDEN); + } + try { $data = $this->request->getParams(); $groups = $data['groups'] ?? []; @@ -2478,7 +2486,6 @@ public function getSuperUserGroups(): JSONResponse /** * Set super user groups * - * @NoAdminRequired * @NoCSRFRequired * * @return JSONResponse Update result @@ -2486,10 +2493,15 @@ public function getSuperUserGroups(): JSONResponse */ public function setSuperUserGroups(): JSONResponse { - if ($this->userSession->getUser() === null) { + $currentUser = $this->userSession->getUser(); + if ($currentUser === null) { return new JSONResponse(['message' => 'Not authenticated'], Http::STATUS_UNAUTHORIZED); } + if ($this->groupManager->isAdmin($currentUser->getUID()) === false) { + return new JSONResponse(['message' => 'Admin privileges required'], Http::STATUS_FORBIDDEN); + } + try { $data = $this->request->getParams(); $groups = $data['groups'] ?? []; diff --git a/lib/Service/SettingsService.php b/lib/Service/SettingsService.php index a17d07ab..2b2c2f6e 100644 --- a/lib/Service/SettingsService.php +++ b/lib/Service/SettingsService.php @@ -1916,11 +1916,6 @@ public function getEmailSettings(): array 'email_user_creation_enabled', 'true' ) === 'true', - 'userPasswordEnabled' => $this->config->getValueString( - $app, - 'email_user_password_enabled', - 'true' - ) === 'true', 'userOrganisationEnabled' => $this->config->getValueString( $app, 'email_user_organisation_enabled', @@ -2021,7 +2016,6 @@ public function getEmailSettings(): array 'organization_registration' => $this->getEmailTemplate(templateName: 'organization_registration'), 'organization_activation' => $this->getEmailTemplate(templateName: 'organization_activation'), 'user_creation' => $this->getEmailTemplate(templateName: 'user_creation'), - 'user_password' => $this->getEmailTemplate(templateName: 'user_password'), ], ]; @@ -2060,7 +2054,6 @@ public function updateEmailSettings(array $emailSettings): array 'organizationRegistrationEnabled' => 'email_org_registration_enabled', 'organizationActivationEnabled' => 'email_org_activation_enabled', 'userCreationEnabled' => 'email_user_creation_enabled', - 'userPasswordEnabled' => 'email_user_password_enabled', 'userOrganisationEnabled' => 'email_user_organisation_enabled', // Symfony Mailer transport configuration. @@ -2221,20 +2214,6 @@ public function getDefaultEmailTemplate(string $templateName): string
U kunt nu inloggen op het platform en gebruik maken van alle functionaliteiten.
Heeft u vragen over uw account? Neem dan contact met ons op.
-Met vriendelijke groet,
Het Software Catalogus Team
Beste {{ user.name }},
-Uw wachtwoord voor de Software Catalogus is aangepast.
-Uw logingegevens:
-U kunt nu inloggen met uw nieuwe wachtwoord.
-We raden u aan om uw wachtwoord te wijzigen na het eerste inloggen.
Met vriendelijke groet,
Het Software Catalogus Team