Integrate Dokuwiki ACL for file endpoint
This commit is contained in:
33
Output.php
33
Output.php
@@ -562,10 +562,37 @@ class Output
|
|||||||
*/
|
*/
|
||||||
protected function itemWebUrl($item, $cachbuster = false)
|
protected function itemWebUrl($item, $cachbuster = false)
|
||||||
{
|
{
|
||||||
if (str_ends_with($this->webdir, '=')) {
|
$webdir = $this->webdir;
|
||||||
$url = $this->webdir . rawurlencode($item['local']);
|
|
||||||
|
// When using the built-in file-serving endpoint, include the current page id
|
||||||
|
// so file.php can enforce DokuWiki ACLs for that page.
|
||||||
|
if (
|
||||||
|
is_string($webdir)
|
||||||
|
&& $webdir !== ''
|
||||||
|
&& strpos($webdir, 'lib/plugins/luxtools/file.php') !== false
|
||||||
|
&& strpos($webdir, 'id=') === false
|
||||||
|
) {
|
||||||
|
global $ID;
|
||||||
|
$pageId = isset($ID) ? (string)$ID : '';
|
||||||
|
if ($pageId !== '') {
|
||||||
|
if (function_exists('cleanID')) {
|
||||||
|
$pageId = (string)cleanID($pageId);
|
||||||
|
}
|
||||||
|
if ($pageId !== '') {
|
||||||
|
$encoded = rawurlencode($pageId);
|
||||||
|
if (strpos($webdir, '&file=') !== false) {
|
||||||
|
$webdir = str_replace('&file=', '&id=' . $encoded . '&file=', $webdir);
|
||||||
|
} elseif (strpos($webdir, '?file=') !== false) {
|
||||||
|
$webdir = str_replace('?file=', '?id=' . $encoded . '&file=', $webdir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_ends_with($webdir, '=')) {
|
||||||
|
$url = $webdir . rawurlencode($item['local']);
|
||||||
} else {
|
} else {
|
||||||
$url = $this->webdir . $item['local'];
|
$url = $webdir . $item['local'];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($cachbuster) {
|
if ($cachbuster) {
|
||||||
|
|||||||
5
Path.php
5
Path.php
@@ -49,11 +49,6 @@ class Path
|
|||||||
$alias = static::cleanPath($line);
|
$alias = static::cleanPath($line);
|
||||||
$paths[$lastRoot]['alias'] = $alias;
|
$paths[$lastRoot]['alias'] = $alias;
|
||||||
$paths[$alias] = &$paths[$lastRoot]; // alias references the original
|
$paths[$alias] = &$paths[$lastRoot]; // alias references the original
|
||||||
} elseif (str_starts_with($line, 'W>')) {
|
|
||||||
// this is a web path for the last read root
|
|
||||||
$line = trim(substr($line, 2));
|
|
||||||
if (!isset($paths[$lastRoot])) continue; // no last path, no web path
|
|
||||||
$paths[$lastRoot]['web'] = $line;
|
|
||||||
} else {
|
} else {
|
||||||
// this is a new path
|
// this is a new path
|
||||||
$line = static::cleanPath($line);
|
$line = static::cleanPath($line);
|
||||||
|
|||||||
28
README.md
28
README.md
@@ -31,15 +31,11 @@ It also ships a small file-serving endpoint (`lib/plugins/luxtools/file.php`) us
|
|||||||
to deliver files and generate cached thumbnails.
|
to deliver files and generate cached thumbnails.
|
||||||
|
|
||||||
|
|
||||||
## Important security note
|
## Note on security
|
||||||
|
|
||||||
The file-serving endpoint (`lib/plugins/luxtools/file.php`) runs inside DokuWiki
|
The file-serving endpoint (`lib/plugins/luxtools/file.php`) runs inside DokuWiki
|
||||||
and can enforce a simple access restriction based on the currently logged-in
|
and enforces access via per-page ACL: the requester must have at least read
|
||||||
user.
|
access to the wiki page that rendered the link.
|
||||||
|
|
||||||
This is intentionally *not* full per-page ACL integration; it is a pragmatic
|
|
||||||
allowlist to avoid “anyone with a guessed URL can fetch the file”.
|
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -60,33 +56,25 @@ luxtools is configured via its dedicated admin page:
|
|||||||
|
|
||||||
Key settings:
|
Key settings:
|
||||||
|
|
||||||
- **access_allow**
|
|
||||||
Allowed users/groups for the file-serving endpoint.
|
|
||||||
- Entries can be separated by newlines, commas, or whitespace.
|
|
||||||
- Use `@group` to allow a whole group.
|
|
||||||
- Leave empty to allow any authenticated (logged-in) user.
|
|
||||||
- Anonymous users are always denied.
|
|
||||||
|
|
||||||
- **paths**
|
- **paths**
|
||||||
Allowed base filesystem roots (one per line). Each root can be followed by:
|
Allowed base filesystem roots (one per line). Each root can be followed by:
|
||||||
- `A> /Alias/` (optional) alias used in wiki syntax and open links
|
- `A> /Alias/` (optional) alias used in wiki syntax and open links
|
||||||
- `W> https://...` (optional) web base URL used for links instead of `file.php`
|
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```
|
```
|
||||||
/srv/share/Datascape/
|
/srv/share/Datascape/
|
||||||
A> /Scape/
|
A> /Scape/
|
||||||
W> https://files.example.example/Datascape/
|
|
||||||
```
|
```
|
||||||
|
|
||||||
If no `W>` line is configured, luxtools links will use the plugin endpoint:
|
luxtools links use the plugin endpoint:
|
||||||
|
|
||||||
`lib/plugins/luxtools/file.php?root=...&file=...`
|
`lib/plugins/luxtools/file.php?root=...&file=...`
|
||||||
|
|
||||||
Note: if you configure a `W>` web URL to an external file server, that server
|
The generated URLs also include the current wiki page id (`id=...`) so
|
||||||
must enforce access itself; DokuWiki ACLs and `access_allow` only apply to
|
`file.php` can enforce ACLs for the host page.
|
||||||
`file.php`.
|
|
||||||
|
|
||||||
|
|
||||||
- **scratchpad_paths**
|
- **scratchpad_paths**
|
||||||
Scratchpad file map (one file path per line, followed by an `A>` alias line).
|
Scratchpad file map (one file path per line, followed by an `A>` alias line).
|
||||||
|
|||||||
@@ -75,11 +75,6 @@ class ScratchpadMap
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore W> lines for compatibility with the Path config style
|
|
||||||
if (str_starts_with($line, 'W>')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Treat as file path (no trailing slash enforced)
|
// Treat as file path (no trailing slash enforced)
|
||||||
$filePath = Path::cleanPath($line, false);
|
$filePath = Path::cleanPath($line, false);
|
||||||
if ($filePath === '' || str_ends_with($filePath, '/')) {
|
if ($filePath === '' || str_ends_with($filePath, '/')) {
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ C:\\xampp\\htdocs\\wiki\\
|
|||||||
/linux/file/path/
|
/linux/file/path/
|
||||||
/linux/another/path/../..//another/blargh/../path
|
/linux/another/path/../..//another/blargh/../path
|
||||||
A> alias
|
A> alias
|
||||||
W> webfoo
|
|
||||||
EOT
|
EOT
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -53,12 +52,12 @@ EOT
|
|||||||
'/linux/another/path/' => [
|
'/linux/another/path/' => [
|
||||||
'root' => '/linux/another/path/',
|
'root' => '/linux/another/path/',
|
||||||
'alias' => 'alias/',
|
'alias' => 'alias/',
|
||||||
'web' => 'webfoo',
|
'web' => '/lib/plugins/luxtools/file.php?root=%2Flinux%2Fanother%2Fpath%2F&file=',
|
||||||
],
|
],
|
||||||
'alias/' => [
|
'alias/' => [
|
||||||
'root' => '/linux/another/path/',
|
'root' => '/linux/another/path/',
|
||||||
'alias' => 'alias/',
|
'alias' => 'alias/',
|
||||||
'web' => 'webfoo',
|
'web' => '/lib/plugins/luxtools/file.php?root=%2Flinux%2Fanother%2Fpath%2F&file=',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ A> start
|
|||||||
|
|
||||||
C:\\pads\\notes.md
|
C:\\pads\\notes.md
|
||||||
A> notes
|
A> notes
|
||||||
W> ignored
|
|
||||||
|
|
||||||
\\\\server\\share\\pads\\team.txt
|
\\\\server\\share\\pads\\team.txt
|
||||||
A> team
|
A> team
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ class plugin_luxtools_test extends DokuWikiTest
|
|||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
// Setup config so that access to the TMP directory will be allowed
|
// Setup config so that access to the TMP directory will be allowed
|
||||||
$conf ['plugin']['luxtools']['paths'] = TMP_DIR . '/filelistdata/' . "\n" . 'A> /Scape' . "\n" . 'W> http://localhost/';
|
// Use the built-in file.php endpoint.
|
||||||
|
$conf ['plugin']['luxtools']['paths'] = TMP_DIR . '/filelistdata/' . "\n" . 'A> /Scape';
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,6 +245,27 @@ class plugin_luxtools_test extends DokuWikiTest
|
|||||||
$this->assertStringContainsString('height="150"', $xhtml);
|
$this->assertStringContainsString('height="150"', $xhtml);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the built-in file endpoint includes the host page id so file.php can
|
||||||
|
* enforce per-page ACL.
|
||||||
|
*/
|
||||||
|
public function test_file_links_include_page_id_for_acl()
|
||||||
|
{
|
||||||
|
global $ID;
|
||||||
|
$ID = 'luxtools:acltest';
|
||||||
|
|
||||||
|
$instructions = p_get_instructions('{{files>' . TMP_DIR . '/filelistdata/*&style=list&direct=1}}');
|
||||||
|
$xhtml = p_render('xhtml', $instructions, $info);
|
||||||
|
|
||||||
|
$doc = new Document();
|
||||||
|
$doc->html($xhtml);
|
||||||
|
|
||||||
|
$href = (string)$doc->find('div.luxtools-plugin a')->first()->attr('href');
|
||||||
|
$this->assertNotSame('', $href);
|
||||||
|
$this->assertStringContainsString('lib/plugins/luxtools/file.php', $href);
|
||||||
|
$this->assertStringContainsString('id=luxtools%3Aacltest', $href);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function checks that the open syntax renders an inline link.
|
* This function checks that the open syntax renders an inline link.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin
|
|||||||
{
|
{
|
||||||
/** @var string[] */
|
/** @var string[] */
|
||||||
protected $configKeys = [
|
protected $configKeys = [
|
||||||
'access_allow',
|
|
||||||
'paths',
|
'paths',
|
||||||
'scratchpad_paths',
|
'scratchpad_paths',
|
||||||
'allow_in_comments',
|
'allow_in_comments',
|
||||||
@@ -49,10 +48,6 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin
|
|||||||
}
|
}
|
||||||
|
|
||||||
$newConf = [];
|
$newConf = [];
|
||||||
$accessAllow = $INPUT->str('access_allow');
|
|
||||||
$accessAllow = str_replace(["\r\n", "\r"], "\n", $accessAllow);
|
|
||||||
$newConf['access_allow'] = $accessAllow;
|
|
||||||
|
|
||||||
// Normalize newlines to "\n" for consistent parsing
|
// Normalize newlines to "\n" for consistent parsing
|
||||||
$paths = $INPUT->str('paths');
|
$paths = $INPUT->str('paths');
|
||||||
$paths = str_replace(["\r\n", "\r"], "\n", $paths);
|
$paths = str_replace(["\r\n", "\r"], "\n", $paths);
|
||||||
@@ -93,12 +88,6 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin
|
|||||||
echo '<fieldset>';
|
echo '<fieldset>';
|
||||||
echo '<legend>' . hsc($this->getLang('legend')) . '</legend>';
|
echo '<legend>' . hsc($this->getLang('legend')) . '</legend>';
|
||||||
|
|
||||||
// access_allow: multiline (users/groups)
|
|
||||||
$accessAllow = (string)$this->getConf('access_allow');
|
|
||||||
echo '<label class="block"><span>' . hsc($this->getLang('access_allow')) . '</span><br />';
|
|
||||||
echo '<textarea name="access_allow" rows="3" cols="80" class="edit">' . hsc($accessAllow) . '</textarea>';
|
|
||||||
echo '</label><br />';
|
|
||||||
|
|
||||||
// paths: multiline textarea
|
// paths: multiline textarea
|
||||||
$paths = (string)$this->getConf('paths');
|
$paths = (string)$this->getConf('paths');
|
||||||
echo '<label class="block"><span>' . hsc($this->getLang('paths')) . '</span><br />';
|
echo '<label class="block"><span>' . hsc($this->getLang('paths')) . '</span><br />';
|
||||||
|
|||||||
@@ -19,8 +19,3 @@ $conf['gallery_thumb_scale'] = 1;
|
|||||||
|
|
||||||
// Local client service used by {{open>...}}.
|
// Local client service used by {{open>...}}.
|
||||||
$conf['open_service_url'] = 'http://127.0.0.1:8765';
|
$conf['open_service_url'] = 'http://127.0.0.1:8765';
|
||||||
|
|
||||||
// Access allowlist for the file-serving endpoint (file.php).
|
|
||||||
// Empty means: any authenticated user may access. Anonymous users are denied.
|
|
||||||
// Use '@group' to allow a whole group.
|
|
||||||
$conf['access_allow'] = '';
|
|
||||||
|
|||||||
107
file.php
107
file.php
@@ -13,90 +13,35 @@ global $INPUT;
|
|||||||
$syntax = plugin_load('syntax', 'luxtools');
|
$syntax = plugin_load('syntax', 'luxtools');
|
||||||
if (!$syntax) die('plugin disabled?');
|
if (!$syntax) die('plugin disabled?');
|
||||||
|
|
||||||
/**
|
|
||||||
* Enforce a simple allowlist based access check.
|
|
||||||
*
|
|
||||||
* The allowlist supports entries separated by commas, whitespace or newlines.
|
|
||||||
* - "alice" allows a specific user
|
|
||||||
* - "@admins" allows a DokuWiki group
|
|
||||||
*
|
|
||||||
* If the allowlist is empty, any authenticated user is allowed.
|
|
||||||
* Anonymous access is always denied.
|
|
||||||
*
|
|
||||||
* @param string $allowListRaw
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
function luxtools_require_access(string $allowListRaw): void
|
|
||||||
{
|
|
||||||
global $INPUT, $USERINFO;
|
|
||||||
|
|
||||||
// We need a logged-in user for any access.
|
|
||||||
$user = '';
|
|
||||||
try {
|
|
||||||
$user = (string)$INPUT->server->str('REMOTE_USER');
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
$user = (string)($_SERVER['REMOTE_USER'] ?? '');
|
|
||||||
}
|
|
||||||
$user = trim($user);
|
|
||||||
|
|
||||||
if ($user === '') {
|
|
||||||
header('Content-Type: text/plain; charset=utf-8');
|
|
||||||
http_status(403);
|
|
||||||
echo 'forbidden';
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$allowListRaw = trim($allowListRaw);
|
|
||||||
if ($allowListRaw === '') {
|
|
||||||
return; // any authenticated user
|
|
||||||
}
|
|
||||||
|
|
||||||
$tokens = preg_split('/[\s,;]+/', $allowListRaw, -1, PREG_SPLIT_NO_EMPTY);
|
|
||||||
if (!is_array($tokens) || $tokens === []) {
|
|
||||||
// If configured but empty after parsing, be conservative.
|
|
||||||
header('Content-Type: text/plain; charset=utf-8');
|
|
||||||
http_status(403);
|
|
||||||
echo 'forbidden';
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$groups = [];
|
|
||||||
if (is_array($USERINFO) && isset($USERINFO['grps']) && is_array($USERINFO['grps'])) {
|
|
||||||
$groups = $USERINFO['grps'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$allowed = false;
|
|
||||||
foreach ($tokens as $t) {
|
|
||||||
$t = trim((string)$t);
|
|
||||||
if ($t === '') continue;
|
|
||||||
|
|
||||||
if ($t[0] === '@') {
|
|
||||||
$g = trim(substr($t, 1));
|
|
||||||
if ($g !== '' && in_array($g, $groups, true)) {
|
|
||||||
$allowed = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if ($t === $user) {
|
|
||||||
$allowed = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$allowed) {
|
|
||||||
header('Content-Type: text/plain; charset=utf-8');
|
|
||||||
http_status(403);
|
|
||||||
echo 'forbidden';
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$pathUtil = new Path($syntax->getConf('paths'));
|
$pathUtil = new Path($syntax->getConf('paths'));
|
||||||
$path = $INPUT->str('root') . $INPUT->str('file');
|
$path = $INPUT->str('root') . $INPUT->str('file');
|
||||||
|
|
||||||
// Enforce access before doing any filesystem work.
|
// Require the user to be able to read the page that rendered the link.
|
||||||
luxtools_require_access((string)$syntax->getConf('access_allow'));
|
$pageId = (string)$INPUT->str('id');
|
||||||
|
if (function_exists('cleanID')) {
|
||||||
|
$pageId = (string)cleanID($pageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($pageId === '') {
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
http_status(403);
|
||||||
|
echo 'forbidden';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('auth_quickaclcheck') || !defined('AUTH_READ')) {
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
http_status(403);
|
||||||
|
echo 'forbidden';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auth_quickaclcheck($pageId) < AUTH_READ) {
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
http_status(403);
|
||||||
|
echo 'forbidden';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a file to the client with basic caching headers.
|
* Send a file to the client with basic caching headers.
|
||||||
|
|||||||
BIN
lang/de/lang.php
BIN
lang/de/lang.php
Binary file not shown.
Binary file not shown.
@@ -25,9 +25,8 @@ $lang['saved'] = 'Settings saved.';
|
|||||||
$lang['err_save'] = 'Could not save settings. Please check write permissions for conf/local.php.';
|
$lang['err_save'] = 'Could not save settings. Please check write permissions for conf/local.php.';
|
||||||
$lang['err_security'] = 'Security token mismatch. Please retry.';
|
$lang['err_security'] = 'Security token mismatch. Please retry.';
|
||||||
|
|
||||||
$lang['access_allow'] = 'Allowed users/groups for file access (one per line or comma-separated). Use @group for groups. Leave empty to allow any logged-in user.';
|
|
||||||
|
|
||||||
$lang['paths'] = 'Allowed base paths (one per line). Optional: follow a path with A> alias and W> web URL.';
|
$lang['paths'] = 'Allowed base paths (one per line). Optional: follow a path with A> alias.';
|
||||||
$lang['scratchpad_paths'] = 'Scratchpad files (one per line). Each file path must include the extension. Use a following A> line to set the pad name used in the wiki.';
|
$lang['scratchpad_paths'] = 'Scratchpad files (one per line). Each file path must include the extension. Use a following A> line to set the pad name used in the wiki.';
|
||||||
$lang['allow_in_comments'] = 'Whether to allow luxtools syntax (files/images/directory/scratchpad) to be used in comments.';
|
$lang['allow_in_comments'] = 'Whether to allow luxtools syntax (files/images/directory/scratchpad) to be used in comments.';
|
||||||
$lang['defaults'] = 'Default options. Use the same syntax as in inline configuration.';
|
$lang['defaults'] = 'Default options. Use the same syntax as in inline configuration.';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
$lang['paths'] = 'Allowed base paths (one per line). Optional: follow a path with A> alias and W> web URL.';
|
$lang['paths'] = 'Allowed base paths (one per line). Optional: follow a path with A> alias.';
|
||||||
$lang['scratchpad_paths'] = 'Scratchpad files (one per line). Each file path must include the extension; use a following A> line to set the pad name used in the wiki.';
|
$lang['scratchpad_paths'] = 'Scratchpad files (one per line). Each file path must include the extension; use a following A> line to set the pad name used in the wiki.';
|
||||||
$lang['allow_in_comments'] = 'Whether to allow luxtools syntax (files/images/directory/scratchpad) to be used in comments.';
|
$lang['allow_in_comments'] = 'Whether to allow luxtools syntax (files/images/directory/scratchpad) to be used in comments.';
|
||||||
$lang['defaults'] = 'Default options. Use the same syntax as in inline configuration';
|
$lang['defaults'] = 'Default options. Use the same syntax as in inline configuration';
|
||||||
|
|||||||
Reference in New Issue
Block a user