Merge pull request #30782 from nextcloud/backport/29349/stable23

This commit is contained in:
John Molakvoæ 2022-01-20 21:25:58 +01:00 committed by GitHub
commit c0b03000a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 633 additions and 126 deletions

78
.github/workflows/smb-kerberos.yml vendored Normal file
View file

@ -0,0 +1,78 @@
name: Samba Kerberos SSO
on:
push:
branches:
- master
- stable*
paths:
- 'apps/files_external/**'
pull_request:
paths:
- 'apps/files_external/**'
jobs:
smb-kerberos-tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php-versions: ['7.4', '8.0']
name: php${{ matrix.php-versions }}-${{ matrix.ftpd }}
steps:
- name: Checkout server
uses: actions/checkout@v2
with:
submodules: true
- name: Pull images
run: |
docker pull icewind1991/samba-krb-test-dc
docker pull icewind1991/samba-krb-test-apache
docker pull icewind1991/samba-krb-test-client
- name: Setup AD-DC
run: |
mkdir data
sudo chown -R 33 data apps config
apps/files_external/tests/setup-krb.sh
- name: Set up Nextcloud
run: |
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'
# setup user_saml
docker exec --user 33 apache ./occ app:enable user_saml --force
docker exec --user 33 apache ./occ config:app:set user_saml type --value 'environment-variable'
docker exec --user 33 apache ./occ config:app:set user_saml general-uid_mapping --value REMOTE_USER
# 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: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
- name: Test SSO
run: |
mkdir cookies
chmod 0777 cookies
DC_IP=$(docker inspect dc --format '{{.NetworkSettings.IPAddress}}')
docker run --rm --name client -v $PWD/cookies:/cookies -v /tmp/shared:/shared --dns $DC_IP --hostname client.domain.test icewind1991/samba-krb-test-client \
curl -c /cookies/jar -s --negotiate -u testuser@DOMAIN.TEST: --delegation always http://httpd.domain.test/index.php/apps/user_saml/saml/login
CONTENT=$(docker run --rm --name client -v $PWD/cookies:/cookies -v /tmp/shared:/shared --dns $DC_IP --hostname client.domain.test icewind1991/samba-krb-test-client \
curl -b /cookies/jar -s --negotiate -u testuser@DOMAIN.TEST: --delegation always http://httpd.domain.test/remote.php/webdav/smb/test.txt)
echo $CONTENT
CONTENT=$(echo $CONTENT | tr -d '[:space:]')
[[ $CONTENT == "testfile" ]]
smb-kerberos-summary:
runs-on: ubuntu-latest
needs: smb-kerberos-tests
if: always()
steps:
- name: Summary status
run: if ${{ needs.smb-kerberos-tests.result != 'success' }}; then exit 1; fi

View file

@ -5,6 +5,8 @@ icewind/smb/install_libsmbclient.sh
icewind/smb/Makefile
icewind/smb/.travis.yml
icewind/smb/.scrutinizer.yml
icewind/smb/example-apache-kerberos.php
icewind/smb/codecov.yml
icewind/streams/tests
.github
.php_cs*

View file

@ -9,6 +9,6 @@
},
"require": {
"icewind/streams": "0.7.4",
"icewind/smb": "3.4.1"
"icewind/smb": "3.5.2"
}
}

View file

