mirror of
https://codeberg.org/icewind/SMB.git
synced 2026-06-03 09:14:06 +02:00
allow getting acls of files
This commit is contained in:
parent
acd992ad8c
commit
3c5e45dc54
12 changed files with 237 additions and 13 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -2,4 +2,5 @@
|
||||||
vendor
|
vendor
|
||||||
composer.lock
|
composer.lock
|
||||||
.php_cs.cache
|
.php_cs.cache
|
||||||
|
listen.php
|
||||||
|
test.php
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require" : {
|
"require" : {
|
||||||
"php": ">=5.6",
|
"php": ">=7.1",
|
||||||
"icewind/streams": ">=0.2.0"
|
"icewind/streams": ">=0.2.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
|
|
||||||
81
src/ACL.php
Normal file
81
src/ACL.php
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2020 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;
|
||||||
|
|
||||||
|
class ACL {
|
||||||
|
const TYPE_ALLOW = 0;
|
||||||
|
const TYPE_DENY = 1;
|
||||||
|
|
||||||
|
const MASK_READ = 0x0001;
|
||||||
|
const MASK_WRITE = 0x0002;
|
||||||
|
const MASK_EXECUTE = 0x00020;
|
||||||
|
const MASK_DELETE = 0x10000;
|
||||||
|
|
||||||
|
const FLAG_OBJECT_INHERIT = 0x1;
|
||||||
|
const FLAG_CONTAINER_INHERIT = 0x2;
|
||||||
|
|
||||||
|
private $type;
|
||||||
|
private $flags;
|
||||||
|
private $mask;
|
||||||
|
|
||||||
|
public function __construct(int $type, int $flags, int $mask) {
|
||||||
|
$this->type = $type;
|
||||||
|
$this->flags = $flags;
|
||||||
|
$this->mask = $mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the acl allows a specific permissions
|
||||||
|
*
|
||||||
|
* Note that this does not take inherited acls into account
|
||||||
|
*
|
||||||
|
* @param int $mask one of the ACL::MASK_* constants
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function allows(int $mask): bool {
|
||||||
|
return $this->type === self::TYPE_ALLOW && ($this->mask & $mask) === $mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the acl allows a specific permissions
|
||||||
|
*
|
||||||
|
* Note that this does not take inherited acls into account
|
||||||
|
*
|
||||||
|
* @param int $mask one of the ACL::MASK_* constants
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function denies(int $mask): bool {
|
||||||
|
return $this->type === self::TYPE_DENY && ($this->mask & $mask) === $mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getType(): int {
|
||||||
|
return $this->type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFlags(): int {
|
||||||
|
return $this->flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMask(): int {
|
||||||
|
return $this->mask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -65,4 +65,9 @@ interface IFileInfo {
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function isArchived();
|
public function isArchived();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return ACL[]
|
||||||
|
*/
|
||||||
|
public function getAcls(): array;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,13 @@ interface ISystem {
|
||||||
*/
|
*/
|
||||||
public function getNetPath();
|
public function getNetPath();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the full path to the `smbcacls` binary of false if the binary is not available
|
||||||
|
*
|
||||||
|
* @return string|bool
|
||||||
|
*/
|
||||||
|
public function getSmbcAclsPath();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the full path to the `stdbuf` binary of false if the binary is not available
|
* Get the full path to the `stdbuf` binary of false if the binary is not available
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
namespace Icewind\SMB\Native;
|
namespace Icewind\SMB\Native;
|
||||||
|
|
||||||
|
use Icewind\SMB\ACL;
|
||||||
use Icewind\SMB\IFileInfo;
|
use Icewind\SMB\IFileInfo;
|
||||||
|
|
||||||
class NativeFileInfo implements IFileInfo {
|
class NativeFileInfo implements IFileInfo {
|
||||||
|
|
@ -156,4 +157,22 @@ class NativeFileInfo implements IFileInfo {
|
||||||
$mode = $this->getMode();
|
$mode = $this->getMode();
|
||||||
return (bool)($mode & IFileInfo::MODE_ARCHIVE);
|
return (bool)($mode & IFileInfo::MODE_ARCHIVE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return ACL[]
|
||||||
|
*/
|
||||||
|
public function getAcls(): array {
|
||||||
|
$acls = [];
|
||||||
|
$attribute = $this->share->getAttribute($this->path, 'system.nt_sec_desc.acl.*+');
|
||||||
|
|
||||||
|
foreach (explode(',', $attribute) as $acl) {
|
||||||
|
[$user, $permissions] = explode(':', $acl, 2);
|
||||||
|
[$type, $flags, $mask] = explode('/', $permissions);
|
||||||
|
$mask = hexdec($mask);
|
||||||
|
|
||||||
|
$acls[$user] = new ACL($type, $flags, $mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $acls;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,10 @@ class System implements ISystem {
|
||||||
return $this->getBinaryPath('net');
|
return $this->getBinaryPath('net');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getSmbcAclsPath() {
|
||||||
|
return $this->getBinaryPath('smbcacls');
|
||||||
|
}
|
||||||
|
|
||||||
public function getStdBufPath() {
|
public function getStdBufPath() {
|
||||||
return $this->getBinaryPath('stdbuf');
|
return $this->getBinaryPath('stdbuf');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
namespace Icewind\SMB\Wrapped;
|
namespace Icewind\SMB\Wrapped;
|
||||||
|
|
||||||
|
use Icewind\SMB\ACL;
|
||||||
use Icewind\SMB\IFileInfo;
|
use Icewind\SMB\IFileInfo;
|
||||||
|
|
||||||
class FileInfo implements IFileInfo {
|
class FileInfo implements IFileInfo {
|
||||||
|
|
@ -35,19 +36,26 @@ class FileInfo implements IFileInfo {
|
||||||
*/
|
*/
|
||||||
protected $mode;
|
protected $mode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var callable
|
||||||
|
*/
|
||||||
|
protected $aclCallback;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $path
|
* @param string $path
|
||||||
* @param string $name
|
* @param string $name
|
||||||
* @param int $size
|
* @param int $size
|
||||||
* @param int $time
|
* @param int $time
|
||||||
* @param int $mode
|
* @param int $mode
|
||||||
|
* @param callable $aclCallback
|
||||||
*/
|
*/
|
||||||
public function __construct($path, $name, $size, $time, $mode) {
|
public function __construct($path, $name, $size, $time, $mode, callable $aclCallback) {
|
||||||
$this->path = $path;
|
$this->path = $path;
|
||||||
$this->name = $name;
|
$this->name = $name;
|
||||||
$this->size = $size;
|
$this->size = $size;
|
||||||
$this->time = $time;
|
$this->time = $time;
|
||||||
$this->mode = $mode;
|
$this->mode = $mode;
|
||||||
|
$this->aclCallback = $aclCallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -112,4 +120,11 @@ class FileInfo implements IFileInfo {
|
||||||
public function isArchived() {
|
public function isArchived() {
|
||||||
return (bool)($this->mode & IFileInfo::MODE_ARCHIVE);
|
return (bool)($this->mode & IFileInfo::MODE_ARCHIVE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return ACL[]
|
||||||
|
*/
|
||||||
|
public function getAcls(): array {
|
||||||
|
return ($this->aclCallback)();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,7 @@ class Parser {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function parseDir($output, $basePath) {
|
public function parseDir($output, $basePath, callable $aclCallback) {
|
||||||
//last line is used space
|
//last line is used space
|
||||||
array_pop($output);
|
array_pop($output);
|
||||||
$regex = '/^\s*(.*?)\s\s\s\s+(?:([NDHARS]*)\s+)?([0-9]+)\s+(.*)$/';
|
$regex = '/^\s*(.*?)\s\s\s\s+(?:([NDHARS]*)\s+)?([0-9]+)\s+(.*)$/';
|
||||||
|
|
@ -162,7 +162,10 @@ class Parser {
|
||||||
if ($name !== '.' and $name !== '..') {
|
if ($name !== '.' and $name !== '..') {
|
||||||
$mode = $this->parseMode($mode);
|
$mode = $this->parseMode($mode);
|
||||||
$time = strtotime($time . ' ' . $this->timeZone);
|
$time = strtotime($time . ' ' . $this->timeZone);
|
||||||
$content[] = new FileInfo($basePath . '/' . $name, $name, $size, $time, $mode);
|
$path = $basePath . '/' . $name;
|
||||||
|
$content[] = new FileInfo($path, $name, $size, $time, $mode, function () use ($aclCallback, $path) {
|
||||||
|
return $aclCallback($path);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
namespace Icewind\SMB\Wrapped;
|
namespace Icewind\SMB\Wrapped;
|
||||||
|
|
||||||
use Icewind\SMB\AbstractShare;
|
use Icewind\SMB\AbstractShare;
|
||||||
|
use Icewind\SMB\ACL;
|
||||||
use Icewind\SMB\Exception\ConnectionException;
|
use Icewind\SMB\Exception\ConnectionException;
|
||||||
use Icewind\SMB\Exception\DependencyException;
|
use Icewind\SMB\Exception\DependencyException;
|
||||||
use Icewind\SMB\Exception\FileInUseException;
|
use Icewind\SMB\Exception\FileInUseException;
|
||||||
|
|
@ -153,7 +154,9 @@ class Share extends AbstractShare {
|
||||||
|
|
||||||
$this->execute('cd /');
|
$this->execute('cd /');
|
||||||
|
|
||||||
return $this->parser->parseDir($output, $path);
|
return $this->parser->parseDir($output, $path, function ($path) {
|
||||||
|
return $this->getAcls($path);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -186,7 +189,9 @@ class Share extends AbstractShare {
|
||||||
$this->parseOutput($output, $path);
|
$this->parseOutput($output, $path);
|
||||||
}
|
}
|
||||||
$stat = $this->parser->parseStat($output);
|
$stat = $this->parser->parseStat($output);
|
||||||
return new FileInfo($path, basename($path), $stat['size'], $stat['mtime'], $stat['mode']);
|
return new FileInfo($path, basename($path), $stat['size'], $stat['mtime'], $stat['mode'], function () use ($path) {
|
||||||
|
return $this->getAcls($path);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -421,13 +426,13 @@ class Share extends AbstractShare {
|
||||||
* @param string[] $lines
|
* @param string[] $lines
|
||||||
* @param string $path
|
* @param string $path
|
||||||
*
|
*
|
||||||
* @throws NotFoundException
|
* @return bool
|
||||||
* @throws \Icewind\SMB\Exception\AlreadyExistsException
|
* @throws \Icewind\SMB\Exception\AlreadyExistsException
|
||||||
* @throws \Icewind\SMB\Exception\AccessDeniedException
|
* @throws \Icewind\SMB\Exception\AccessDeniedException
|
||||||
* @throws \Icewind\SMB\Exception\NotEmptyException
|
* @throws \Icewind\SMB\Exception\NotEmptyException
|
||||||
* @throws \Icewind\SMB\Exception\InvalidTypeException
|
* @throws \Icewind\SMB\Exception\InvalidTypeException
|
||||||
* @throws \Icewind\SMB\Exception\Exception
|
* @throws \Icewind\SMB\Exception\Exception
|
||||||
* @return bool
|
* @throws NotFoundException
|
||||||
*/
|
*/
|
||||||
protected function parseOutput($lines, $path = '') {
|
protected function parseOutput($lines, $path = '') {
|
||||||
if (count($lines) === 0) {
|
if (count($lines) === 0) {
|
||||||
|
|
@ -470,6 +475,84 @@ class Share extends AbstractShare {
|
||||||
return '"' . $path . '"';
|
return '"' . $path . '"';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function getAcls($path) {
|
||||||
|
$commandPath = $this->system->getSmbcAclsPath();
|
||||||
|
if (!$commandPath) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$command = sprintf(
|
||||||
|
'%s %s %s %s/%s %s',
|
||||||
|
$commandPath,
|
||||||
|
$this->getAuthFileArgument(),
|
||||||
|
$this->server->getAuth()->getExtraCommandLineArguments(),
|
||||||
|
escapeshellarg('//' . $this->server->getHost()),
|
||||||
|
escapeshellarg($this->name),
|
||||||
|
escapeshellarg($path)
|
||||||
|
);
|
||||||
|
$connection = new RawConnection($command);
|
||||||
|
$connection->writeAuthentication($this->server->getAuth()->getUsername(), $this->server->getAuth()->getPassword());
|
||||||
|
$connection->connect();
|
||||||
|
if (!$connection->isValid()) {
|
||||||
|
throw new ConnectionException($connection->readLine());
|
||||||
|
}
|
||||||
|
|
||||||
|
$rawAcls = $connection->readAll();
|
||||||
|
var_dump($rawAcls);
|
||||||
|
|
||||||
|
$acls = [];
|
||||||
|
foreach ($rawAcls as $acl) {
|
||||||
|
[$type, $acl] = explode(':', $acl, 2);
|
||||||
|
if ($type !== 'ACL') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
[$user, $permissions] = explode(':', $acl, 2);
|
||||||
|
[$type, $flags, $mask] = explode('/', $permissions);
|
||||||
|
|
||||||
|
$type = $type === 'ALLOWED' ? ACL::TYPE_ALLOW : ACL::TYPE_DENY;
|
||||||
|
|
||||||
|
$flagsInt = 0;
|
||||||
|
foreach (explode('|', $flags) as $flagString) {
|
||||||
|
if ($flagString === 'OI') {
|
||||||
|
$flagsInt += ACL::FLAG_OBJECT_INHERIT;
|
||||||
|
} elseif ($flagString === 'CI') {
|
||||||
|
$flagsInt += ACL::FLAG_CONTAINER_INHERIT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (substr($mask, 0, 2) === '0x') {
|
||||||
|
$maskInt = hexdec($mask);
|
||||||
|
} else {
|
||||||
|
$maskInt = 0;
|
||||||
|
foreach (explode('|', $mask) as $maskString) {
|
||||||
|
if ($maskString === 'R') {
|
||||||
|
$maskInt += ACL::MASK_READ;
|
||||||
|
} elseif ($maskString === 'W') {
|
||||||
|
$maskInt += ACL::MASK_WRITE;
|
||||||
|
} elseif ($maskString === 'X') {
|
||||||
|
$maskInt += ACL::MASK_EXECUTE;
|
||||||
|
} elseif ($maskString === 'D') {
|
||||||
|
$maskInt += ACL::MASK_DELETE;
|
||||||
|
} elseif ($maskString === 'READ') {
|
||||||
|
$maskInt += ACL::MASK_READ + ACL::MASK_EXECUTE;
|
||||||
|
} elseif ($maskString === 'CHANGE') {
|
||||||
|
$maskInt += ACL::MASK_READ + ACL::MASK_EXECUTE + ACL::MASK_WRITE + ACL::MASK_DELETE;
|
||||||
|
} elseif ($maskString === 'FULL') {
|
||||||
|
$maskInt += ACL::MASK_READ + ACL::MASK_EXECUTE + ACL::MASK_WRITE + ACL::MASK_DELETE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($acls[$user])) {
|
||||||
|
$existing = $acls[$user];
|
||||||
|
$maskInt += $existing->getMask();
|
||||||
|
}
|
||||||
|
$acls[$user] = new ACL($type, $flagsInt, $maskInt);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $acls;
|
||||||
|
}
|
||||||
|
|
||||||
public function __destruct() {
|
public function __destruct() {
|
||||||
unset($this->connection);
|
unset($this->connection);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -516,7 +516,7 @@ abstract class AbstractShareTest extends TestCase {
|
||||||
}
|
}
|
||||||
$this->assertTrue($dirEntry->isDirectory());
|
$this->assertTrue($dirEntry->isDirectory());
|
||||||
$this->assertFalse($dirEntry->isReadOnly());
|
$this->assertFalse($dirEntry->isReadOnly());
|
||||||
$this->assertFalse($dirEntry->isReadOnly());
|
$this->assertFalse($dirEntry->isHidden());
|
||||||
|
|
||||||
if ($dir[0]->getName() === 'file.txt') {
|
if ($dir[0]->getName() === 'file.txt') {
|
||||||
$fileEntry = $dir[0];
|
$fileEntry = $dir[0];
|
||||||
|
|
@ -525,7 +525,7 @@ abstract class AbstractShareTest extends TestCase {
|
||||||
}
|
}
|
||||||
$this->assertFalse($fileEntry->isDirectory());
|
$this->assertFalse($fileEntry->isDirectory());
|
||||||
$this->assertFalse($fileEntry->isReadOnly());
|
$this->assertFalse($fileEntry->isReadOnly());
|
||||||
$this->assertFalse($fileEntry->isReadOnly());
|
$this->assertFalse($fileEntry->isHidden());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ class ParserTest extends \PHPUnit_Framework_TestCase {
|
||||||
['RAH', IFileInfo::MODE_READONLY | IFileInfo::MODE_ARCHIVE | IFileInfo::MODE_HIDDEN]
|
['RAH', IFileInfo::MODE_READONLY | IFileInfo::MODE_ARCHIVE | IFileInfo::MODE_HIDDEN]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dataProvider modeProvider
|
* @dataProvider modeProvider
|
||||||
*/
|
*/
|
||||||
|
|
@ -108,7 +109,10 @@ class ParserTest extends \PHPUnit_Framework_TestCase {
|
||||||
'c.pdf',
|
'c.pdf',
|
||||||
29634,
|
29634,
|
||||||
strtotime('12 Oct 2013 19:05:58 CEST'),
|
strtotime('12 Oct 2013 19:05:58 CEST'),
|
||||||
IFileInfo::MODE_NORMAL
|
IFileInfo::MODE_NORMAL,
|
||||||
|
function () {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
@ -120,6 +124,8 @@ class ParserTest extends \PHPUnit_Framework_TestCase {
|
||||||
*/
|
*/
|
||||||
public function testDir($output, $dir) {
|
public function testDir($output, $dir) {
|
||||||
$parser = new \Icewind\SMB\Wrapped\Parser('CEST');
|
$parser = new \Icewind\SMB\Wrapped\Parser('CEST');
|
||||||
$this->assertEquals($dir, $parser->parseDir($output, ''));
|
$this->assertEquals($dir, $parser->parseDir($output, '', function () {
|
||||||
|
return [];
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue