diff --git a/components/ILIAS/Authentication/classes/class.ilObjAuthSettingsGUI.php b/components/ILIAS/Authentication/classes/class.ilObjAuthSettingsGUI.php index 3f2308574ed3..c7f9adf5b5b9 100755 --- a/components/ILIAS/Authentication/classes/class.ilObjAuthSettingsGUI.php +++ b/components/ILIAS/Authentication/classes/class.ilObjAuthSettingsGUI.php @@ -18,6 +18,7 @@ declare(strict_types=1); +use ILIAS\AuthSOAP\ConnectionTester; use ILIAS\Style\Content\GUIService; use ILIAS\components\Authentication\Pages\AuthPageEditorContext; @@ -488,8 +489,13 @@ public function testSoapAuthConnectionObject(): void $test_form = $test_form->withRequest($this->request); $result = $test_form->getData(); if (!is_null($result)) { - $panel_content[] = $this->ui->legacy( - ilSOAPAuth::testConnection($result["ext_uid"], $result["soap_pw"], $result["new_user"]) + $panel_content = array_merge( + $panel_content, + (new ConnectionTester($this->settings, $this->ui))->testConnection( + $result['ext_uid'], + $result['soap_pw'], + $result['new_user'] + ) ); } } diff --git a/components/ILIAS/SOAPAuth/README.md b/components/ILIAS/SOAPAuth/README.md index c687d1c219ce..df223a5bb0c5 100755 --- a/components/ILIAS/SOAPAuth/README.md +++ b/components/ILIAS/SOAPAuth/README.md @@ -31,7 +31,9 @@ goto.php?clientid=abc&target=crs_123&soap_pw=kjWjb34&ext_uid=500 SOAP call to the external server: There is an example implementation of a SOAP server (=external master system) -in classes/class.ilSoapDummyAuthServer.php). The main soap call is: +in src/SoapDummyAuthServer.php (`ILIAS\AuthSOAP`). Public test endpoints are provided +as `auth/soap/example/dummy_server.php` and `auth/soap/example/dummy_client.php`. The main +soap call is: isValidSession: in: diff --git a/components/ILIAS/SOAPAuth/SOAPAuth.php b/components/ILIAS/SOAPAuth/SOAPAuth.php index 1efcc55e1729..0444064222d1 100644 --- a/components/ILIAS/SOAPAuth/SOAPAuth.php +++ b/components/ILIAS/SOAPAuth/SOAPAuth.php @@ -20,7 +20,11 @@ namespace ILIAS; -class SOAPAuth implements Component\Component +use ILIAS\Component\Component; +use ILIAS\Component\Resource\Endpoint; +use ILIAS\Component\Resource\PublicAsset; + +class SOAPAuth implements Component { public function init( array | \ArrayAccess &$define, @@ -32,6 +36,10 @@ public function init( array | \ArrayAccess &$pull, array | \ArrayAccess &$internal, ): void { - // ... + $contribute[PublicAsset::class] = fn(): Endpoint => + new Endpoint($this, 'dummy_server.php', 'auth/soap/example'); + + $contribute[PublicAsset::class] = fn(): Endpoint => + new Endpoint($this, 'dummy_client.php', 'auth/soap/example'); } } diff --git a/components/ILIAS/SOAPAuth/classes/class.ilAuthProviderSoap.php b/components/ILIAS/SOAPAuth/classes/class.ilAuthProviderSoap.php deleted file mode 100755 index 2bc687f9dd83..000000000000 --- a/components/ILIAS/SOAPAuth/classes/class.ilAuthProviderSoap.php +++ /dev/null @@ -1,227 +0,0 @@ -settings = $DIC->settings(); - $this->logger = $DIC->logger()->auth(); - $this->language = $DIC->language(); - $this->rbacAdmin = $DIC->rbac()->admin(); - - parent::__construct($credentials); - } - - private function initClient(): void - { - $this->server_host = (string) $this->settings->get('soap_auth_server', ''); - $this->server_port = (string) $this->settings->get('soap_auth_port', ''); - $this->server_uri = (string) $this->settings->get('soap_auth_uri', ''); - $this->server_nms = (string) $this->settings->get('soap_auth_namespace', ''); - $this->server_https = (bool) $this->settings->get('soap_auth_use_https', '0'); - $this->use_dot_net = (bool) $this->settings->get('use_dotnet', '0'); - - $this->uri = $this->server_https ? 'https://' : 'http://'; - $this->uri .= $this->server_host; - - if ($this->server_port > 0) { - $this->uri .= (':' . $this->server_port); - } - if ($this->server_uri) { - $this->uri .= ('/' . $this->server_uri); - } - - require_once __DIR__ . '/../../soap/lib/nusoap.php'; - $this->client = new nusoap_client($this->uri); - } - - /** - * @inheritDoc - */ - public function doAuthentication(ilAuthStatus $status): bool - { - try { - $this->initClient(); - $this->handleSoapAuth($status); - } catch (Exception $e) { - $this->getLogger()->error($e->getMessage()); - $this->getLogger()->error($e->getTraceAsString()); - - $this->handleAuthenticationFail($status, 'err_wrong_login'); - - return false; - } - - if ($status->getAuthenticatedUserId() > 0 && $status->getAuthenticatedUserId() !== ANONYMOUS_USER_ID) { - $this->logger->info('Successfully authenticated user via SOAP: ' . $this->getCredentials()->getUsername()); - $status->setStatus(ilAuthStatus::STATUS_AUTHENTICATED); - ilSession::set('used_external_auth_mode', ilAuthUtils::AUTH_SOAP); - - return true; - } - - $this->handleAuthenticationFail($status, 'err_wrong_login'); - - return false; - } - - private function handleSoapAuth(ilAuthStatus $status): bool - { - $this->logger->debug(sprintf( - 'Login observer called for SOAP authentication request of ext_account "%s" and auth_mode "%s".', - $this->getCredentials()->getUsername(), - 'soap' - )); - $this->logger->debug(sprintf( - 'Trying to find ext_account "%s" for auth_mode "%s".', - $this->getCredentials()->getUsername(), - 'soap' - )); - - $internalLogin = ilObjUser::_checkExternalAuthAccount( - 'soap', - $this->getCredentials()->getUsername() - ); - - $isNewUser = false; - if ('' === $internalLogin || null === $internalLogin) { - $isNewUser = true; - } - - $soapAction = ''; - $nspref = ''; - if ($this->use_dot_net) { - $soapAction = $this->server_nms . '/isValidSession'; - $nspref = 'ns1:'; - } - - $valid = $this->client->call( - 'isValidSession', - [ - $nspref . 'ext_uid' => $this->getCredentials()->getUsername(), - $nspref . 'soap_pw' => $this->getCredentials()->getPassword(), - $nspref . 'new_user' => $isNewUser - ], - $this->server_nms, - $soapAction - ); - - if (!is_array($valid)) { - $valid = ['valid' => false]; - } - - if ($valid['valid'] !== true) { - $valid['valid'] = false; - } - - if (!$valid['valid']) { - $status->setReason('err_wrong_login'); - return false; - } - - if (!$isNewUser) { - $status->setAuthenticatedUserId(ilObjUser::_lookupId($internalLogin)); - return true; - } - - if (!$this->settings->get('soap_auth_create_users')) { - // Translate the reasons, otherwise the default failure is displayed - $status->setTranslatedReason($this->language->txt('err_valid_login_account_creation_disabled')); - return false; - } - - $userObj = new ilObjUser(); - $internalLogin = ilAuthUtils::_generateLogin($this->getCredentials()->getUsername()); - - $usrData = []; - $usrData['firstname'] = $valid['firstname']; - $usrData['lastname'] = $valid['lastname']; - $usrData['email'] = $valid['email']; - $usrData['login'] = $internalLogin; - $usrData['passwd'] = ''; - $usrData['passwd_type'] = ilObjUser::PASSWD_CRYPTED; - - $password = ''; - if ($this->settings->get('soap_auth_allow_local')) { - $passwords = ilSecuritySettingsChecker::generatePasswords(1); - $password = $passwords[0]; - $usrData['passwd'] = $password; - $usrData['passwd_type'] = ilObjUser::PASSWD_PLAIN; - } - - $usrData['auth_mode'] = 'soap'; - $usrData['ext_account'] = $this->getCredentials()->getUsername(); - $usrData['profile_incomplete'] = 1; - - $userObj->assignData($usrData); - $userObj->setTitle($userObj->getFullname()); - $userObj->setDescription($userObj->getEmail()); - $userObj->setLanguage($this->language->getDefaultLanguage()); - - $userObj->setTimeLimitOwner(USER_FOLDER_ID); - $userObj->setTimeLimitUnlimited(true); - $userObj->setTimeLimitFrom(time()); - $userObj->setTimeLimitUntil(time()); - $userObj->setOwner(0); - $userObj->create(); - $userObj->setActive(true); - $userObj->updateOwner(); - $userObj->saveAsNew(); - $userObj->writePrefs(); - - $this->rbacAdmin->assignUser( - (int) $this->settings->get('soap_auth_user_default_role', '4'), - $userObj->getId() - ); - - if ($this->settings->get('soap_auth_account_mail', '0')) { - $registrationSettings = new ilRegistrationSettings(); - $registrationSettings->setPasswordGenerationStatus(true); - - $accountMail = new ilAccountRegistrationMail( - $registrationSettings, - $this->language, - $this->logger - ); - $accountMail - ->withDirectRegistrationMode() - ->send($userObj, $password, false); - } - - $status->setAuthenticatedUserId($userObj->getId()); - return true; - } -} diff --git a/components/ILIAS/SOAPAuth/classes/class.ilSOAPAuth.php b/components/ILIAS/SOAPAuth/classes/class.ilSOAPAuth.php deleted file mode 100755 index c29f9495cf11..000000000000 --- a/components/ILIAS/SOAPAuth/classes/class.ilSOAPAuth.php +++ /dev/null @@ -1,80 +0,0 @@ -getAll(); - - $server_hostname = (string) ($settings['soap_auth_server'] ?? ''); - $server_port = (int) ($settings['soap_auth_port'] ?? 0); - $server_uri = (string) ($settings['soap_auth_uri'] ?? ''); - $namespace = (string) ($settings['soap_auth_namespace'] ?? ''); - $use_dotnet = (bool) ($settings['soap_auth_use_dotnet'] ?? false); - $uri = 'http://'; - if (isset($settings['soap_auth_use_https']) && $settings['soap_auth_use_https']) { - $uri = 'https://'; - } - - $uri .= $server_hostname; - - if ($server_port > 0) { - $uri .= ':' . $server_port; - } - - if ($server_uri !== '') { - $uri .= '/' . $server_uri; - } - - require_once __DIR__ . '/../../soap/lib/nusoap.php'; - $soap_client = new nusoap_client($uri); - if ($err = $soap_client->getError()) { - return 'SOAP Authentication Initialisation Error: ' . $err; - } - - $soapAction = ''; - $nspref = ''; - if ($use_dotnet) { - $soapAction = $namespace . '/isValidSession'; - $nspref = 'ns1:'; - } - - $valid = $soap_client->call( - 'isValidSession', - [ - $nspref . 'ext_uid' => $a_ext_uid, - $nspref . 'soap_pw' => $a_soap_pw, - $nspref . 'new_user' => $a_new_user - ], - $namespace, - $soapAction - ); - - return - '
== Request ==' . - '
' . htmlspecialchars(str_replace('" ', "\"\n ", str_replace('>', ">\n", $soap_client->request)), ENT_QUOTES) . '

' . - '
== Response ==' . - '
Valid: -' . $valid['valid'] . '-' . - '
' . htmlspecialchars(str_replace('" ', "\"\n ", str_replace('>', ">\n", $soap_client->response)), ENT_QUOTES) . '
'; - } -} diff --git a/components/ILIAS/SOAPAuth/examples/class.ilSoapDummyAuthServer.php b/components/ILIAS/SOAPAuth/examples/class.ilSoapDummyAuthServer.php deleted file mode 100755 index 0817003f46dd..000000000000 --- a/components/ILIAS/SOAPAuth/examples/class.ilSoapDummyAuthServer.php +++ /dev/null @@ -1,130 +0,0 @@ - '', - 'lastname' => '', - 'email' => '' - ]; - - // generate some dummy values - if ($new_user) { - $ret['firstname'] = 'first ' . $ext_uid; - $ret['lastname'] = 'last ' . $ext_uid; - $ret['email'] = $ext_uid . '@de.de'; - } - - // return valid authentication if user id equals soap password - if ($ext_uid === $soap_pw) { - $ret['valid'] = true; - } else { - $ret['valid'] = false; - } - - return $ret; -} - -class ilSoapDummyAuthServer -{ - public ?soap_server $server = null; - - public function __construct(bool $a_use_wsdl = true) - { - define('SERVICE_NAME', 'ILIAS SOAP Dummy Authentication Server'); - define('SERVICE_NAMESPACE', 'urn:ilSoapDummyAuthServer'); - define('SERVICE_STYLE', 'rpc'); - define('SERVICE_USE', 'encoded'); - - $this->server = new soap_server(); - - if ($a_use_wsdl) { - $this->enableWSDL(); - } - - $this->registerMethods(); - } - - public function start(): void - { - $postdata = file_get_contents('php://input'); - $this->server->service($postdata); - exit(); - } - - public function enableWSDL(): bool - { - $this->server->configureWSDL(SERVICE_NAME, SERVICE_NAMESPACE); - - return true; - } - - public function registerMethods(): bool - { - // Add useful complex types. E.g. array("a","b") or array(1,2) - $this->server->wsdl->addComplexType( - 'intArray', - 'complexType', - 'array', - '', - 'SOAP-ENC:Array', - [], - [['ref' => 'SOAP-ENC:arrayType', 'wsdl:arrayType' => 'xsd:int[]']], - 'xsd:int' - ); - - - $this->server->wsdl->addComplexType( - 'stringArray', - 'complexType', - 'array', - '', - 'SOAP-ENC:Array', - [], - [['ref' => 'SOAP-ENC:arrayType', 'wsdl:arrayType' => 'xsd:string[]']], - 'xsd:string' - ); - - $this->server->register( - 'isValidSession', - [ - 'ext_uid' => 'xsd:string', - 'soap_pw' => 'xsd:string', - 'new_user' => 'xsd:boolean' - ], - [ - 'valid' => 'xsd:boolean', - 'firstname' => 'xsd:string', - 'lastname' => 'xsd:string', - 'email' => 'xsd:string' - ], - SERVICE_NAMESPACE, - SERVICE_NAMESPACE . '#isValidSession', - SERVICE_STYLE, - SERVICE_USE, - 'Dummy Session Validation' - ); - - return true; - } -} diff --git a/components/ILIAS/SOAPAuth/examples/dummy_client.php b/components/ILIAS/SOAPAuth/examples/dummy_client.php deleted file mode 100755 index 4d17f268ffe9..000000000000 --- a/components/ILIAS/SOAPAuth/examples/dummy_client.php +++ /dev/null @@ -1,92 +0,0 @@ -' . - 'server ' . - '
ext_uid ' . - '
soap_pw ' . - '
new_user (1 for true, 0 for false)' . - '