@ -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": "0ffc772b2aaaaffe52decb8d13361976",
"content-hash": "524c99fd87297e01d004eb5a4e53b04c",
"packages": [
{
"name": "icewind/smb",
"version": "v3.4.1",
"version": "v3.5.2",
"source": {
"type": "git",
"url": "https://github.com/icewind1991/SMB.git",
"reference": "9dba42ab2a3990de29e18cc62b0a8270aceb74e3"
"reference": "0a425bd21acf7ae112b135dca34640e1b1a825c3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/icewind1991/SMB/zipball/9dba42ab2a3990de29e18cc62b0a8270aceb74e3",
"reference": "9dba42ab2a3990de29e18cc62b0a8270aceb74e3",
"url": "https://api.github.com/repos/icewind1991/SMB/zipball/0a425bd21acf7ae112b135dca34640e1b1a825c3",
"reference": "0a425bd21acf7ae112b135dca34640e1b1a825c3",
"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.4.1"
"source": "https://github.com/icewind1991/SMB/tree/v3.5.2"
},
"time": "2021-04-19T13:53:08+00:00"
"time": "2022-01-20T14:51:51+00:00"
},
{
"name": "icewind/streams",
@ -107,5 +107,5 @@
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.1.0"
"plugin-api-version": "2.2.0"
}

View file

@ -42,30 +42,75 @@ namespace Composer\Autoload;
*/
class ClassLoader
{
/** @var ?string */
private $vendorDir;
// PSR-4
/**
* @var array[]
* @psalm-var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array[]
* @psalm-var array<string, array<int, string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var array[]
* @psalm-var array<string, string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* @var array[]
* @psalm-var array<string, array<string, string[]>>
*/
private $prefixesPsr0 = array();
/**
* @var array[]
* @psalm-var array<string, string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var string[]
* @psalm-var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var bool[]
* @psalm-var array<string, bool>
*/
private $missingClasses = array();
/** @var ?string */
private $apcuPrefix;
/**
* @var self[]
*/
private static $registeredLoaders = array();
/**
* @param ?string $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
}
/**
* @return string[]
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
@ -75,28 +120,47 @@ class ClassLoader
return array();
}
/**
* @return array[]
* @psalm-return array<string, array<int, string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return array[]
* @psalm-return array<string, string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return array[]
* @psalm-return array<string, string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return string[] Array of classname => path
* @psalm-return array<string, string>
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array $classMap Class to filename map
* @param string[] $classMap Class to filename map
* @psalm-param array<string, string> $classMap
*
* @return void
*/
public function addClassMap(array $classMap)
{
@ -111,9 +175,11 @@ class ClassLoader
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param array|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
* @param string $prefix The prefix
* @param string[]|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
@ -156,11 +222,13 @@ class ClassLoader
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param string[]|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
@ -204,8 +272,10 @@ class ClassLoader
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param array|string $paths The PSR-0 base directories
* @param string $prefix The prefix
* @param string[]|string $paths The PSR-0 base directories
*
* @return void
*/
public function set($prefix, $paths)
{
@ -220,10 +290,12 @@ class ClassLoader
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-4 base directories
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param string[]|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
@ -243,6 +315,8 @@ class ClassLoader
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
@ -265,6 +339,8 @@ class ClassLoader
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
@ -285,6 +361,8 @@ class ClassLoader
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
@ -305,6 +383,8 @@ class ClassLoader
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
@ -324,6 +404,8 @@ class ClassLoader
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
@ -403,6 +485,11 @@ class ClassLoader
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
@ -474,6 +561,10 @@ class ClassLoader
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
* @private
*/
function includeFile($file)
{

View file

@ -20,12 +20,25 @@ use Composer\Semver\VersionParser;
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require it's presence, you can require `composer-runtime-api ^2.0`
* To require its presence, you can require `composer-runtime-api ^2.0`
*/
class InstalledVersions
{
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}|array{}|null
*/
private static $installed;
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
*/
private static $installedByVendor = array();
/**
@ -228,7 +241,7 @@ class InstalledVersions
/**
* @return array
* @psalm-return array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string}
* @psalm-return array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}
*/
public static function getRootPackage()
{
@ -242,7 +255,7 @@ class InstalledVersions
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string}>}
* @psalm-return array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}
*/
public static function getRawData()
{
@ -265,7 +278,7 @@ class InstalledVersions
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string}>}>
* @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
*/
public static function getAllRawData()
{
@ -288,7 +301,7 @@ class InstalledVersions
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string}>} $data
* @psalm-param array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>} $data
*/
public static function reload($data)
{
@ -298,7 +311,7 @@ class InstalledVersions
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string}>}>
* @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
*/
private static function getInstalled()
{

View file

@ -48,6 +48,7 @@ return array(
'Icewind\\SMB\\IShare' => $vendorDir . '/icewind/smb/src/IShare.php',
'Icewind\\SMB\\ISystem' => $vendorDir . '/icewind/smb/src/ISystem.php',
'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\\Native\\NativeFileInfo' => $vendorDir . '/icewind/smb/src/Native/NativeFileInfo.php',
'Icewind\\SMB\\Native\\NativeReadStream' => $vendorDir . '/icewind/smb/src/Native/NativeReadStream.php',

View file

@ -68,6 +68,7 @@ class ComposerStaticInit98fe9b281934250b3a93f69a5ce843b3
'Icewind\\SMB\\IShare' => __DIR__ . '/..' . '/icewind/smb/src/IShare.php',
'Icewind\\SMB\\ISystem' => __DIR__ . '/..' . '/icewind/smb/src/ISystem.php',
'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\\Native\\NativeFileInfo' => __DIR__ . '/..' . '/icewind/smb/src/Native/NativeFileInfo.php',
'Icewind\\SMB\\Native\\NativeReadStream' => __DIR__ . '/..' . '/icewind/smb/src/Native/NativeReadStream.php',

View file

@ -2,17 +2,17 @@
"packages": [
{
"name": "icewind/smb",
"version": "v3.4.1",
"version_normalized": "3.4.1.0",
"version": "v3.5.2",
"version_normalized": "3.5.2.0",
"source": {
"type": "git",
"url": "https://github.com/icewind1991/SMB.git",
"reference": "9dba42ab2a3990de29e18cc62b0a8270aceb74e3"
"reference": "0a425bd21acf7ae112b135dca34640e1b1a825c3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/icewind1991/SMB/zipball/9dba42ab2a3990de29e18cc62b0a8270aceb74e3",
"reference": "9dba42ab2a3990de29e18cc62b0a8270aceb74e3",
"url": "https://api.github.com/repos/icewind1991/SMB/zipball/0a425bd21acf7ae112b135dca34640e1b1a825c3",
"reference": "0a425bd21acf7ae112b135dca34640e1b1a825c3",
"shasum": ""
},
"require": {
@ -25,7 +25,7 @@
"phpunit/phpunit": "^8.5|^9.3.8",
"psalm/phar": "^4.3"
},
"time": "2021-04-19T13:53:08+00:00",
"time": "2022-01-20T14:51:51+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.4.1"
"source": "https://github.com/icewind1991/SMB/tree/v3.5.2"
},
"install-path": "../icewind/smb"
},

View file

@ -5,7 +5,7 @@
'type' => 'library',
'install_path' => __DIR__ . '/../',
'aliases' => array(),
'reference' => '70483a16a3a232758979bb6fa363629b5a16b6a4',
'reference' => '0bed61f949bc7a8c69cd154919e78b704e28c99e',
'name' => 'files_external/3rdparty',
'dev' => true,
),
@ -16,16 +16,16 @@
'type' => 'library',
'install_path' => __DIR__ . '/../',
'aliases' => array(),
'reference' => '70483a16a3a232758979bb6fa363629b5a16b6a4',
'reference' => '0bed61f949bc7a8c69cd154919e78b704e28c99e',
'dev_requirement' => false,
),
'icewind/smb' => array(
'pretty_version' => 'v3.4.1',
'version' => '3.4.1.0',
'pretty_version' => 'v3.5.2',
'version' => '3.5.2.0',
'type' => 'library',
'install_path' => __DIR__ . '/../icewind/smb',
'aliases' => array(),
'reference' => '9dba42ab2a3990de29e18cc62b0a8270aceb74e3',
'reference' => '0a425bd21acf7ae112b135dca34640e1b1a825c3',
'dev_requirement' => false,
),
'icewind/streams' => array(

View file

@ -44,13 +44,42 @@ $server = $serverFactory->createServer('localhost', $auth);
### Using kerberos authentication ###
There are two ways of using kerberos to authenticate against the smb server:
- Using a ticket from the php server
- Re-using a ticket send by the client
### Using a server ticket
Using a server ticket allows the web server to authenticate against the smb server using an existing machine account.
The ticket needs to be available in the environment of the php process.
```php
$serverFactory = new ServerFactory();
$auth = new KerberosAuth();
$server = $serverFactory->createServer('localhost', $auth);
```
Note that this requires a valid kerberos ticket to already be available for php
### Re-using a client ticket
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.
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
- Php has the krb5 extension installed
- The client authenticates using a ticket with forwarding enabled
```php
$serverFactory = new ServerFactory();
$auth = new KerberosApacheAuth();
$server = $serverFactory->createServer('localhost', $auth);
```
### Upload a file ###

View file

@ -45,7 +45,7 @@ interface IShare {
public function put(string $source, string $target): bool;
/**
* Open a readable stream top a remote file
* Open a readable stream to a remote file
*
* @param string $source
* @return resource a read only stream with the contents of the remote file

View file

@ -0,0 +1,136 @@
<?php
/**
* @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
namespace Icewind\SMB;
use Icewind\SMB\Exception\DependencyException;
use Icewind\SMB\Exception\Exception;
/**
* Use existing kerberos ticket to authenticate and reuse the apache ticket cache (mod_auth_kerb)
*/
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");
}
/**
* 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();
}
/**
* 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);
}
}
}

View file

@ -99,7 +99,7 @@ class NativeFileInfo implements IFileInfo {
public function isDirectory(): bool {
$mode = $this->getMode();
if ($mode > 0x1000) {
return (bool)($mode & 0x4000); // 0x4000: unix directory flag
return ($mode & 0x4000 && !($mode & 0x8000)); // 0x4000: unix directory flag shares bits with 0xC000: socket
} else {
return (bool)($mode & IFileInfo::MODE_DIRECTORY);
}

View file

@ -267,14 +267,14 @@ class NativeShare extends AbstractShare {
* Open a writeable stream to a remote file
* Note: This method will truncate the file to 0bytes first
*
* @param string $source
* @param string $target
* @return resource a writeable stream
*
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function write(string $source) {
$url = $this->buildUrl($source);
public function write(string $target) {
$url = $this->buildUrl($target);
$handle = $this->getState()->create($url);
return NativeWriteStream::wrap($this->getState(), $handle, 'w', $url);
}
@ -282,14 +282,14 @@ class NativeShare extends AbstractShare {
/**
* Open a writeable stream and set the cursor to the end of the stream
*
* @param string $source
* @param string $target
* @return resource a writeable stream
*
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function append(string $source) {
$url = $this->buildUrl($source);
public function append(string $target) {
$url = $this->buildUrl($target);
$handle = $this->getState()->open($url, "a+");
return NativeWriteStream::wrap($this->getState(), $handle, "a", $url);
}

View file

@ -38,6 +38,14 @@ class NativeState {
/** @var bool */
protected $connected = false;
/**
* sync the garbage collection cycle
* __deconstruct() of KerberosAuth should not called too soon
*
* @var IAuth|null $auth
*/
protected $auth = null;
// see error.h
const EXCEPTION_MAP = [
1 => ForbiddenException::class,
@ -107,6 +115,11 @@ class NativeState {
}
$auth->setExtraSmbClientOptions($this->state);
// sync the garbage collection cycle
// __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());

View file

@ -62,7 +62,7 @@ class System implements ISystem {
$result = null;
$output = [];
exec("which $binary 2>&1", $output, $result);
$this->paths[$binary] = $result === 0 ? trim(implode('', $output)) : null;
$this->paths[$binary] = $result === 0 && isset($output[0]) ? (string)$output[0] : null;
}
return $this->paths[$binary];
}

View file

@ -47,7 +47,7 @@ class Connection extends RawConnection {
public function clearTillPrompt(): void {
$this->write('');
do {
$promptLine = $this->readLine();
$promptLine = $this->readTillPrompt();
if ($promptLine === false) {
break;
}
@ -56,13 +56,12 @@ class Connection extends RawConnection {
if ($this->write('') === false) {
throw new ConnectionRefusedException();
}
$this->readLine();
$this->readTillPrompt();
}
/**
* get all unprocessed output from smbclient until the next prompt
*
* @param (callable(string):bool)|null $callback (optional) callback to call for every line read
* @return string[]
* @throws AuthenticationException
* @throws ConnectException
@ -71,42 +70,22 @@ class Connection extends RawConnection {
* @throws NoLoginServerException
* @throws AccessDeniedException
*/
public function read(callable $callback = null): array {
public function read(): array {
if (!$this->isValid()) {
throw new ConnectionException('Connection not valid');
}
$promptLine = $this->readLine(); //first line is prompt
if ($promptLine === false) {
$this->unknownError($promptLine);
}
$this->parser->checkConnectionError($promptLine);
$output = [];
if (!$this->isPrompt($promptLine)) {
$line = $promptLine;
} else {
$line = $this->readLine();
}
if ($line === false) {
$this->unknownError($promptLine);
}
while ($line !== false && !$this->isPrompt($line)) { //next prompt functions as delimiter
if (is_callable($callback)) {
$result = $callback($line);
if ($result === false) { // allow the callback to close the connection for infinite running commands
$this->close(true);
break;
}
} else {
$output[] = $line;
}
$line = $this->readLine();
$output = $this->readTillPrompt();
if ($output === false) {
$this->unknownError(false);
}
$output = explode("\n", $output);
// last line contains the prompt
array_pop($output);
return $output;
}
private function isPrompt(string $line): bool {
return mb_substr($line, 0, self::DELIMITER_LENGTH) === self::DELIMITER;
return substr($line, 0, self::DELIMITER_LENGTH) === self::DELIMITER;
}
/**
@ -132,6 +111,6 @@ class Connection extends RawConnection {
// ignore any errors while trying to send the close command, the process might already be dead
@$this->write('close' . PHP_EOL);
}
parent::close($terminate);
$this->close_process($terminate);
}
}

View file

@ -65,16 +65,20 @@ class NotifyHandler implements INotifyHandler {
*/
public function listen(callable $callback): void {
if ($this->listening) {
$this->connection->read(function (string $line) use ($callback): bool {
while (true) {
$line = $this->connection->readLine();
if ($line === false) {
break;
}
$this->checkForError($line);
$change = $this->parseChangeLine($line);
if ($change) {
$result = $callback($change);
return $result === false ? false : true;
} else {
return true;
if ($result === false) {
break;
}
}
});
};
}
}

View file

@ -71,7 +71,8 @@ class RawConnection {
setlocale(LC_ALL, Server::LOCALE);
$env = array_merge($this->env, [
'CLI_FORCE_INTERACTIVE' => 'y', // Needed or the prompt isn't displayed!!
'CLI_FORCE_INTERACTIVE' => 'y', // Make sure the prompt is displayed
'CLI_NO_READLINE' => 1, // Not all distros build smbclient with readline, disable it to get consistent behaviour
'LC_ALL' => Server::LOCALE,
'LANG' => Server::LOCALE,
'COLUMNS' => 8192 // prevent smbclient from line-wrapping it's output
@ -91,7 +92,7 @@ class RawConnection {
public function isValid(): bool {
if (is_resource($this->process)) {
$status = proc_get_status($this->process);
return (bool)$status['running'];
return $status['running'];
} else {
return false;
}
@ -109,13 +110,30 @@ class RawConnection {
return $result;
}
/**
* read output till the next prompt
*
* @return string|false
*/
public function readTillPrompt() {
$output = "";
do {
$chunk = $this->readLine('\> ');
if ($chunk === false) {
return false;
}
$output .= $chunk;
} while (strlen($chunk) == 4096 && strpos($chunk, "smb:") === false);
return $output;
}
/**
* read a line of output
*
* @return string|false
*/
public function readLine() {
return stream_get_line($this->getOutputStream(), 4086, "\n");
public function readLine(string $end = "\n") {
return stream_get_line($this->getOutputStream(), 4096, $end);
}
/**
@ -202,6 +220,14 @@ class RawConnection {
* @psalm-assert null $this->process
*/
public function close(bool $terminate = true): void {
$this->close_process($terminate);
}
/**
* @param bool $terminate
* @psalm-assert null $this->process
*/
protected function close_process(bool $terminate = true): void {
if (!is_resource($this->process)) {
return;
}

View file

@ -345,11 +345,17 @@ class Share extends AbstractShare {
// since returned stream is closed by the caller we need to create a new instance
// since we can't re-use the same file descriptor over multiple calls
$connection = $this->getConnection();
stream_set_blocking($connection->getOutputStream(), false);
$connection->write('get ' . $source . ' ' . $this->system->getFD(5));
$connection->write('exit');
$fh = $connection->getFileOutputStream();
stream_context_set_option($fh, 'file', 'connection', $connection);
$fh = CallbackWrapper::wrap($fh, function() use ($connection) {
$connection->write('');
});
if (!is_resource($fh)) {
throw new Exception("Failed to wrap file output");
}
return $fh;
}
@ -374,7 +380,9 @@ class Share extends AbstractShare {
// use a close callback to ensure the upload is finished before continuing
// this also serves as a way to keep the connection in scope
$stream = CallbackWrapper::wrap($fh, null, null, function () use ($connection) {
$stream = CallbackWrapper::wrap($fh, function() use ($connection) {
$connection->write('');
}, null, function () use ($connection) {
$connection->close(false); // dont terminate, give the upload some time
});
if (is_resource($stream)) {
@ -446,7 +454,7 @@ class Share extends AbstractShare {
* @return string[]
*/
protected function execute(string $command): array {
$this->connect()->write($command . PHP_EOL);
$this->connect()->write($command);
return $this->connect()->read();
}

View file

@ -31,8 +31,6 @@ namespace OCA\Files_External\AppInfo;
use OCA\Files_External\Config\ConfigAdapter;
use OCA\Files_External\Config\UserPlaceholderHandler;
use OCA\Files_External\Listener\GroupDeletedListener;
use OCA\Files_External\Listener\UserDeletedListener;
use OCA\Files_External\Lib\Auth\AmazonS3\AccessKey;
use OCA\Files_External\Lib\Auth\Builtin;
use OCA\Files_External\Lib\Auth\NullMechanism;
@ -49,6 +47,7 @@ use OCA\Files_External\Lib\Auth\Password\UserGlobalAuth;
use OCA\Files_External\Lib\Auth\Password\UserProvided;
use OCA\Files_External\Lib\Auth\PublicKey\RSA;
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\Backend\AmazonS3;
use OCA\Files_External\Lib\Backend\DAV;
@ -62,6 +61,8 @@ use OCA\Files_External\Lib\Backend\SMB_OC;
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\Listener\GroupDeletedListener;
use OCA\Files_External\Listener\UserDeletedListener;
use OCA\Files_External\Service\BackendService;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
@ -126,16 +127,16 @@ class Application extends App implements IBackendProvider, IAuthMechanismProvide
$container = $this->getContainer();
$backends = [
$container->query(Local::class),
$container->query(FTP::class),
$container->query(DAV::class),
$container->query(OwnCloud::class),
$container->query(SFTP::class),
$container->query(AmazonS3::class),
$container->query(Swift::class),
$container->query(SFTP_Key::class),
$container->query(SMB::class),
$container->query(SMB_OC::class),
$container->get(Local::class),
$container->get(FTP::class),
$container->get(DAV::class),
$container->get(OwnCloud::class),
$container->get(SFTP::class),
$container->get(AmazonS3::class),
$container->get(Swift::class),
$container->get(SFTP_Key::class),
$container->get(SMB::class),
$container->get(SMB_OC::class),
];
return $backends;
@ -149,37 +150,38 @@ class Application extends App implements IBackendProvider, IAuthMechanismProvide
return [
// AuthMechanism::SCHEME_NULL mechanism
$container->query(NullMechanism::class),
$container->get(NullMechanism::class),
// AuthMechanism::SCHEME_BUILTIN mechanism
$container->query(Builtin::class),
$container->get(Builtin::class),
// AuthMechanism::SCHEME_PASSWORD mechanisms
$container->query(Password::class),
$container->query(SessionCredentials::class),
$container->query(LoginCredentials::class),
$container->query(UserProvided::class),
$container->query(GlobalAuth::class),
$container->query(UserGlobalAuth::class),
$container->get(Password::class),
$container->get(SessionCredentials::class),
$container->get(LoginCredentials::class),
$container->get(UserProvided::class),
$container->get(GlobalAuth::class),
$container->get(UserGlobalAuth::class),
// AuthMechanism::SCHEME_OAUTH1 mechanisms
$container->query(OAuth1::class),
$container->get(OAuth1::class),
// AuthMechanism::SCHEME_OAUTH2 mechanisms
$container->query(OAuth2::class),
$container->get(OAuth2::class),
// AuthMechanism::SCHEME_PUBLICKEY mechanisms
$container->query(RSA::class),
$container->query(RSAPrivateKey::class),
$container->get(RSA::class),
$container->get(RSAPrivateKey::class),
// AuthMechanism::SCHEME_OPENSTACK mechanisms
$container->query(OpenStackV2::class),
$container->query(OpenStackV3::class),
$container->query(Rackspace::class),
$container->get(OpenStackV2::class),
$container->get(OpenStackV3::class),
$container->get(Rackspace::class),
// Specialized mechanisms
$container->query(AccessKey::class),
$container->query(KerberosAuth::class),
$container->get(AccessKey::class),
$container->get(KerberosAuth::class),
$container->get(KerberosApacheAuth::class),
];
}
}

View file

@ -0,0 +1,53 @@
<?php
/**
* @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl>
*
* @author Robin Appelman <robin@icewind.nl>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Files_External\Lib\Auth\SMB;
use OCA\Files_External\Lib\Auth\AuthMechanism;
use OCA\Files_External\Lib\DefinitionParameter;
use OCP\Authentication\LoginCredentials\IStore;
use OCP\IL10N;
class KerberosApacheAuth extends AuthMechanism {
/** @var IStore */
private $credentialsStore;
public function __construct(IL10N $l, IStore $credentialsStore) {
$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::kerberosapache')
->setScheme(self::SCHEME_SMB)
->setText($l->t('Kerberos ticket apache mode'))
->addParameter($realm);
$this->credentialsStore = $credentialsStore;
}
public function getCredentialsStore(): IStore {
return $this->credentialsStore;
}
}

View file

@ -24,16 +24,19 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Files_External\Lib\Backend;
use Icewind\SMB\BasicAuth;
use Icewind\SMB\KerberosApacheAuth;
use Icewind\SMB\KerberosAuth;
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\DefinitionParameter;
use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException;
use OCA\Files_External\Lib\LegacyDependencyCheckPolyfill;
use OCA\Files_External\Lib\StorageConfig;
use OCP\IL10N;
use OCP\IUser;
@ -69,10 +72,6 @@ class SMB extends Backend {
->setLegacyAuthMechanism($legacyAuth);
}
/**
* @param StorageConfig $storage
* @param IUser $user
*/
public function manipulateStorageConfig(StorageConfig &$storage, IUser $user = null) {
$auth = $storage->getAuthMechanism();
if ($auth->getScheme() === AuthMechanism::SCHEME_PASSWORD) {
@ -89,6 +88,45 @@ class SMB extends Backend {
switch ($auth->getIdentifier()) {
case 'smb::kerberos':
$smbAuth = new KerberosAuth();
break;
case 'smb::kerberosapache':
if (!$auth instanceof KerberosApacheAuthMechanism) {
throw new \InvalidArgumentException('invalid authentication backend');
}
$credentialsStore = $auth->getCredentialsStore();
$kerbAuth = new KerberosApacheAuth();
// check if a kerberos ticket is available, else fallback to session credentials
if ($kerbAuth->checkTicket()) {
$smbAuth = $kerbAuth;
} else {
try {
$credentials = $credentialsStore->getLoginCredentials();
$user = $credentials->getLoginName();
$pass = $credentials->getPassword();
preg_match('/(.*)@(.*)/', $user, $matches);
$realm = $storage->getBackendOption('default_realm');
if (empty($realm)) {
$realm = 'WORKGROUP';
}
$userPart = $matches[1];
$domainPart = $matches[2];
if (count($matches) === 0) {
$username = $user;
$workgroup = $realm;
} else {
$username = $userPart;
$workgroup = $domainPart;
}
$smbAuth = new BasicAuth(
$username,
$workgroup,
$pass
);
} catch (\Exception $e) {
throw new InsufficientDataForMeaningfulAnswerException('No session credentials saved');
}
}
break;
default:
throw new \InvalidArgumentException('unknown authentication backend');

View file

@ -0,0 +1,33 @@
#!/bin/bash
function getContainerHealth {
docker inspect --format "{{.State.Health.Status}}" $1
}
function waitContainer {
while STATUS=$(getContainerHealth $1); [ $STATUS != "healthy" ]; do
if [ $STATUS == "unhealthy" ]; then
echo "Failed!"
exit -1
fi
printf .
lf=$'\n'
sleep 1
done
printf "$lf"
}
mkdir /tmp/shared
# start the dc
docker run -dit --name dc -v /tmp/shared:/shared --hostname krb.domain.test --cap-add SYS_ADMIN icewind1991/samba-krb-test-dc
DC_IP=$(docker inspect dc --format '{{.NetworkSettings.IPAddress}}')
waitContainer dc
# start apache
docker run -d --name apache -v $PWD:/var/www/html -v /tmp/shared:/shared --dns $DC_IP --hostname httpd.domain.test icewind1991/samba-krb-test-apache
APACHE_IP=$(docker inspect apache --format '{{.NetworkSettings.IPAddress}}')
# 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