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)
|
||||
{
|
||||
if (str_ends_with($this->webdir, '=')) {
|
||||
$url = $this->webdir . rawurlencode($item['local']);
|
||||
$webdir = $this->webdir;
|
||||
|
||||
// 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 {
|
||||
$url = $this->webdir . $item['local'];
|
||||
$url = $webdir . $item['local'];
|
||||
}
|
||||
|
||||
if ($cachbuster) {
|
||||
|
||||
5
Path.php
5
Path.php
@@ -49,11 +49,6 @@ class Path
|
||||
$alias = static::cleanPath($line);
|
||||
$paths[$lastRoot]['alias'] = $alias;
|
||||
$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 {
|
||||
// this is a new path
|
||||
$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.
|
||||
|
||||
|
||||
## Important security note
|
||||
## Note on security
|
||||
|
||||
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
|
||||
user.
|
||||
|
||||
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”.
|
||||
|
||||
and enforces access via per-page ACL: the requester must have at least read
|
||||
access to the wiki page that rendered the link.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -60,33 +56,25 @@ luxtools is configured via its dedicated admin page:
|
||||
|
||||
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**
|
||||
Allowed base filesystem roots (one per line). Each root can be followed by:
|
||||
- `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:
|
||||
|
||||
```
|
||||
/srv/share/Datascape/
|
||||
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=...`
|
||||
|
||||
Note: if you configure a `W>` web URL to an external file server, that server
|
||||
must enforce access itself; DokuWiki ACLs and `access_allow` only apply to
|
||||
`file.php`.
|
||||
The generated URLs also include the current wiki page id (`id=...`) so
|
||||
`file.php` can enforce ACLs for the host page.
|
||||
|
||||
|
||||
|
||||
- **scratchpad_paths**
|
||||
Scratchpad file map (one file path per line, followed by an `A>` alias line).
|
||||
|
||||
@@ -75,11 +75,6 @@ class ScratchpadMap
|
||||
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)
|
||||
$filePath = Path::cleanPath($line, false);
|
||||
if ($filePath === '' || str_ends_with($filePath, '/')) {
|
||||
|
||||
@@ -27,7 +27,6 @@ C:\\xampp\\htdocs\\wiki\\
|
||||
/linux/file/path/
|
||||
/linux/another/path/../..//another/blargh/../path
|
||||
A> alias
|
||||
W> webfoo
|
||||
EOT
|
||||
);
|
||||
}
|
||||
@@ -53,12 +52,12 @@ EOT
|
||||
'/linux/another/path/' => [
|
||||
'root' => '/linux/another/path/',
|
||||
'alias' => 'alias/',
|
||||
'web' => 'webfoo',
|
||||
'web' => '/lib/plugins/luxtools/file.php?root=%2Flinux%2Fanother%2Fpath%2F&file=',
|
||||
],
|
||||
'alias/' => [
|
||||
'root' => '/linux/another/path/',
|
||||
'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
|
||||
A> notes
|
||||
W> ignored
|
||||
|
||||
\\\\server\\share\\pads\\team.txt
|
||||
A> team
|
||||
|
||||
@@ -29,7 +29,8 @@ class plugin_luxtools_test extends DokuWikiTest
|
||||
parent::setUp();
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@@ -10,7 +10,6 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin
|
||||
{
|
||||
/** @var string[] */
|
||||
protected $configKeys = [
|
||||
'access_allow',
|
||||
'paths',
|
||||
'scratchpad_paths',
|
||||
'allow_in_comments',
|
||||
@@ -49,10 +48,6 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin
|
||||
}
|
||||
|
||||
$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
|
||||
$paths = $INPUT->str('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 '<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 = (string)$this->getConf('paths');
|
||||
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>...}}.
|
||||
$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');
|
||||
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'));
|
||||
$path = $INPUT->str('root') . $INPUT->str('file');
|
||||
|
||||
// Enforce access before doing any filesystem work.
|
||||
luxtools_require_access((string)$syntax->getConf('access_allow'));
|
||||
// Require the user to be able to read the page that rendered the link.
|
||||
$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.
|
||||
|
||||
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_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['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.';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?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['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';
|
||||
|
||||
Reference in New Issue
Block a user