' . - 'The test server will return true/valid, if ext_uid == soap_pw.' . - ''; - -echo "

----------------------------------------------

Calling Server..."; - -// initialize soap client -require_once __DIR__ . '/../../soap/lib/nusoap.php'; -$client = new nusoap_client($server); -if ($err = $client->getError()) { - echo '

Constructor error

' . $err . '
'; -} - - -// isValidSession call -//$valid = $client->call('isValidSession', -// array('ext_uid' => $ext_uid, -// 'soap_pw' => $soap_pw, -// 'new_user' => $new_user)); - -$namespace = "http://testuri.org"; - -$valid = $client->call( - 'isValidSession', - [ - 'ns1:ext_uid' => $ext_uid, - 'ns1:soap_pw' => $soap_pw, - 'ns1:new_user' => $new_user - ], - $namespace, - $namespace . "/isValidSession" -); - -showResult($client, $valid, 'isValidSession'); - -echo "
End Test"; - -function showResult(nusoap_client $client, array $data, string $message): void -{ - if ($client->fault) { - echo '

Fault

';
-        print_r($data);
-        echo '
'; - } else { - // Check for errors - $err = $client->getError(); - if ($err) { - // Display the error - echo '

Error

' . $err . '
'; - exit; - } else { - // Display the result - echo '

