Access control for file serving
Authenticated user only for now
This commit is contained in:
21
README.md
21
README.md
@@ -33,12 +33,12 @@ to deliver files and generate cached thumbnails.
|
|||||||
|
|
||||||
## Important security note
|
## Important security note
|
||||||
|
|
||||||
The file-serving endpoint is designed for convenience and caching and does NOT
|
The file-serving endpoint (`lib/plugins/luxtools/file.php`) runs inside DokuWiki
|
||||||
apply DokuWiki ACLs. Anything reachable through a configured root may be
|
and can enforce a simple access restriction based on the currently logged-in
|
||||||
accessible to anyone who can access your wiki and guess/copy the generated URLs.
|
user.
|
||||||
|
|
||||||
Only configure roots that contain non-sensitive data, or protect access on the
|
This is intentionally *not* full per-page ACL integration; it is a pragmatic
|
||||||
webserver/network level.
|
allowlist to avoid “anyone with a guessed URL can fetch the file”.
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
@@ -60,6 +60,13 @@ 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
|
||||||
@@ -77,6 +84,10 @@ Key settings:
|
|||||||
|
|
||||||
`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
|
||||||
|
must enforce access itself; DokuWiki ACLs and `access_allow` only apply to
|
||||||
|
`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).
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ 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',
|
||||||
@@ -48,6 +49,10 @@ 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);
|
||||||
@@ -88,6 +93,12 @@ 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,3 +19,8 @@ $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'] = '';
|
||||||
|
|||||||
86
file.php
86
file.php
@@ -5,7 +5,6 @@
|
|||||||
use dokuwiki\plugin\luxtools\Path;
|
use dokuwiki\plugin\luxtools\Path;
|
||||||
|
|
||||||
if (!defined('DOKU_INC')) define('DOKU_INC', __DIR__ . '/../../../');
|
if (!defined('DOKU_INC')) define('DOKU_INC', __DIR__ . '/../../../');
|
||||||
if (!defined('NOSESSION')) define('NOSESSION', true); // we do not use a session or authentication here (better caching)
|
|
||||||
if (!defined('DOKU_DISABLE_GZIP_OUTPUT')) define('DOKU_DISABLE_GZIP_OUTPUT', 1); // we gzip ourself here
|
if (!defined('DOKU_DISABLE_GZIP_OUTPUT')) define('DOKU_DISABLE_GZIP_OUTPUT', 1); // we gzip ourself here
|
||||||
require_once(DOKU_INC . 'inc/init.php');
|
require_once(DOKU_INC . 'inc/init.php');
|
||||||
|
|
||||||
@@ -14,9 +13,91 @@ 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.
|
||||||
|
luxtools_require_access((string)$syntax->getConf('access_allow'));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a file to the client with basic caching headers.
|
* Send a file to the client with basic caching headers.
|
||||||
*
|
*
|
||||||
@@ -47,7 +128,8 @@ function luxtools_sendfile($path, $mime, $download = false, $downloadName = null
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($maxAge !== null) {
|
if ($maxAge !== null) {
|
||||||
header('Cache-Control: public, max-age=' . (int)$maxAge . ', immutable');
|
// Authentication may apply; keep caching private.
|
||||||
|
header('Cache-Control: private, max-age=' . (int)$maxAge . ', immutable');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Conditional request handling
|
// Conditional request handling
|
||||||
|
|||||||
BIN
lang/de/lang.php
BIN
lang/de/lang.php
Binary file not shown.
@@ -25,6 +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 and W> web URL.';
|
||||||
$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.';
|
||||||
|
|||||||
Reference in New Issue
Block a user