diff --git a/.github/workflows/smb-kerberos.yml b/.github/workflows/smb-kerberos.yml index c10d4f59d2940..1dd88fe02d8c3 100644 --- a/.github/workflows/smb-kerberos.yml +++ b/.github/workflows/smb-kerberos.yml @@ -18,8 +18,6 @@ jobs: if: ${{ github.repository_owner != 'nextcloud-gmbh' }} - name: smb-kerberos-sso - steps: - name: Checkout server uses: actions/checkout@v3 @@ -30,7 +28,7 @@ jobs: with: repository: nextcloud/user_saml path: apps/user_saml - ref: stable27 + ref: stable-5.2 - name: Pull images run: | docker pull ghcr.io/icewind1991/samba-krb-test-dc @@ -56,3 +54,89 @@ jobs: FILEPATH=$(docker exec --user 33 apache ./occ log:file | grep "Log file:" | cut -d' ' -f3) echo "$FILEPATH:" docker exec --user 33 apache cat $FILEPATH + + smb-kerberos-session-tests: + runs-on: ubuntu-latest + + if: ${{ github.repository_owner != 'nextcloud-gmbh' }} + + steps: + - name: Checkout server + uses: actions/checkout@v3 + with: + submodules: true + - name: Checkout user_saml + uses: actions/checkout@v3 + with: + repository: nextcloud/user_saml + path: apps/user_saml + ref: stable-5.2 + - name: Pull images + run: | + docker pull ghcr.io/icewind1991/samba-krb-test-dc + docker pull ghcr.io/icewind1991/samba-krb-test-apache + docker pull ghcr.io/icewind1991/samba-krb-test-client + docker tag ghcr.io/icewind1991/samba-krb-test-dc icewind1991/samba-krb-test-dc + docker tag ghcr.io/icewind1991/samba-krb-test-apache icewind1991/samba-krb-test-apache + docker tag ghcr.io/icewind1991/samba-krb-test-client icewind1991/samba-krb-test-client + - name: Setup AD-DC + run: | + DC_IP=$(apps/files_external/tests/sso-setup/start-dc.sh) + sleep 1 + apps/files_external/tests/sso-setup/start-apache.sh $DC_IP $PWD -v $PWD/apps/files_external/tests/sso-setup/apache-session.conf:/etc/apache2/sites-enabled/000-default.conf + echo "DC_IP=$DC_IP" >> $GITHUB_ENV + - name: Set up Nextcloud + run: | + apps/files_external/tests/sso-setup/setup-sso-nc.sh smb::kerberos_sso_session + - name: Test SSO + run: | + apps/files_external/tests/sso-setup/test-sso-smb-session.sh ${{ env.DC_IP }} + - name: Show logs + if: failure() + run: | + FILEPATH=$(docker exec --user 33 apache ./occ log:file | grep "Log file:" | cut -d' ' -f3) + echo "$FILEPATH:" + docker exec --user 33 apache cat $FILEPATH + + smb-kerberos-database-tests: + runs-on: ubuntu-latest + + if: ${{ github.repository_owner != 'nextcloud-gmbh' }} + + steps: + - name: Checkout server + uses: actions/checkout@v3 + with: + submodules: true + - name: Checkout user_saml + uses: actions/checkout@v3 + with: + repository: nextcloud/user_saml + path: apps/user_saml + ref: stable-5.2 + - name: Pull images + run: | + docker pull ghcr.io/icewind1991/samba-krb-test-dc + docker pull ghcr.io/icewind1991/samba-krb-test-apache + docker pull ghcr.io/icewind1991/samba-krb-test-client + docker tag ghcr.io/icewind1991/samba-krb-test-dc icewind1991/samba-krb-test-dc + docker tag ghcr.io/icewind1991/samba-krb-test-apache icewind1991/samba-krb-test-apache + docker tag ghcr.io/icewind1991/samba-krb-test-client icewind1991/samba-krb-test-client + - name: Setup AD-DC + run: | + DC_IP=$(apps/files_external/tests/sso-setup/start-dc.sh) + sleep 1 + apps/files_external/tests/sso-setup/start-apache.sh $DC_IP $PWD -v $PWD/apps/files_external/tests/sso-setup/apache-session.conf:/etc/apache2/sites-enabled/000-default.conf + echo "DC_IP=$DC_IP" >> $GITHUB_ENV + - name: Set up Nextcloud + run: | + apps/files_external/tests/sso-setup/setup-sso-nc.sh smb::kerberos_sso_database + - name: Test SSO + run: | + apps/files_external/tests/sso-setup/test-sso-smb-session.sh ${{ env.DC_IP }} + - name: Show logs + if: failure() + run: | + FILEPATH=$(docker exec --user 33 apache ./occ log:file | grep "Log file:" | cut -d' ' -f3) + echo "$FILEPATH:" + docker exec --user 33 apache cat $FILEPATH diff --git a/apps/files_external/3rdparty/composer.json b/apps/files_external/3rdparty/composer.json index 348e482c2c35c..0da6cfdf11efb 100644 --- a/apps/files_external/3rdparty/composer.json +++ b/apps/files_external/3rdparty/composer.json @@ -8,7 +8,7 @@ "classmap-authoritative": true }, "require": { - "icewind/smb": "3.5.4", + "icewind/smb": "3.6.0", "icewind/streams": "0.7.7" } } diff --git a/apps/files_external/3rdparty/composer.lock b/apps/files_external/3rdparty/composer.lock index 281fdef3e234d..2be44f816314d 100644 --- a/apps/files_external/3rdparty/composer.lock +++ b/apps/files_external/3rdparty/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1419e286d2372dfbce44dd73ddbab2ff", + "content-hash": "dc5d93a9b45922aeec3823baf867acdc", "packages": [ { "name": "icewind/smb", - "version": "v3.5.4", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/icewind1991/SMB.git", - "reference": "76995aa11c14e39bccd0f2370ed63b2f8f623a6d" + "reference": "e0e86b16640f5892dd00408ed50ad18357dac6c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/icewind1991/SMB/zipball/76995aa11c14e39bccd0f2370ed63b2f8f623a6d", - "reference": "76995aa11c14e39bccd0f2370ed63b2f8f623a6d", + "url": "https://api.github.com/repos/icewind1991/SMB/zipball/e0e86b16640f5892dd00408ed50ad18357dac6c1", + "reference": "e0e86b16640f5892dd00408ed50ad18357dac6c1", "shasum": "" }, "require": { @@ -49,9 +49,9 @@ "description": "php wrapper for smbclient and libsmbclient-php", "support": { "issues": "https://github.com/icewind1991/SMB/issues", - "source": "https://github.com/icewind1991/SMB/tree/v3.5.4" + "source": "https://github.com/icewind1991/SMB/tree/v3.6.0" }, - "time": "2022-05-30T15:18:19+00:00" + "time": "2023-08-10T13:17:39+00:00" }, { "name": "icewind/streams", diff --git a/apps/files_external/3rdparty/composer/autoload_classmap.php b/apps/files_external/3rdparty/composer/autoload_classmap.php index 66b472bd42219..8ed82b8402335 100644 --- a/apps/files_external/3rdparty/composer/autoload_classmap.php +++ b/apps/files_external/3rdparty/composer/autoload_classmap.php @@ -32,6 +32,7 @@ 'Icewind\\SMB\\Exception\\InvalidPathException' => $vendorDir . '/icewind/smb/src/Exception/InvalidPathException.php', 'Icewind\\SMB\\Exception\\InvalidRequestException' => $vendorDir . '/icewind/smb/src/Exception/InvalidRequestException.php', 'Icewind\\SMB\\Exception\\InvalidResourceException' => $vendorDir . '/icewind/smb/src/Exception/InvalidResourceException.php', + 'Icewind\\SMB\\Exception\\InvalidTicket' => $vendorDir . '/icewind/smb/src/Exception/InvalidTicket.php', 'Icewind\\SMB\\Exception\\InvalidTypeException' => $vendorDir . '/icewind/smb/src/Exception/InvalidTypeException.php', 'Icewind\\SMB\\Exception\\NoLoginServerException' => $vendorDir . '/icewind/smb/src/Exception/NoLoginServerException.php', 'Icewind\\SMB\\Exception\\NoRouteToHostException' => $vendorDir . '/icewind/smb/src/Exception/NoRouteToHostException.php', @@ -50,6 +51,7 @@ 'Icewind\\SMB\\ITimeZoneProvider' => $vendorDir . '/icewind/smb/src/ITimeZoneProvider.php', 'Icewind\\SMB\\KerberosApacheAuth' => $vendorDir . '/icewind/smb/src/KerberosApacheAuth.php', 'Icewind\\SMB\\KerberosAuth' => $vendorDir . '/icewind/smb/src/KerberosAuth.php', + 'Icewind\\SMB\\KerberosTicket' => $vendorDir . '/icewind/smb/src/KerberosTicket.php', 'Icewind\\SMB\\Native\\NativeFileInfo' => $vendorDir . '/icewind/smb/src/Native/NativeFileInfo.php', 'Icewind\\SMB\\Native\\NativeReadStream' => $vendorDir . '/icewind/smb/src/Native/NativeReadStream.php', 'Icewind\\SMB\\Native\\NativeServer' => $vendorDir . '/icewind/smb/src/Native/NativeServer.php', diff --git a/apps/files_external/3rdparty/composer/autoload_static.php b/apps/files_external/3rdparty/composer/autoload_static.php index 1d309dcd6f1ca..75b1adaa17b55 100644 --- a/apps/files_external/3rdparty/composer/autoload_static.php +++ b/apps/files_external/3rdparty/composer/autoload_static.php @@ -52,6 +52,7 @@ class ComposerStaticInit98fe9b281934250b3a93f69a5ce843b3 'Icewind\\SMB\\Exception\\InvalidPathException' => __DIR__ . '/..' . '/icewind/smb/src/Exception/InvalidPathException.php', 'Icewind\\SMB\\Exception\\InvalidRequestException' => __DIR__ . '/..' . '/icewind/smb/src/Exception/InvalidRequestException.php', 'Icewind\\SMB\\Exception\\InvalidResourceException' => __DIR__ . '/..' . '/icewind/smb/src/Exception/InvalidResourceException.php', + 'Icewind\\SMB\\Exception\\InvalidTicket' => __DIR__ . '/..' . '/icewind/smb/src/Exception/InvalidTicket.php', 'Icewind\\SMB\\Exception\\InvalidTypeException' => __DIR__ . '/..' . '/icewind/smb/src/Exception/InvalidTypeException.php', 'Icewind\\SMB\\Exception\\NoLoginServerException' => __DIR__ . '/..' . '/icewind/smb/src/Exception/NoLoginServerException.php', 'Icewind\\SMB\\Exception\\NoRouteToHostException' => __DIR__ . '/..' . '/icewind/smb/src/Exception/NoRouteToHostException.php', @@ -70,6 +71,7 @@ class ComposerStaticInit98fe9b281934250b3a93f69a5ce843b3 'Icewind\\SMB\\ITimeZoneProvider' => __DIR__ . '/..' . '/icewind/smb/src/ITimeZoneProvider.php', 'Icewind\\SMB\\KerberosApacheAuth' => __DIR__ . '/..' . '/icewind/smb/src/KerberosApacheAuth.php', 'Icewind\\SMB\\KerberosAuth' => __DIR__ . '/..' . '/icewind/smb/src/KerberosAuth.php', + 'Icewind\\SMB\\KerberosTicket' => __DIR__ . '/..' . '/icewind/smb/src/KerberosTicket.php', 'Icewind\\SMB\\Native\\NativeFileInfo' => __DIR__ . '/..' . '/icewind/smb/src/Native/NativeFileInfo.php', 'Icewind\\SMB\\Native\\NativeReadStream' => __DIR__ . '/..' . '/icewind/smb/src/Native/NativeReadStream.php', 'Icewind\\SMB\\Native\\NativeServer' => __DIR__ . '/..' . '/icewind/smb/src/Native/NativeServer.php', diff --git a/apps/files_external/3rdparty/composer/installed.json b/apps/files_external/3rdparty/composer/installed.json index 9b66e6497158c..40f08ff4be6cd 100644 --- a/apps/files_external/3rdparty/composer/installed.json +++ b/apps/files_external/3rdparty/composer/installed.json @@ -2,17 +2,17 @@ "packages": [ { "name": "icewind/smb", - "version": "v3.5.4", - "version_normalized": "3.5.4.0", + "version": "v3.6.0", + "version_normalized": "3.6.0.0", "source": { "type": "git", "url": "https://github.com/icewind1991/SMB.git", - "reference": "76995aa11c14e39bccd0f2370ed63b2f8f623a6d" + "reference": "e0e86b16640f5892dd00408ed50ad18357dac6c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/icewind1991/SMB/zipball/76995aa11c14e39bccd0f2370ed63b2f8f623a6d", - "reference": "76995aa11c14e39bccd0f2370ed63b2f8f623a6d", + "url": "https://api.github.com/repos/icewind1991/SMB/zipball/e0e86b16640f5892dd00408ed50ad18357dac6c1", + "reference": "e0e86b16640f5892dd00408ed50ad18357dac6c1", "shasum": "" }, "require": { @@ -25,7 +25,7 @@ "phpunit/phpunit": "^8.5|^9.3.8", "psalm/phar": "^4.3" }, - "time": "2022-05-30T15:18:19+00:00", + "time": "2023-08-10T13:17:39+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -46,7 +46,7 @@ "description": "php wrapper for smbclient and libsmbclient-php", "support": { "issues": "https://github.com/icewind1991/SMB/issues", - "source": "https://github.com/icewind1991/SMB/tree/v3.5.4" + "source": "https://github.com/icewind1991/SMB/tree/v3.6.0" }, "install-path": "../icewind/smb" }, diff --git a/apps/files_external/3rdparty/composer/installed.php b/apps/files_external/3rdparty/composer/installed.php index 65684203f3f4f..f70a30b8ca781 100644 --- a/apps/files_external/3rdparty/composer/installed.php +++ b/apps/files_external/3rdparty/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'files_external/3rdparty', 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '979f4033ca1cd0be7f255f028d4cc637a216440d', + 'reference' => '733d6b54097417b3f49673ac0491aebdb46648fd', 'type' => 'library', 'install_path' => __DIR__ . '/../', 'aliases' => array(), @@ -13,16 +13,16 @@ 'files_external/3rdparty' => array( 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '979f4033ca1cd0be7f255f028d4cc637a216440d', + 'reference' => '733d6b54097417b3f49673ac0491aebdb46648fd', 'type' => 'library', 'install_path' => __DIR__ . '/../', 'aliases' => array(), 'dev_requirement' => false, ), 'icewind/smb' => array( - 'pretty_version' => 'v3.5.4', - 'version' => '3.5.4.0', - 'reference' => '76995aa11c14e39bccd0f2370ed63b2f8f623a6d', + 'pretty_version' => 'v3.6.0', + 'version' => '3.6.0.0', + 'reference' => 'e0e86b16640f5892dd00408ed50ad18357dac6c1', 'type' => 'library', 'install_path' => __DIR__ . '/../icewind/smb', 'aliases' => array(), diff --git a/apps/files_external/3rdparty/icewind/smb/README.md b/apps/files_external/3rdparty/icewind/smb/README.md index fec1faefbadb6..ed1d54e323c37 100644 --- a/apps/files_external/3rdparty/icewind/smb/README.md +++ b/apps/files_external/3rdparty/icewind/smb/README.md @@ -65,19 +65,20 @@ $server = $serverFactory->createServer('localhost', $auth); By re-using a client ticket you can create a single sign-on setup where the user authenticates against the web service using kerberos. And the web server can forward that ticket to the smb server, allowing it -to act on the behalf of the user without requiring the user to enter his passord. +to act on the behalf of the user without requiring the user to enter his password. The setup for such a system is fairly involved and requires roughly the following this - The web server is authenticated against kerberos with a machine account - Delegation is enabled for the web server's machine account -- Apache is setup to perform kerberos authentication and save the ticket in it's environment +- The web server is setup to perform kerberos authentication and save the ticket in it's environment - Php has the krb5 extension installed - The client authenticates using a ticket with forwarding enabled ```php $serverFactory = new ServerFactory(); -$auth = new KerberosApacheAuth(); +$auth = new KerberosAuth(); +$auth->setTicket(KerberosTicket::fromEnv()); $server = $serverFactory->createServer('localhost', $auth); ``` diff --git a/apps/files_external/3rdparty/icewind/smb/example-sso-kerberos.php b/apps/files_external/3rdparty/icewind/smb/example-sso-kerberos.php new file mode 100644 index 0000000000000..ca1055bf37499 --- /dev/null +++ b/apps/files_external/3rdparty/icewind/smb/example-sso-kerberos.php @@ -0,0 +1,26 @@ +setTicket(KerberosTicket::fromEnv()); +$serverFactory = new \Icewind\SMB\ServerFactory(); + +$server = $serverFactory->createServer($host, $auth); + +$share = $server->getShare($share); + +$files = $share->dir('/'); +foreach ($files as $file) { + echo $file->getName() . "\n"; +} diff --git a/apps/files_external/3rdparty/icewind/smb/src/Exception/InvalidTicket.php b/apps/files_external/3rdparty/icewind/smb/src/Exception/InvalidTicket.php new file mode 100644 index 0000000000000..9021346aba6be --- /dev/null +++ b/apps/files_external/3rdparty/icewind/smb/src/Exception/InvalidTicket.php @@ -0,0 +1,28 @@ + + * + * @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 Icewind\SMB\Exception; + +class InvalidTicket extends Exception { + +} \ No newline at end of file diff --git a/apps/files_external/3rdparty/icewind/smb/src/KerberosApacheAuth.php b/apps/files_external/3rdparty/icewind/smb/src/KerberosApacheAuth.php index c49918be114a6..c8de5555b31a8 100644 --- a/apps/files_external/3rdparty/icewind/smb/src/KerberosApacheAuth.php +++ b/apps/files_external/3rdparty/icewind/smb/src/KerberosApacheAuth.php @@ -23,114 +23,40 @@ use Icewind\SMB\Exception\DependencyException; use Icewind\SMB\Exception\Exception; +use Icewind\SMB\Exception\InvalidTicket; /** * Use existing kerberos ticket to authenticate and reuse the apache ticket cache (mod_auth_kerb) + * + * @deprecated Use `KerberosAuth` with `$auth->setTicket(KerberosTicket::fromEnv())` instead */ class KerberosApacheAuth extends KerberosAuth implements IAuth { - /** @var string */ - private $ticketPath = ""; - - /** @var bool */ - private $init = false; - - /** @var string|false */ - private $ticketName; - - public function __construct() { - $this->ticketName = getenv("KRB5CCNAME"); + public function getTicket(): KerberosTicket { + if ($this->ticket === null) { + $ticket = KerberosTicket::fromEnv(); + if ($ticket === null) { + throw new InvalidTicket("No ticket found in environment"); + } + $this->ticket = $ticket; + } + return $this->ticket; } - /** * Copy the ticket to a temporary location and use that ticket for authentication * * @return void */ public function copyTicket(): void { - if (!$this->checkTicket()) { - return; - } - $krb5 = new \KRB5CCache(); - $krb5->open($this->ticketName); - $tmpFilename = tempnam("/tmp", "krb5cc_php_"); - $tmpCacheFile = "FILE:" . $tmpFilename; - $krb5->save($tmpCacheFile); - $this->ticketPath = $tmpFilename; - $this->ticketName = $tmpCacheFile; - } - - /** - * Pass the ticket to smbclient by memory instead of path - * - * @return void - */ - public function passTicketFromMemory(): void { - if (!$this->checkTicket()) { - return; - } - $krb5 = new \KRB5CCache(); - $krb5->open($this->ticketName); - $this->ticketName = (string)$krb5->getName(); + $this->ticket = KerberosTicket::load($this->getTicket()->save()); } /** * Check if a valid kerberos ticket is present * * @return bool - * @psalm-assert-if-true string $this->ticketName */ public function checkTicket(): bool { - //read apache kerberos ticket cache - if (!$this->ticketName) { - return false; - } - - $krb5 = new \KRB5CCache(); - $krb5->open($this->ticketName); - /** @psalm-suppress MixedArgument */ - return count($krb5->getEntries()) > 0; - } - - private function init(): void { - if ($this->init) { - return; - } - $this->init = true; - // inspired by https://git.typo3.org/TYPO3CMS/Extensions/fal_cifs.git - - if (!extension_loaded("krb5")) { - // https://pecl.php.net/package/krb5 - throw new DependencyException('Ensure php-krb5 is installed.'); - } - - //read apache kerberos ticket cache - if (!$this->checkTicket()) { - throw new Exception('No kerberos ticket cache environment variable (KRB5CCNAME) found.'); - } - - // note that even if the ticketname is the value we got from `getenv("KRB5CCNAME")` we still need to set the env variable ourselves - // this is because `getenv` also reads the variables passed from the SAPI (apache-php) and we need to set the variable in the OS's env - putenv("KRB5CCNAME=" . $this->ticketName); - } - - public function getExtraCommandLineArguments(): string { - $this->init(); - return parent::getExtraCommandLineArguments(); - } - - public function setExtraSmbClientOptions($smbClientState): void { - $this->init(); - try { - parent::setExtraSmbClientOptions($smbClientState); - } catch (Exception $e) { - // suppress - } - } - - public function __destruct() { - if (!empty($this->ticketPath) && file_exists($this->ticketPath) && is_file($this->ticketPath)) { - unlink($this->ticketPath); - } + return $this->getTicket()->isValid(); } } diff --git a/apps/files_external/3rdparty/icewind/smb/src/KerberosAuth.php b/apps/files_external/3rdparty/icewind/smb/src/KerberosAuth.php index 68fb74ff9abdc..d827cfcaf3228 100644 --- a/apps/files_external/3rdparty/icewind/smb/src/KerberosAuth.php +++ b/apps/files_external/3rdparty/icewind/smb/src/KerberosAuth.php @@ -27,6 +27,17 @@ * Use existing kerberos ticket to authenticate */ class KerberosAuth implements IAuth { + /** @var ?KerberosTicket */ + protected $ticket = null; + + public function getTicket(): ?KerberosTicket { + return $this->ticket; + } + + public function setTicket(?KerberosTicket $ticket): void { + $this->ticket = $ticket; + } + public function getUsername(): ?string { return 'dummy'; } @@ -39,11 +50,25 @@ public function getPassword(): ?string { return null; } + private function setEnv():void { + $ticket = $this->getTicket(); + if ($ticket) { + $ticket->validate(); + + // note that even if the ticket name is the value we got from `getenv("KRB5CCNAME")` we still need to set the env variable ourselves + // this is because `getenv` also reads the variables passed from the SAPI (apache-php) and we need to set the variable in the OS's env + putenv("KRB5CCNAME=" . $ticket->getCacheName()); + } + } + public function getExtraCommandLineArguments(): string { + $this->setEnv(); return '-k'; } public function setExtraSmbClientOptions($smbClientState): void { + $this->setEnv(); + $success = (bool)smbclient_option_set($smbClientState, SMBCLIENT_OPT_USE_KERBEROS, true); $success = $success && smbclient_option_set($smbClientState, SMBCLIENT_OPT_FALLBACK_AFTER_KERBEROS, false); diff --git a/apps/files_external/3rdparty/icewind/smb/src/KerberosTicket.php b/apps/files_external/3rdparty/icewind/smb/src/KerberosTicket.php new file mode 100644 index 0000000000000..ea074501e684c --- /dev/null +++ b/apps/files_external/3rdparty/icewind/smb/src/KerberosTicket.php @@ -0,0 +1,100 @@ + + * + * @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 Icewind\SMB; + +use Icewind\SMB\Exception\InvalidTicket; +use KRB5CCache; + +class KerberosTicket { + /** @var KRB5CCache */ + private $krb5; + /** @var string */ + private $cacheName; + + public function __construct(KRB5CCache $krb5, string $cacheName) { + $this->krb5 = $krb5; + $this->cacheName = $cacheName; + } + + public function getCacheName(): string { + return $this->cacheName; + } + + public function getName(): string{ + return $this->krb5->getName(); + } + + public function isValid(): bool { + return count($this->krb5->getEntries()) > 0; + } + + public function validate(): void { + if (!$this->isValid()) { + throw new InvalidTicket("No kerberos ticket found."); + } + } + + /** + * Load the ticket from the cache specified by the KRB5CCNAME variable. + * + * @return KerberosTicket|null + */ + public static function fromEnv(): ?KerberosTicket { + $ticketName = getenv("KRB5CCNAME"); + if (!$ticketName) { + return null; + } + $krb5 = new KRB5CCache(); + $krb5->open($ticketName); + return new KerberosTicket($krb5, $ticketName); + } + + public static function load(string $ticket): KerberosTicket { + $tmpFilename = tempnam(sys_get_temp_dir(), "krb5cc_php_"); + file_put_contents($tmpFilename, $ticket); + register_shutdown_function(function () use ($tmpFilename) { + if (file_exists($tmpFilename)) { + unlink($tmpFilename); + } + }); + + $ticketName = "FILE:" . $tmpFilename; + $krb5 = new KRB5CCache(); + $krb5->open($ticketName); + return new KerberosTicket($krb5, $ticketName); + } + + public function save(): string { + if (substr($this->cacheName, 0, 5) === 'FILE:') { + $ticket = file_get_contents(substr($this->cacheName, 5)); + } else { + $tmpFilename = tempnam(sys_get_temp_dir(), "krb5cc_php_"); + $tmpCacheFile = "FILE:" . $tmpFilename; + $this->krb5->save($tmpCacheFile); + $ticket = file_get_contents($tmpFilename); + unlink($tmpFilename); + } + return $ticket; + } +} \ No newline at end of file diff --git a/apps/files_external/3rdparty/icewind/smb/src/Native/NativeState.php b/apps/files_external/3rdparty/icewind/smb/src/Native/NativeState.php index 088e6f733d123..acbfef141983e 100644 --- a/apps/files_external/3rdparty/icewind/smb/src/Native/NativeState.php +++ b/apps/files_external/3rdparty/icewind/smb/src/Native/NativeState.php @@ -8,6 +8,7 @@ namespace Icewind\SMB\Native; use Icewind\SMB\Exception\AlreadyExistsException; +use Icewind\SMB\Exception\ConnectionException; use Icewind\SMB\Exception\ConnectionRefusedException; use Icewind\SMB\Exception\ConnectionResetException; use Icewind\SMB\Exception\Exception; @@ -32,9 +33,6 @@ class NativeState { /** @var resource|null */ protected $state = null; - /** @var bool */ - protected $handlerSet = false; - /** @var bool */ protected $connected = false; @@ -67,7 +65,9 @@ class NativeState { ]; protected function handleError(?string $path): void { - /** @var int $error */ + if (!$this->state) { + return; + } $error = smbclient_state_errno($this->state); if ($error === 0) { return; @@ -120,7 +120,6 @@ public function init(IAuth $auth, IOptions $options) { // __deconstruct() of KerberosAuth should not caled too soon $this->auth = $auth; - /** @var bool $result */ $result = @smbclient_state_init($this->state, $auth->getWorkgroup(), $auth->getUsername(), $auth->getPassword()); $this->testResult($result, ''); @@ -133,6 +132,9 @@ public function init(IAuth $auth, IOptions $options) { * @return resource */ public function opendir(string $uri) { + if (!$this->state) { + throw new ConnectionException("Not connected"); + } /** @var resource $result */ $result = @smbclient_opendir($this->state, $uri); @@ -146,6 +148,9 @@ public function opendir(string $uri) { * @return array{"type": string, "comment": string, "name": string}|false */ public function readdir($dir, string $path) { + if (!$this->state) { + throw new ConnectionException("Not connected"); + } /** @var array{"type": string, "comment": string, "name": string}|false $result */ $result = @smbclient_readdir($this->state, $dir); @@ -159,8 +164,10 @@ public function readdir($dir, string $path) { * @return bool */ public function closedir($dir, string $path): bool { - /** @var bool $result */ - $result = smbclient_closedir($this->state, $dir); + if (!$this->state) { + throw new ConnectionException("Not connected"); + } + $result = @smbclient_closedir($this->state, $dir); $this->testResult($result, $path); return $result; @@ -172,7 +179,9 @@ public function closedir($dir, string $path): bool { * @return bool */ public function rename(string $old, string $new): bool { - /** @var bool $result */ + if (!$this->state) { + throw new ConnectionException("Not connected"); + } $result = @smbclient_rename($this->state, $old, $this->state, $new); $this->testResult($result, $new); @@ -184,7 +193,9 @@ public function rename(string $old, string $new): bool { * @return bool */ public function unlink(string $uri): bool { - /** @var bool $result */ + if (!$this->state) { + throw new ConnectionException("Not connected"); + } $result = @smbclient_unlink($this->state, $uri); $this->testResult($result, $uri); @@ -197,7 +208,9 @@ public function unlink(string $uri): bool { * @return bool */ public function mkdir(string $uri, int $mask = 0777): bool { - /** @var bool $result */ + if (!$this->state) { + throw new ConnectionException("Not connected"); + } $result = @smbclient_mkdir($this->state, $uri, $mask); $this->testResult($result, $uri); @@ -209,7 +222,9 @@ public function mkdir(string $uri, int $mask = 0777): bool { * @return bool */ public function rmdir(string $uri): bool { - /** @var bool $result */ + if (!$this->state) { + throw new ConnectionException("Not connected"); + } $result = @smbclient_rmdir($this->state, $uri); $this->testResult($result, $uri); @@ -221,6 +236,9 @@ public function rmdir(string $uri): bool { * @return array{"mtime": int, "size": int, "mode": int} */ public function stat(string $uri): array { + if (!$this->state) { + throw new ConnectionException("Not connected"); + } /** @var array{"mtime": int, "size": int, "mode": int} $result */ $result = @smbclient_stat($this->state, $uri); @@ -234,6 +252,9 @@ public function stat(string $uri): array { * @return array{"mtime": int, "size": int, "mode": int} */ public function fstat($file, string $path): array { + if (!$this->state) { + throw new ConnectionException("Not connected"); + } /** @var array{"mtime": int, "size": int, "mode": int} $result */ $result = @smbclient_fstat($this->state, $file); @@ -248,6 +269,9 @@ public function fstat($file, string $path): array { * @return resource */ public function open(string $uri, string $mode, int $mask = 0666) { + if (!$this->state) { + throw new ConnectionException("Not connected"); + } /** @var resource $result */ $result = @smbclient_open($this->state, $uri, $mode, $mask); @@ -261,6 +285,9 @@ public function open(string $uri, string $mode, int $mask = 0666) { * @return resource */ public function create(string $uri, int $mask = 0666) { + if (!$this->state) { + throw new ConnectionException("Not connected"); + } /** @var resource $result */ $result = @smbclient_creat($this->state, $uri, $mask); @@ -275,6 +302,9 @@ public function create(string $uri, int $mask = 0666) { * @return string */ public function read($file, int $bytes, string $path): string { + if (!$this->state) { + throw new ConnectionException("Not connected"); + } /** @var string $result */ $result = @smbclient_read($this->state, $file, $bytes); @@ -290,10 +320,19 @@ public function read($file, int $bytes, string $path): string { * @return int */ public function write($file, string $data, string $path, ?int $length = null): int { - /** @var int $result */ - $result = @smbclient_write($this->state, $file, $data, $length); + if (!$this->state) { + throw new ConnectionException("Not connected"); + } + if ($length) { + $result = @smbclient_write($this->state, $file, $data, $length); + } else { + $result = @smbclient_write($this->state, $file, $data); + } $this->testResult($result, $path); + if ($result === false) { + return 0; + } return $result; } @@ -302,10 +341,18 @@ public function write($file, string $data, string $path, ?int $length = null): i * @param int $offset * @param int $whence SEEK_SET | SEEK_CUR | SEEK_END * @param string|null $path - * @return int|false new file offset as measured from the start of the file on success. + * + * @return false|int new file offset as measured from the start of the file on success. */ public function lseek($file, int $offset, int $whence = SEEK_SET, string $path = null) { - /** @var int|false $result */ + if (!$this->state) { + throw new ConnectionException("Not connected"); + } + // psalm doesn't think int|false == int|false for some reason, so we do a needless annotation to help it out + /** + * @psalm-suppress UnnecessaryVarAnnotation + * @var int|false $result + */ $result = @smbclient_lseek($this->state, $file, $offset, $whence); $this->testResult($result, $path); @@ -319,7 +366,9 @@ public function lseek($file, int $offset, int $whence = SEEK_SET, string $path = * @return bool */ public function ftruncate($file, int $size, string $path): bool { - /** @var bool $result */ + if (!$this->state) { + throw new ConnectionException("Not connected"); + } $result = @smbclient_ftruncate($this->state, $file, $size); $this->testResult($result, $path); @@ -332,7 +381,9 @@ public function ftruncate($file, int $size, string $path): bool { * @return bool */ public function close($file, string $path): bool { - /** @var bool $result */ + if (!$this->state) { + return false; + } $result = @smbclient_close($this->state, $file); $this->testResult($result, $path); @@ -345,6 +396,9 @@ public function close($file, string $path): bool { * @return string */ public function getxattr(string $uri, string $key) { + if (!$this->state) { + throw new ConnectionException("Not connected"); + } /** @var string $result */ $result = @smbclient_getxattr($this->state, $uri, $key); @@ -360,6 +414,9 @@ public function getxattr(string $uri, string $key) { * @return bool */ public function setxattr(string $uri, string $key, string $value, int $flags = 0) { + if (!$this->state) { + throw new ConnectionException("Not connected"); + } /** @var bool $result */ $result = @smbclient_setxattr($this->state, $uri, $key, $value, $flags); @@ -368,7 +425,7 @@ public function setxattr(string $uri, string $key, string $value, int $flags = 0 } public function __destruct() { - if ($this->connected) { + if ($this->connected && $this->state) { if (smbclient_state_free($this->state) === false) { throw new Exception("Failed to free smb state"); } diff --git a/apps/files_external/composer/composer/autoload_classmap.php b/apps/files_external/composer/composer/autoload_classmap.php index c6037ea9ded82..48562ac8564df 100644 --- a/apps/files_external/composer/composer/autoload_classmap.php +++ b/apps/files_external/composer/composer/autoload_classmap.php @@ -54,6 +54,8 @@ 'OCA\\Files_External\\Lib\\Auth\\PublicKey\\RSAPrivateKey' => $baseDir . '/../lib/Lib/Auth/PublicKey/RSAPrivateKey.php', 'OCA\\Files_External\\Lib\\Auth\\SMB\\KerberosApacheAuth' => $baseDir . '/../lib/Lib/Auth/SMB/KerberosApacheAuth.php', 'OCA\\Files_External\\Lib\\Auth\\SMB\\KerberosAuth' => $baseDir . '/../lib/Lib/Auth/SMB/KerberosAuth.php', + 'OCA\\Files_External\\Lib\\Auth\\SMB\\KerberosSsoDatabase' => $baseDir . '/../lib/Lib/Auth/SMB/KerberosSsoDatabase.php', + 'OCA\\Files_External\\Lib\\Auth\\SMB\\KerberosSsoSession' => $baseDir . '/../lib/Lib/Auth/SMB/KerberosSsoSession.php', 'OCA\\Files_External\\Lib\\Backend\\AmazonS3' => $baseDir . '/../lib/Lib/Backend/AmazonS3.php', 'OCA\\Files_External\\Lib\\Backend\\Backend' => $baseDir . '/../lib/Lib/Backend/Backend.php', 'OCA\\Files_External\\Lib\\Backend\\DAV' => $baseDir . '/../lib/Lib/Backend/DAV.php', @@ -94,6 +96,7 @@ 'OCA\\Files_External\\Lib\\Storage\\SMB' => $baseDir . '/../lib/Lib/Storage/SMB.php', 'OCA\\Files_External\\Lib\\Storage\\StreamWrapper' => $baseDir . '/../lib/Lib/Storage/StreamWrapper.php', 'OCA\\Files_External\\Lib\\Storage\\Swift' => $baseDir . '/../lib/Lib/Storage/Swift.php', + 'OCA\\Files_External\\Lib\\TicketSaveMiddleware' => $baseDir . '/../lib/Lib/TicketSaveMiddleware.php', 'OCA\\Files_External\\Lib\\VisibilityTrait' => $baseDir . '/../lib/Lib/VisibilityTrait.php', 'OCA\\Files_External\\Listener\\GroupDeletedListener' => $baseDir . '/../lib/Listener/GroupDeletedListener.php', 'OCA\\Files_External\\Listener\\StorePasswordListener' => $baseDir . '/../lib/Listener/StorePasswordListener.php', diff --git a/apps/files_external/composer/composer/autoload_static.php b/apps/files_external/composer/composer/autoload_static.php index 26091fb32b3e0..f77e20afa6b12 100644 --- a/apps/files_external/composer/composer/autoload_static.php +++ b/apps/files_external/composer/composer/autoload_static.php @@ -69,6 +69,8 @@ class ComposerStaticInitFiles_External 'OCA\\Files_External\\Lib\\Auth\\PublicKey\\RSAPrivateKey' => __DIR__ . '/..' . '/../lib/Lib/Auth/PublicKey/RSAPrivateKey.php', 'OCA\\Files_External\\Lib\\Auth\\SMB\\KerberosApacheAuth' => __DIR__ . '/..' . '/../lib/Lib/Auth/SMB/KerberosApacheAuth.php', 'OCA\\Files_External\\Lib\\Auth\\SMB\\KerberosAuth' => __DIR__ . '/..' . '/../lib/Lib/Auth/SMB/KerberosAuth.php', + 'OCA\\Files_External\\Lib\\Auth\\SMB\\KerberosSsoDatabase' => __DIR__ . '/..' . '/../lib/Lib/Auth/SMB/KerberosSsoDatabase.php', + 'OCA\\Files_External\\Lib\\Auth\\SMB\\KerberosSsoSession' => __DIR__ . '/..' . '/../lib/Lib/Auth/SMB/KerberosSsoSession.php', 'OCA\\Files_External\\Lib\\Backend\\AmazonS3' => __DIR__ . '/..' . '/../lib/Lib/Backend/AmazonS3.php', 'OCA\\Files_External\\Lib\\Backend\\Backend' => __DIR__ . '/..' . '/../lib/Lib/Backend/Backend.php', 'OCA\\Files_External\\Lib\\Backend\\DAV' => __DIR__ . '/..' . '/../lib/Lib/Backend/DAV.php', @@ -109,6 +111,7 @@ class ComposerStaticInitFiles_External 'OCA\\Files_External\\Lib\\Storage\\SMB' => __DIR__ . '/..' . '/../lib/Lib/Storage/SMB.php', 'OCA\\Files_External\\Lib\\Storage\\StreamWrapper' => __DIR__ . '/..' . '/../lib/Lib/Storage/StreamWrapper.php', 'OCA\\Files_External\\Lib\\Storage\\Swift' => __DIR__ . '/..' . '/../lib/Lib/Storage/Swift.php', + 'OCA\\Files_External\\Lib\\TicketSaveMiddleware' => __DIR__ . '/..' . '/../lib/Lib/TicketSaveMiddleware.php', 'OCA\\Files_External\\Lib\\VisibilityTrait' => __DIR__ . '/..' . '/../lib/Lib/VisibilityTrait.php', 'OCA\\Files_External\\Listener\\GroupDeletedListener' => __DIR__ . '/..' . '/../lib/Listener/GroupDeletedListener.php', 'OCA\\Files_External\\Listener\\StorePasswordListener' => __DIR__ . '/..' . '/../lib/Listener/StorePasswordListener.php', diff --git a/apps/files_external/lib/AppInfo/Application.php b/apps/files_external/lib/AppInfo/Application.php index 6f8018746b34b..b29e96b1c94a4 100644 --- a/apps/files_external/lib/AppInfo/Application.php +++ b/apps/files_external/lib/AppInfo/Application.php @@ -49,6 +49,8 @@ use OCA\Files_External\Lib\Auth\PublicKey\RSAPrivateKey; use OCA\Files_External\Lib\Auth\SMB\KerberosApacheAuth; use OCA\Files_External\Lib\Auth\SMB\KerberosAuth; +use OCA\Files_External\Lib\Auth\SMB\KerberosSsoDatabase; +use OCA\Files_External\Lib\Auth\SMB\KerberosSsoSession; use OCA\Files_External\Lib\Backend\AmazonS3; use OCA\Files_External\Lib\Backend\DAV; use OCA\Files_External\Lib\Backend\FTP; @@ -61,6 +63,7 @@ use OCA\Files_External\Lib\Backend\Swift; use OCA\Files_External\Lib\Config\IAuthMechanismProvider; use OCA\Files_External\Lib\Config\IBackendProvider; +use OCA\Files_External\Lib\TicketSaveMiddleware; use OCA\Files_External\Listener\GroupDeletedListener; use OCA\Files_External\Listener\UserDeletedListener; use OCA\Files_External\Service\BackendService; @@ -91,6 +94,7 @@ public function __construct(array $urlParams = []) { public function register(IRegistrationContext $context): void { $context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class); $context->registerEventListener(GroupDeletedEvent::class, GroupDeletedListener::class); + $context->registerMiddleware(TicketSaveMiddleware::class, true); } public function boot(IBootContext $context): void { @@ -182,6 +186,8 @@ public function getAuthMechanisms() { $container->get(AccessKey::class), $container->get(KerberosAuth::class), $container->get(KerberosApacheAuth::class), + $container->get(KerberosSsoSession::class), + $container->get(KerberosSsoDatabase::class), ]; } } diff --git a/apps/files_external/lib/Lib/Auth/SMB/KerberosApacheAuth.php b/apps/files_external/lib/Lib/Auth/SMB/KerberosApacheAuth.php index 17492280275b2..1581b286f4b3b 100644 --- a/apps/files_external/lib/Lib/Auth/SMB/KerberosApacheAuth.php +++ b/apps/files_external/lib/Lib/Auth/SMB/KerberosApacheAuth.php @@ -42,7 +42,7 @@ public function __construct(IL10N $l, IStore $credentialsStore) { $this ->setIdentifier('smb::kerberosapache') ->setScheme(self::SCHEME_SMB) - ->setText($l->t('Kerberos ticket Apache mode')) + ->setText($l->t('Kerberos ticket SSO')) ->addParameter($realm); $this->credentialsStore = $credentialsStore; } diff --git a/apps/files_external/lib/Lib/Auth/SMB/KerberosSsoDatabase.php b/apps/files_external/lib/Lib/Auth/SMB/KerberosSsoDatabase.php new file mode 100644 index 0000000000000..5152b059a0415 --- /dev/null +++ b/apps/files_external/lib/Lib/Auth/SMB/KerberosSsoDatabase.php @@ -0,0 +1,73 @@ + + * + * @author Robin Appelman + * + * @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\Files_External\Lib\Auth\SMB; + +use Icewind\SMB\KerberosTicket; +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\DefinitionParameter; +use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException; +use OCP\IL10N; +use OCP\ISession; +use OCP\IUser; +use OCP\Security\ICredentialsManager; + +class KerberosSsoDatabase extends AuthMechanism { + private ICredentialsManager $credentialsManager; + + public function __construct(IL10N $l, ICredentialsManager $credentialsManager) { + $realm = new DefinitionParameter('default_realm', 'Default realm'); + $realm + ->setType(DefinitionParameter::VALUE_TEXT) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL) + ->setTooltip($l->t('Kerberos default realm, defaults to "WORKGROUP"')); + $this + ->setIdentifier('smb::kerberos_sso_database') + ->setScheme(self::SCHEME_SMB) + ->setText($l->t('Kerberos ticket SSO, save in database')) + ->addParameter($realm); + $this->credentialsManager = $credentialsManager; + } + + public function getTicket(?IUser $user): KerberosTicket { + if (!isset($user)) { + throw new InsufficientDataForMeaningfulAnswerException('No kerberos ticket saved'); + } + try { + $envTicket = KerberosTicket::fromEnv(); + } catch (\Exception $e) { + $envTicket = null; + } + if ($envTicket) { + $this->credentialsManager->store($user->getUID(), 'kerberos_ticket', base64_encode($envTicket->save())); + return $envTicket; + } + + $savedTicket = $this->credentialsManager->retrieve($user->getUID(), 'kerberos_ticket'); + if (!$savedTicket) { + throw new InsufficientDataForMeaningfulAnswerException('No kerberos ticket saved'); + } + return KerberosTicket::load(base64_decode($savedTicket)); + } +} diff --git a/apps/files_external/lib/Lib/Auth/SMB/KerberosSsoSession.php b/apps/files_external/lib/Lib/Auth/SMB/KerberosSsoSession.php new file mode 100644 index 0000000000000..76ad9bcdeb419 --- /dev/null +++ b/apps/files_external/lib/Lib/Auth/SMB/KerberosSsoSession.php @@ -0,0 +1,68 @@ + + * + * @author Robin Appelman + * + * @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\Files_External\Lib\Auth\SMB; + +use Icewind\SMB\KerberosTicket; +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\DefinitionParameter; +use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException; +use OCP\IL10N; +use OCP\ISession; + +class KerberosSsoSession extends AuthMechanism { + private ISession $session; + + public function __construct(IL10N $l, ISession $session) { + $realm = new DefinitionParameter('default_realm', 'Default realm'); + $realm + ->setType(DefinitionParameter::VALUE_TEXT) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL) + ->setTooltip($l->t('Kerberos default realm, defaults to "WORKGROUP"')); + $this + ->setIdentifier('smb::kerberos_sso_session') + ->setScheme(self::SCHEME_SMB) + ->setText($l->t('Kerberos ticket SSO, save in session')) + ->addParameter($realm); + $this->session = $session; + } + + public function getTicket(): KerberosTicket { + try { + $envTicket = KerberosTicket::fromEnv(); + } catch (\Exception $e) { + $envTicket = null; + } + if ($envTicket) { + $this->session->set('kerberos_ticket', base64_encode($envTicket->save())); + return $envTicket; + } + + $savedTicket = $this->session->get('kerberos_ticket'); + if (!$savedTicket) { + throw new InsufficientDataForMeaningfulAnswerException('No kerberos ticket saved'); + } + return KerberosTicket::load(base64_decode($savedTicket)); + } +} diff --git a/apps/files_external/lib/Lib/Backend/SMB.php b/apps/files_external/lib/Lib/Backend/SMB.php index bf73c5b40f844..d9daa47903d05 100644 --- a/apps/files_external/lib/Lib/Backend/SMB.php +++ b/apps/files_external/lib/Lib/Backend/SMB.php @@ -30,9 +30,12 @@ use Icewind\SMB\BasicAuth; use Icewind\SMB\KerberosApacheAuth; use Icewind\SMB\KerberosAuth; +use Icewind\SMB\KerberosTicket; use OCA\Files_External\Lib\Auth\AuthMechanism; use OCA\Files_External\Lib\Auth\Password\Password; use OCA\Files_External\Lib\Auth\SMB\KerberosApacheAuth as KerberosApacheAuthMechanism; +use OCA\Files_External\Lib\Auth\SMB\KerberosSsoDatabase; +use OCA\Files_External\Lib\Auth\SMB\KerberosSsoSession; use OCA\Files_External\Lib\DefinitionParameter; use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException; use OCA\Files_External\Lib\LegacyDependencyCheckPolyfill; @@ -89,16 +92,32 @@ public function manipulateStorageConfig(StorageConfig &$storage, IUser $user = n case 'smb::kerberos': $smbAuth = new KerberosAuth(); break; + case 'smb::kerberos_sso_database': + if (!$auth instanceof KerberosSsoDatabase) { + throw new \InvalidArgumentException('invalid authentication backend'); + } + $smbAuth = new KerberosAuth(); + $smbAuth->setTicket($auth->getTicket($user)); + break; + case 'smb::kerberos_sso_session': + if (!$auth instanceof KerberosSsoSession) { + throw new \InvalidArgumentException('invalid authentication backend'); + } + $smbAuth = new KerberosAuth(); + $smbAuth->setTicket($auth->getTicket()); + break; case 'smb::kerberosapache': if (!$auth instanceof KerberosApacheAuthMechanism) { throw new \InvalidArgumentException('invalid authentication backend'); } - $credentialsStore = $auth->getCredentialsStore(); - $kerbAuth = new KerberosApacheAuth(); + $ticket = KerberosTicket::fromEnv(); // check if a kerberos ticket is available, else fallback to session credentials - if ($kerbAuth->checkTicket()) { + if ($ticket && $ticket->isValid()) { + $kerbAuth = new KerberosAuth(); + $kerbAuth->setTicket($ticket); $smbAuth = $kerbAuth; } else { + $credentialsStore = $auth->getCredentialsStore(); try { $credentials = $credentialsStore->getLoginCredentials(); $user = $credentials->getLoginName(); diff --git a/apps/files_external/lib/Lib/TicketSaveMiddleware.php b/apps/files_external/lib/Lib/TicketSaveMiddleware.php new file mode 100644 index 0000000000000..1a3326870b7f8 --- /dev/null +++ b/apps/files_external/lib/Lib/TicketSaveMiddleware.php @@ -0,0 +1,91 @@ + + * + * @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\Files_External\Lib; + +use Icewind\SMB\KerberosTicket; +use OCA\Files_External\Controller\UserGlobalStoragesController; +use OCA\Files_External\Lib\Auth\SMB\KerberosSsoDatabase; +use OCA\Files_External\Lib\Auth\SMB\KerberosSsoSession; +use OCA\Files_External\Service\UserGlobalStoragesService; +use OCP\AppFramework\Http\Response; +use OCP\AppFramework\Middleware; +use OCP\ISession; +use OCP\IUser; +use OCP\IUserSession; +use OCP\Security\ICredentialsManager; + +class TicketSaveMiddleware extends Middleware { + const SAVE_SESSION = 1; + const SAVE_DB = 2; + + private ISession $session; + private IUserSession $userSession; + private UserGlobalStoragesService $storagesService; + private ICredentialsManager $credentialsManager; + + public function __construct( + ISession $session, + ICredentialsManager $credentialsManager, + IUserSession $userSession, + UserGlobalStoragesService $storagesService + ) { + $this->session = $session; + $this->credentialsManager = $credentialsManager; + $this->userSession = $userSession; + $this->storagesService = $storagesService; + } + + public function afterController($controller, $methodName, Response $response) { + $user = $this->userSession->getUser(); + if (!$user) { + return $response; + } + $ticket = KerberosTicket::fromEnv(); + if ($ticket && $ticket->isValid()) { + $save = $this->needToSaveTicket($user); + if ($save & self::SAVE_SESSION) { + $this->session->set('kerberos_ticket', base64_encode($ticket->save())); + } + if ($save & self::SAVE_DB) { + $this->credentialsManager->store($user->getUID(), 'kerberos_ticket', base64_encode($ticket->save())); + } + } + return $response; + } + + private function needToSaveTicket(IUser $user): int { + $save = 0; + $storages = $this->storagesService->getAllStoragesForUser($user); + foreach ($storages as $storage) { + $auth = $storage->getAuthMechanism(); + if ($auth instanceof KerberosSsoSession) { + $save = $save | self::SAVE_SESSION; + } + if ($auth instanceof KerberosSsoDatabase) { + $save = $save | self::SAVE_DB; + } + } + return $save; + } +} diff --git a/apps/files_external/tests/sso-setup/apache-session.conf b/apps/files_external/tests/sso-setup/apache-session.conf new file mode 100644 index 0000000000000..d401b07710ed6 --- /dev/null +++ b/apps/files_external/tests/sso-setup/apache-session.conf @@ -0,0 +1,31 @@ + + ServerAdmin webmaster@localhost + DocumentRoot /var/www/html + + + AuthType Kerberos + AuthName "Kerberos authenticated intranet" + KrbAuthRealms DOMAIN.TEST + KrbServiceName HTTP/httpd.domain.test + Krb5Keytab /shared/httpd.keytab + KrbMethodNegotiate On + KrbMethodK5Passwd On + KrbSaveCredentials On + require valid-user + + + + AuthType Kerberos + AuthName "Kerberos authenticated intranet" + KrbAuthRealms DOMAIN.TEST + KrbServiceName HTTP/httpd.domain.test + Krb5Keytab /shared/httpd.keytab + KrbMethodNegotiate On + KrbMethodK5Passwd On + KrbSaveCredentials On + require valid-user + + + ErrorLog /shared/apache-error.log + CustomLog /shared/apache-access.log combined + diff --git a/apps/files_external/tests/sso-setup/run.sh b/apps/files_external/tests/sso-setup/run.sh new file mode 100755 index 0000000000000..af7a72467a2d8 --- /dev/null +++ b/apps/files_external/tests/sso-setup/run.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -e + +SCRIPT_DIR="${0%/*}" + +DC_IP=$("$SCRIPT_DIR"/start-dc.sh) +"$SCRIPT_DIR"/start-apache.sh "$DC_IP" "$PWD" -v "$PWD/$SCRIPT_DIR"/apache-session.conf:/etc/apache2/sites-enabled/000-default.conf +"$SCRIPT_DIR"/setup-sso-nc.sh smb::kerberos_sso_session + +"$SCRIPT_DIR"/test-sso-smb-session.sh "$DC_IP" diff --git a/apps/files_external/tests/sso-setup/setup-sso-nc.sh b/apps/files_external/tests/sso-setup/setup-sso-nc.sh index 60cc51ff68d36..af73a48732d23 100755 --- a/apps/files_external/tests/sso-setup/setup-sso-nc.sh +++ b/apps/files_external/tests/sso-setup/setup-sso-nc.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash set -e +AUTH=${1:-"smb::kerberosapache"} + docker exec --user 33 apache ./occ maintenance:install --verbose --database=sqlite --database-name=nextcloud --database-host=127.0.0.1 --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass password docker exec --user 33 apache ./occ config:system:set trusted_domains 1 --value 'httpd.domain.test' @@ -15,7 +17,7 @@ docker exec -e OC_PASS=test --user 33 apache ./occ user:add 'testuser@DOMAIN.TES # setup external storage docker exec --user 33 apache ./occ app:enable files_external --force -docker exec --user 33 apache ./occ files_external:create smb smb smb::kerberosapache +docker exec --user 33 apache ./occ files_external:create smb smb "$AUTH" docker exec --user 33 apache ./occ files_external:config 1 host krb.domain.test docker exec --user 33 apache ./occ files_external:config 1 share netlogon docker exec --user 33 apache ./occ files_external:list diff --git a/apps/files_external/tests/sso-setup/start-apache.sh b/apps/files_external/tests/sso-setup/start-apache.sh index b483c73fa4ff9..b947c3fe052ab 100755 --- a/apps/files_external/tests/sso-setup/start-apache.sh +++ b/apps/files_external/tests/sso-setup/start-apache.sh @@ -5,12 +5,18 @@ SCRIPT_DIR="${0%/*}" docker rm -f apache 2>/dev/null > /dev/null -docker run -d --name apache -v $2:/var/www/html -v /var/www/html/data -v /var/www/html/config -v /var/www/html/extra-apps -v /tmp/shared:/shared --dns $1 --hostname httpd.domain.test icewind1991/samba-krb-test-apache 1>&2 +DC_IP="$1" +DIR="$2" +shift 2 + +# shellcheck disable=SC2068 +docker run -d --name apache -v "$DIR":/var/www/html -v /var/www/html/data -v /var/www/html/config -v /var/www/html/extra-apps -v /tmp/shared:/shared \ + --add-host host.docker.internal:host-gateway --dns "$DC_IP" --hostname httpd.domain.test $@ icewind1991/samba-krb-test-apache 1>&2 APACHE_IP=$(docker inspect apache --format '{{.NetworkSettings.IPAddress}}') docker exec apache chown 33 /var/www/html/config /var/www/html/data /var/www/html/extra-apps docker cp "$SCRIPT_DIR/apps.config.php" apache:/var/www/html/config/apps.config.php # add the dns record for apache -docker exec dc samba-tool dns add krb.domain.test domain.test httpd A $APACHE_IP -U administrator --password=passwOrd1 1>&2 +docker exec dc samba-tool dns add krb.domain.test domain.test httpd A "$APACHE_IP" -U administrator --password=passwOrd1 1>&2 -echo $APACHE_IP +echo "$APACHE_IP" diff --git a/apps/files_external/tests/sso-setup/start-dc.sh b/apps/files_external/tests/sso-setup/start-dc.sh index df8b02318dfe1..821a72bfb67b4 100755 --- a/apps/files_external/tests/sso-setup/start-dc.sh +++ b/apps/files_external/tests/sso-setup/start-dc.sh @@ -2,14 +2,14 @@ set -e function getContainerHealth { - docker inspect --format "{{.State.Health.Status}}" $1 + docker inspect --format "{{.State.Health.Status}}" "$1" } function waitContainer { - while STATUS=$(getContainerHealth $1); [ $STATUS != "healthy" ]; do - if [ $STATUS == "unhealthy" ]; then + while STATUS=$(getContainerHealth "$1"); [ "$STATUS" != "healthy" ]; do + if [ "$STATUS" == "unhealthy" ]; then echo "Failed!" 1>&2 - exit -1 + exit 1 fi printf . 1>&2 lf=$'\n' @@ -27,4 +27,6 @@ docker run -dit --name dc -v /tmp/shared:/shared --hostname krb.domain.test --ca waitContainer dc +sleep 5 + docker inspect dc --format '{{.NetworkSettings.IPAddress}}' diff --git a/apps/files_external/tests/sso-setup/test-sso-smb-session.sh b/apps/files_external/tests/sso-setup/test-sso-smb-session.sh new file mode 100755 index 0000000000000..d39b4dea815b6 --- /dev/null +++ b/apps/files_external/tests/sso-setup/test-sso-smb-session.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -e + +DC_IP="$1" +SCRIPT_DIR="${0%/*}" + +echo -n "Checking that we can authenticate using kerberos: " +LOGIN_CONTENT=$("$SCRIPT_DIR/client-cmd.sh" "$DC_IP" curl -i -s -c /shared/cookie -i -s --negotiate -u testuser@DOMAIN.TEST: --delegation always 'http://httpd.domain.test/index.php/apps/user_saml/saml/login?originalUrl=success&XDEBUG_SESSION_START=1') +if [[ "$LOGIN_CONTENT" =~ "Location: success" ]]; then + echo "✔️" +else + echo "❌" + exit 1 +fi + +"$SCRIPT_DIR/client-cmd.sh" "$DC_IP" curl -s -b /shared/cookie -c /shared/cookie --negotiate -u testuser@DOMAIN.TEST: --delegation always http://httpd.domain.test/index.php + +echo -n "Getting test with session file: " +CONTENT=$("$SCRIPT_DIR/client-cmd.sh" "$DC_IP" curl -s -b /shared/cookie 'http://httpd.domain.test/remote.php/webdav/smb/test.txt?XDEBUG_SESSION_START=1') +CONTENT=$(echo "$CONTENT" | head -n 1 | tr -d '[:space:]') + +if [[ $CONTENT == "testfile" ]]; then + echo "✔️" +else + echo "❌" + exit 1 +fi diff --git a/apps/files_external/tests/sso-setup/test-sso-smb.sh b/apps/files_external/tests/sso-setup/test-sso-smb.sh index b0f0a2c7af92f..d21df094e8df8 100755 --- a/apps/files_external/tests/sso-setup/test-sso-smb.sh +++ b/apps/files_external/tests/sso-setup/test-sso-smb.sh @@ -5,7 +5,7 @@ DC_IP="$1" SCRIPT_DIR="${0%/*}" echo -n "Checking that we can authenticate using kerberos: " -LOGIN_CONTENT=$("$SCRIPT_DIR/client-cmd.sh" $DC_IP curl -i -s --negotiate -u testuser@DOMAIN.TEST: --delegation always http://httpd.domain.test/index.php/apps/user_saml/saml/login?originalUrl=success) +LOGIN_CONTENT=$("$SCRIPT_DIR/client-cmd.sh" "$DC_IP" curl -i -s --negotiate -u testuser@DOMAIN.TEST: --delegation always http://httpd.domain.test/index.php/apps/user_saml/saml/login?originalUrl=success) if [[ "$LOGIN_CONTENT" =~ "Location: success" ]]; then echo "✔️" else @@ -13,8 +13,8 @@ else exit 1 fi echo -n "Getting test file: " -CONTENT=$("$SCRIPT_DIR/client-cmd.sh" $DC_IP curl -s --negotiate -u testuser@DOMAIN.TEST: --delegation always http://httpd.domain.test/remote.php/webdav/smb/test.txt) -CONTENT=$(echo $CONTENT | head -n 1 | tr -d '[:space:]') +CONTENT=$("$SCRIPT_DIR/client-cmd.sh" "$DC_IP" curl -s --negotiate -u testuser@DOMAIN.TEST: --delegation always http://httpd.domain.test/remote.php/webdav/smb/test.txt) +CONTENT=$(echo "$CONTENT" | head -n 1 | tr -d '[:space:]') if [[ $CONTENT == "testfile" ]]; then echo "✔️"