Result ' . $message . '

';
-            print_r($data ?: 'FAILED');
-            echo '
'; - } - } -} diff --git a/components/ILIAS/SOAPAuth/maintenance.json b/components/ILIAS/SOAPAuth/maintenance.json index 8bc3cbb6963e..362d20a8ddb7 100755 --- a/components/ILIAS/SOAPAuth/maintenance.json +++ b/components/ILIAS/SOAPAuth/maintenance.json @@ -8,7 +8,7 @@ ], "tester": "", "testcase_writer": "", - "path": "Services/SOAPAuth", + "path": "components/ILIAS/SOAPAuth", "belong_to_component": "SOAP", "used_in_components": [] } \ No newline at end of file diff --git a/components/ILIAS/SOAPAuth/resources/dummy_client.php b/components/ILIAS/SOAPAuth/resources/dummy_client.php new file mode 100755 index 000000000000..8a6bd88d21e2 --- /dev/null +++ b/components/ILIAS/SOAPAuth/resources/dummy_client.php @@ -0,0 +1,85 @@ +' . + 'server ' . + '
ext_uid ' . + '
soap_pw ' . + '
new_user (1 for true, 0 for false)' . + '

' . + 'The test server will return true/valid, if ext_uid == soap_pw.' . + ''; + +echo '

----------------------------------------------

Calling Server...'; + +$client = new SessionValidationClient( + $server, + 'http://testuri.org', + true +); + +try { + $result = $client->validateSession($ext_uid, $soap_pw, $new_user); + show_result($result, 'isValidSession'); +} catch (SoapFault $fault) { + echo '

SOAP Fault

' . htmlspecialchars($fault->getMessage(), ENT_QUOTES) . '
'; +} + +echo '
End Test'; + +function show_result(SessionValidationResult $result, string $message): void +{ + echo '

Result ' . htmlspecialchars($message, ENT_QUOTES) . '

';
+    print_r($result->validation ?: 'FAILED');
+    echo '
'; + + if ($result->request !== '' || $result->response !== '') { + echo '

Request

' . htmlspecialchars(
+            str_replace('" ', "\"\n ", str_replace('>', ">\n", $result->request)),
+            ENT_QUOTES
+        ) . '
'; + echo '

Response

' . htmlspecialchars(
+            str_replace('" ', "\"\n ", str_replace('>', ">\n", $result->response)),
+            ENT_QUOTES
+        ) . '
'; + } +} diff --git a/components/ILIAS/SOAPAuth/examples/dummy_server.php b/components/ILIAS/SOAPAuth/resources/dummy_server.php similarity index 68% rename from components/ILIAS/SOAPAuth/examples/dummy_server.php rename to components/ILIAS/SOAPAuth/resources/dummy_server.php index 46ef41a9f895..0c703e959517 100755 --- a/components/ILIAS/SOAPAuth/examples/dummy_server.php +++ b/components/ILIAS/SOAPAuth/resources/dummy_server.php @@ -18,17 +18,12 @@ declare(strict_types=1); -exit; // Copy this script to the publically(!) available ILIAS (root) folder +exit(); -chdir('../..'); +require_once __DIR__ . '/../../../../vendor/composer/vendor/autoload.php'; -require_once 'vendor/composer/vendor/autoload.php'; - -global $HTTP_RAW_POST_DATA; - -// Initialize the error_reporting level, until it will be overwritte when ILIAS gets initialized ini_set('display_errors', '1'); error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED); -$server = new ilSoapDummyAuthServer(); +$server = new ILIAS\AuthSOAP\SoapDummyAuthServer(); $server->start(); diff --git a/components/ILIAS/SOAPAuth/src/ConnectionTester.php b/components/ILIAS/SOAPAuth/src/ConnectionTester.php new file mode 100755 index 000000000000..deca3f0c8bb6 --- /dev/null +++ b/components/ILIAS/SOAPAuth/src/ConnectionTester.php @@ -0,0 +1,98 @@ + + */ + public function testConnection(string $ext_uid, string $soap_pw, bool $new_user): array + { + $client = (new SoapAuthEndpoint($this->settings))->createValidationClient(); + + try { + $result = $client->validateSession($ext_uid, $soap_pw, $new_user); + } catch (\SoapFault $e) { + return [ + $this->ui_factory->messageBox()->failure( + 'SOAP Authentication Call Error: ' . $e->getMessage() + ) + ]; + } + + $validation = $result->validation; + $is_valid = (bool) ($validation['valid'] ?? false); + $status = $is_valid + ? $this->ui_factory->messageBox()->success('SOAP authentication succeeded.') + : $this->ui_factory->messageBox()->info('SOAP authentication returned invalid credentials.'); + + return [ + $status, + $this->ui_factory->listing()->descriptive([ + 'Valid' => $is_valid ? 'true' : 'false', + 'First Name' => (string) ($validation['firstname'] ?? ''), + 'Last Name' => (string) ($validation['lastname'] ?? ''), + 'Email' => (string) ($validation['email'] ?? ''), + 'Request XML' => $this->renderXmlBlock($result->request), + 'Response XML' => $this->renderXmlBlock($result->response), + ]), + ]; + } + + private function renderXmlBlock(string $xml): Component + { + return $this->ui_factory->legacy( + $this->formatXmlForDisplay($xml) + ); + } + + private function formatXmlForDisplay(string $xml): string + { + $formatted = $this->prettyPrintXml($xml); + $escaped = htmlspecialchars($formatted, ENT_QUOTES); + $escaped = str_replace(' ', ' ', $escaped); + + return nl2br($escaped, false); + } + + private function prettyPrintXml(string $xml): string + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->preserveWhiteSpace = false; + $dom->formatOutput = true; + + if (@$dom->loadXML($xml) === false) { + return str_replace(['>', '" '], [">\n", "\"\n "], $xml); + } + + return (string) $dom->saveXML(); + } +} diff --git a/components/ILIAS/SOAPAuth/src/SessionValidationClient.php b/components/ILIAS/SOAPAuth/src/SessionValidationClient.php new file mode 100644 index 000000000000..ee7be50975fd --- /dev/null +++ b/components/ILIAS/SOAPAuth/src/SessionValidationClient.php @@ -0,0 +1,102 @@ +namespace !== '' ? $this->namespace : $this->location; + + $soap_client = new \SoapClient(null, [ + 'location' => $this->location, + 'uri' => $service_namespace, + 'trace' => true, + 'exceptions' => true, + 'style' => SOAP_RPC, + 'use' => SOAP_ENCODED, + ]); + + $call_options = ['uri' => $service_namespace]; + if ($this->use_dotnet) { + $call_options['soapaction'] = $service_namespace . '/isValidSession'; + } + + $valid = $soap_client->__soapCall( + 'isValidSession', + $this->buildCallParameters($ext_uid, $soap_pw, $new_user), + $call_options + ); + + return new SessionValidationResult( + $this->normalizeValidationResult($valid), + (string) $soap_client->__getLastRequest(), + (string) $soap_client->__getLastResponse(), + ); + } + + /** + * Mirrors legacy nusoap_client::call() parameter naming (ns1: prefix in .NET mode). + * @return list<\SoapParam> + */ + private function buildCallParameters(string $ext_uid, string $soap_pw, bool $new_user): array + { + if ($this->use_dotnet) { + return [ + new \SoapParam($ext_uid, 'ns1:ext_uid'), + new \SoapParam($soap_pw, 'ns1:soap_pw'), + new \SoapParam($new_user, 'ns1:new_user'), + ]; + } + + return [ + new \SoapParam($ext_uid, 'ext_uid'), + new \SoapParam($soap_pw, 'soap_pw'), + new \SoapParam($new_user, 'new_user'), + ]; + } + + /** + * @return array{valid: bool, firstname?: string, lastname?: string, email?: string} + */ + private function normalizeValidationResult(mixed $valid): array + { + if (\is_object($valid)) { + $valid = (array) $valid; + } + + if (!\is_array($valid)) { + return ['valid' => false]; + } + + return $valid; + } +} diff --git a/components/ILIAS/SOAPAuth/src/SessionValidationResult.php b/components/ILIAS/SOAPAuth/src/SessionValidationResult.php new file mode 100644 index 000000000000..25a7dfebf476 --- /dev/null +++ b/components/ILIAS/SOAPAuth/src/SessionValidationResult.php @@ -0,0 +1,37 @@ +namespace = (string) $settings->get('soap_auth_namespace', ''); + $this->use_dotnet = (bool) $settings->get('soap_auth_use_dotnet', '0'); + $this->location = $this->buildLocation( + (string) $settings->get('soap_auth_server', ''), + (int) $settings->get('soap_auth_port', '0'), + (string) $settings->get('soap_auth_uri', ''), + (bool) $settings->get('soap_auth_use_https', '0'), + ); + } + + public function getLocation(): string + { + return $this->location; + } + + public function getNamespace(): string + { + return $this->namespace; + } + + public function useDotnet(): bool + { + return $this->use_dotnet; + } + + public function createValidationClient(): SessionValidationClient + { + return new SessionValidationClient( + $this->location, + $this->namespace, + $this->use_dotnet, + ); + } + + private function buildLocation( + string $server_hostname, + int $server_port, + string $server_uri, + bool $use_https, + ): string { + $uri = $use_https ? 'https://' : 'http://'; + $uri .= $server_hostname; + + if ($server_port > 0) { + $uri .= ':' . $server_port; + } + + if ($server_uri !== '') { + $uri .= '/' . $server_uri; + } + + return $uri; + } +} diff --git a/components/ILIAS/SOAPAuth/src/SoapDummyAuthHandler.php b/components/ILIAS/SOAPAuth/src/SoapDummyAuthHandler.php new file mode 100644 index 000000000000..6e1cc3349ac8 --- /dev/null +++ b/components/ILIAS/SOAPAuth/src/SoapDummyAuthHandler.php @@ -0,0 +1,119 @@ +buildValidationResult($ext_uid, $soap_pw, $new_user); + } + + /** + * @param mixed[] $arguments + * @return array + */ + public function __soapCall(string $function_name, array $arguments): array + { + if ($this->localOperationName($function_name) !== 'isValidSession') { + throw new \SoapFault('SOAP-ENV:Server', "Procedure '$function_name' not present"); + } + + return $this->invokeIsValidSession($arguments); + } + + private function localOperationName(string $name): string + { + if (str_contains($name, ':')) { + return substr($name, strrpos($name, ':') + 1); + } + + return $name; + } + + /** + * @param mixed[] $arguments + * @return array + */ + private function invokeIsValidSession(array $arguments): array + { + if (\count($arguments) === 1) { + $first = $arguments[0]; + if (\is_object($first)) { + $arguments = (array) $first; + } elseif (\is_array($first)) { + $arguments = $first; + } + } + + $parameters = $this->normalizeParameterKeys($arguments); + + return $this->buildValidationResult( + (string) ($parameters['ext_uid'] ?? ''), + (string) ($parameters['soap_pw'] ?? ''), + (bool) ($parameters['new_user'] ?? false) + ); + } + + /** + * @param array $arguments + * @return array + */ + private function normalizeParameterKeys(array $arguments): array + { + $normalized = []; + foreach ($arguments as $key => $value) { + if (!\is_string($key)) { + continue; + } + $normalized[$this->localOperationName($key)] = $value; + } + + if ($normalized !== []) { + return $normalized; + } + + return [ + 'ext_uid' => $arguments[0] ?? '', + 'soap_pw' => $arguments[1] ?? '', + 'new_user' => $arguments[2] ?? false, + ]; + } + + private function buildValidationResult(string $ext_uid, string $soap_pw, bool $new_user): array + { + $result = [ + 'firstname' => '', + 'lastname' => '', + 'email' => '' + ]; + + if ($new_user) { + $result['firstname'] = 'first ' . $ext_uid; + $result['lastname'] = 'last ' . $ext_uid; + $result['email'] = $ext_uid . '@de.de'; + } + + $result['valid'] = $ext_uid === $soap_pw; + + return $result; + } +} diff --git a/components/ILIAS/SOAPAuth/src/SoapDummyAuthServer.php b/components/ILIAS/SOAPAuth/src/SoapDummyAuthServer.php new file mode 100755 index 000000000000..b8bddaa32555 --- /dev/null +++ b/components/ILIAS/SOAPAuth/src/SoapDummyAuthServer.php @@ -0,0 +1,147 @@ +isPostRequest()) { + $this->handleNativeSoapRequest(); + } else { + $this->handleNusoapRequest(); + } + + exit(); + } + + private function isPostRequest(): bool + { + return strcasecmp($_SERVER['REQUEST_METHOD'] ?? '', 'POST') === 0; + } + + private function handleNativeSoapRequest(): void + { + $uri = $this->buildEndpointUri(); + + // Non-WSDL mode: nusoap WSDL describes RPC/encoded ops as "ns1:isValidSession", which + // native SoapServer cannot map when loading that WSDL. Dispatch via __soapCall instead. + $soap_server = new \SoapServer(null, [ + 'uri' => self::SERVICE_NAMESPACE, + 'location' => $uri, + ]); + $soap_server->setObject(new SoapDummyAuthHandler()); + $soap_server->handle(); + } + + private function handleNusoapRequest(): void + { + $server = $this->createNusoapServer(); + + $post_data = file_get_contents('php://input'); + + $server->service($post_data); + } + + private function createNusoapServer(): \soap_server + { + require_once __DIR__ . '/../../soap/lib/nusoap.php'; + $server = new \soap_server(); + $server->class = SoapDummyAuthHandler::class; + + if ($this->use_wsdl) { + $this->enableWSDL($server); + } + + $this->registerNusoapMethods($server); + + return $server; + } + + private function buildEndpointUri(): string + { + $https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'); + $scheme = $https ? 'https' : 'http'; + $host = $_SERVER['HTTP_HOST'] ?? 'localhost'; + $script = $_SERVER['SCRIPT_NAME'] ?? ''; + + return $scheme . '://' . $host . $script; + } + + public function enableWSDL(\soap_server $server): bool + { + $server->configureWSDL(self::SERVICE_NAME, self::SERVICE_NAMESPACE); + + return true; + } + + private function registerNusoapMethods(\soap_server $server): void + { + $server->wsdl->addComplexType( + 'intArray', + 'complexType', + 'array', + '', + 'SOAP-ENC:Array', + [], + [['ref' => 'SOAP-ENC:arrayType', 'wsdl:arrayType' => 'xsd:int[]']], + 'xsd:int' + ); + + $server->wsdl->addComplexType( + 'stringArray', + 'complexType', + 'array', + '', + 'SOAP-ENC:Array', + [], + [['ref' => 'SOAP-ENC:arrayType', 'wsdl:arrayType' => 'xsd:string[]']], + 'xsd:string' + ); + + $server->register( + 'isValidSession', + [ + 'ext_uid' => 'xsd:string', + 'soap_pw' => 'xsd:string', + 'new_user' => 'xsd:boolean' + ], + [ + 'valid' => 'xsd:boolean', + 'firstname' => 'xsd:string', + 'lastname' => 'xsd:string', + 'email' => 'xsd:string' + ], + self::SERVICE_NAMESPACE, + self::SERVICE_NAMESPACE . '#isValidSession', + 'rpc', + 'encoded', + 'Dummy Session Validation' + ); + } +} diff --git a/components/ILIAS/SOAPAuth/src/class.ilAuthProviderSoap.php b/components/ILIAS/SOAPAuth/src/class.ilAuthProviderSoap.php new file mode 100755 index 000000000000..2c9e60878f93 --- /dev/null +++ b/components/ILIAS/SOAPAuth/src/class.ilAuthProviderSoap.php @@ -0,0 +1,188 @@ +settings = $DIC->settings(); + $this->logger = $DIC->logger()->auth(); + $this->language = $DIC->language(); + $this->rbac_admin = $DIC->rbac()->admin(); + + parent::__construct($credentials); + } + + private function initClient(): void + { + $this->client = (new SoapAuthEndpoint($this->settings))->createValidationClient(); + } + + /** + * @inheritDoc + */ + public function doAuthentication(ilAuthStatus $status): bool + { + try { + $this->initClient(); + $this->handleSoapAuth($status); + } catch (Exception $e) { + $this->getLogger()->error($e->getMessage()); + $this->getLogger()->error($e->getTraceAsString()); + + $this->handleAuthenticationFail($status, 'err_wrong_login'); + + return false; + } + + if ($status->getAuthenticatedUserId() > 0 && $status->getAuthenticatedUserId() !== ANONYMOUS_USER_ID) { + $this->logger->info('Successfully authenticated user via SOAP: ' . $this->getCredentials()->getUsername()); + $status->setStatus(ilAuthStatus::STATUS_AUTHENTICATED); + ilSession::set('used_external_auth_mode', ilAuthUtils::AUTH_SOAP); + + return true; + } + + $this->handleAuthenticationFail($status, 'err_wrong_login'); + + return false; + } + + private function handleSoapAuth(ilAuthStatus $status): bool + { + $this->logger->debug(sprintf( + 'Login observer called for SOAP authentication request of ext_account "%s" and auth_mode "%s".', + $this->getCredentials()->getUsername(), + 'soap' + )); + $this->logger->debug(sprintf( + 'Trying to find ext_account "%s" for auth_mode "%s".', + $this->getCredentials()->getUsername(), + 'soap' + )); + + $internal_login = ilObjUser::_checkExternalAuthAccount( + 'soap', + $this->getCredentials()->getUsername() + ); + + $is_new_user = false; + if ($internal_login === '' || $internal_login === null) { + $is_new_user = true; + } + + $valid = $this->client->validateSession( + $this->getCredentials()->getUsername(), + $this->getCredentials()->getPassword(), + $is_new_user + )->validation; + + if ($valid['valid'] !== true) { + $valid['valid'] = false; + } + + if (!$valid['valid']) { + $status->setReason('err_wrong_login'); + return false; + } + + if (!$is_new_user) { + $status->setAuthenticatedUserId(ilObjUser::_lookupId($internal_login)); + return true; + } + + if (!$this->settings->get('soap_auth_create_users')) { + $status->setTranslatedReason($this->language->txt('err_valid_login_account_creation_disabled')); + return false; + } + + $user_obj = new ilObjUser(); + $internal_login = ilAuthUtils::_generateLogin($this->getCredentials()->getUsername()); + + $usr_data = []; + $usr_data['firstname'] = $valid['firstname']; + $usr_data['lastname'] = $valid['lastname']; + $usr_data['email'] = $valid['email']; + $usr_data['login'] = $internal_login; + $usr_data['passwd'] = ''; + $usr_data['passwd_type'] = ilObjUser::PASSWD_CRYPTED; + + $password = ''; + if ($this->settings->get('soap_auth_allow_local')) { + $passwords = ilSecuritySettingsChecker::generatePasswords(1); + $password = $passwords[0]; + $usr_data['passwd'] = $password; + $usr_data['passwd_type'] = ilObjUser::PASSWD_PLAIN; + } + + $usr_data['auth_mode'] = 'soap'; + $usr_data['ext_account'] = $this->getCredentials()->getUsername(); + $usr_data['profile_incomplete'] = 1; + + $user_obj->assignData($usr_data); + $user_obj->setTitle($user_obj->getFullname()); + $user_obj->setDescription($user_obj->getEmail()); + $user_obj->setLanguage($this->language->getDefaultLanguage()); + + $user_obj->setTimeLimitOwner(USER_FOLDER_ID); + $user_obj->setTimeLimitUnlimited(true); + $user_obj->setTimeLimitFrom(time()); + $user_obj->setTimeLimitUntil(time()); + $user_obj->setOwner(0); + $user_obj->create(); + $user_obj->setActive(true); + $user_obj->saveAsNew(); + $user_obj->updateOwner(); + $user_obj->writePrefs(); + + $this->rbac_admin->assignUser( + (int) $this->settings->get('soap_auth_user_default_role', '4'), + $user_obj->getId() + ); + + if ($this->settings->get('soap_auth_account_mail', '0')) { + $registration_settings = new ilRegistrationSettings(); + $registration_settings->setPasswordGenerationStatus(true); + + $account_mail = new ilAccountRegistrationMail( + $registration_settings, + $this->language, + $this->logger + ); + $account_mail + ->withDirectRegistrationMode() + ->send($user_obj, $password, false); + } + + $status->setAuthenticatedUserId($user_obj->getId()); + return true; + } +} diff --git a/components/ILIAS/Setup/src/AbstractOfFinder.php b/components/ILIAS/Setup/src/AbstractOfFinder.php index a33bfe6f5f85..d8003e42685e 100755 --- a/components/ILIAS/Setup/src/AbstractOfFinder.php +++ b/components/ILIAS/Setup/src/AbstractOfFinder.php @@ -52,8 +52,6 @@ abstract class AbstractOfFinder '.*/components/ILIAS/UI/tests/', '.*/components/ILIAS/VirusScanner/tests/', '.*/components/ILIAS/setup_/', - // Classes using removed Auth-class from PEAR - '.*ilSOAPAuth.*', // Classes using unknown '.*ilPDExternalFeedBlockGUI.*', ];