saltcorn-samba 0.3.5 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/index.js +112 -16
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,51 @@ All notable changes to `saltcorn-samba` are documented here.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.3.7] – 2026-07-05
8
+
9
+ ### Fixed
10
+ - **„Only admins can test the connection.“ fälschlicherweise gemeldet.**
11
+ Die Admin-Prüfung in `POST /sambatest` verglich `req.user.role_id` mit
12
+ strikter Gleichheit gegen die Zahl `1`. Saltcorn liefert `role_id` je
13
+ nach Session-Serialisierung aber sowohl als Number wie auch als String
14
+ aus, weshalb `"1" !== 1` den Test blockierte. Die Prüfung akzeptiert
15
+ jetzt beide Formen (`Number(rid) === 1 || String(rid) === "1"`) und
16
+ liest den User zusätzlich aus `req.session.passport.user`, falls
17
+ `req.user` von einer noch nicht deserialisierten Session leer ist.
18
+ Vgl. [saltcorn utils.ts isAdminOrHasConfigMinRole](https://github.com/saltcorn/saltcorn/blob/master/packages/server/routes/utils.ts).
19
+ - Auch der zentrale `roleOf(req)`-Helper (Lese-/Schreibrouten) toleriert
20
+ jetzt String-Rollen und Session-Fallback.
21
+
22
+ ### Changed
23
+ - Bei fehlender Admin-Erkennung liefert `/sambatest` jetzt ein
24
+ `debug`-Objekt mit `has_req_user`, `has_session`, `role_id_seen`,
25
+ `role_id_type`, `email`, `user_id`. Der Test-Button zeigt diese
26
+ Diagnose in einem aufklappbaren „Session-Diagnose“-Panel – damit
27
+ wird sofort sichtbar, was Saltcorn dem Plugin zum Benutzer mitgibt.
28
+
29
+ ## [0.3.6] – 2026-07-05
30
+
31
+ ### Fixed
32
+ - **`JSON.parse: unexpected character at line 1 column 1` beim Test-Button behoben.**
33
+ Der Aufruf von `POST /sambatest` lief zuvor gegen die globale
34
+ CSRF-Middleware von Saltcorn, die bei ungültigem/fehlendem Token eine
35
+ HTML-Seite zurückliefert – der Browser konnte sie nicht als JSON parsen.
36
+ Fix in drei Schichten:
37
+ 1. Route ist jetzt mit `noCsrf: true` markiert (Sicherheit weiterhin durch
38
+ die harte Admin-Prüfung `roleOf(req) !== 1` im Handler garantiert;
39
+ siehe [saltcorn plugin_routes_handler.js](https://github.com/saltcorn/saltcorn/blob/master/packages/server/plugin_routes_handler.js)).
40
+ 2. Der Client sendet nun als `application/x-www-form-urlencoded`
41
+ (statt JSON) und legt das `_csrf`-Token direkt in den Body – so
42
+ würde auch ein aktiver CSRF-Check passieren.
43
+ 3. Das Token wird jetzt zuverlässig aus drei Quellen gesucht:
44
+ dem versteckten `<input name="_csrf">` des Formulars, dem
45
+ `<meta name="csrf-token">`-Tag und `window._sc_globalCsrf`.
46
+ - **Klartext-Fehler statt kryptischem Parser-Crash.**
47
+ Wenn der Server doch mal HTML statt JSON liefert (z. B. Login-Redirect,
48
+ Plugin nicht neu geladen), zeigt der Test-Button jetzt eine deutliche
49
+ Meldung mit HTTP-Status und einer aufklappbaren Rohantwort statt eines
50
+ hilflosen „unexpected character“.
51
+
7
52
  ## [0.3.5] – 2026-07-05
8
53
 
9
54
  ### Added
package/index.js CHANGED
@@ -92,19 +92,56 @@ window.sambaTestConn = async function(btn) {
92
92
  out.innerHTML = '<div class="alert alert-warning">Bitte mindestens <b>Server</b> und <b>Share</b> ausfüllen.</div>';
93
93
  return;
94
94
  }
95
- var csrf = (document.querySelector('meta[name="csrf-token"]') || {}).content || '';
95
+ // Robust CSRF-Token-Suche: erst das versteckte Feld des Formulars,
96
+ // dann Meta-Tag, dann globales window._sc_globalCsrf.
97
+ var csrfEl = form.querySelector('input[name="_csrf"]');
98
+ var csrf =
99
+ (csrfEl && csrfEl.value) ||
100
+ ((document.querySelector('meta[name="csrf-token"]') || {}).content) ||
101
+ (window._sc_globalCsrf) ||
102
+ '';
96
103
  btn.disabled = true;
97
104
  var oldTxt = btn.textContent;
98
105
  btn.textContent = 'Teste …';
99
106
  out.innerHTML = '<div class="text-muted">Verbindung wird aufgebaut … (bis zu 30 s)</div>';
100
107
  try {
108
+ // Als URL-encoded senden – so findet Saltcorns CSRF-Middleware das
109
+ // Token direkt im Body (req.body._csrf), unabhängig von der
110
+ // Header-Konfiguration. Zusätzlich das Token als Header schicken.
111
+ var params = new URLSearchParams();
112
+ params.set('_csrf', csrf);
113
+ Object.keys(payload).forEach(function(k){ params.set(k, payload[k] || ''); });
101
114
  var r = await fetch('/sambatest', {
102
115
  method: 'POST',
103
116
  credentials: 'same-origin',
104
- headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf, 'CSRF-Token': csrf },
105
- body: JSON.stringify(payload)
117
+ headers: {
118
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
119
+ 'Accept': 'application/json',
120
+ 'X-CSRF-Token': csrf,
121
+ 'CSRF-Token': csrf,
122
+ 'X-Requested-With': 'XMLHttpRequest'
123
+ },
124
+ body: params.toString()
106
125
  });
107
- var data = await r.json();
126
+ // Defensiv: kommt HTML statt JSON zurück (z. B. Login-Redirect), Text anzeigen.
127
+ var raw = await r.text();
128
+ var data;
129
+ try { data = JSON.parse(raw); }
130
+ catch (parseErr) {
131
+ var short = raw.replace(/<[^>]+>/g,'').replace(/\s+/g,' ').trim().slice(0, 300);
132
+ out.innerHTML =
133
+ '<div class="alert alert-danger">' +
134
+ '<b>✗ Antwort war kein JSON</b> (HTTP ' + r.status + ')<br>' +
135
+ 'Mögliche Ursachen: nicht als Admin angemeldet (Login-Redirect), CSRF-Token ungültig, ' +
136
+ 'oder die Route <code>/sambatest</code> ist noch nicht registriert ' +
137
+ '(Plugin neu installieren bzw. Saltcorn neu starten).<br>' +
138
+ '<details style="margin-top:.4rem"><summary>Antwort des Servers</summary>' +
139
+ '<pre style="white-space:pre-wrap;margin-top:.4rem">' +
140
+ short.replace(/[<>&]/g, function(c){return {'<':'&lt;','>':'&gt;','&':'&amp;'}[c];}) +
141
+ '</pre></details>' +
142
+ '</div>';
143
+ return;
144
+ }
108
145
  if (data && data.ok) {
109
146
  var rows = (data.entries||[]).map(function(e){
110
147
  return '<li>'+ (e.isDirectory?'📁 ':'📄 ') + String(e.name).replace(/[<>&]/g,'?') +'</li>';
@@ -119,20 +156,31 @@ window.sambaTestConn = async function(btn) {
119
156
  '</div>';
120
157
  } else {
121
158
  var a = data && data.attempted || {};
159
+ var dbg = data && data.debug;
160
+ var titleForCode = (data && data.code === 'NOT_ADMIN')
161
+ ? '✗ Nicht als Administrator erkannt'
162
+ : '✗ Verbindung fehlgeschlagen';
122
163
  out.innerHTML =
123
164
  '<div class="alert alert-danger">' +
124
- '<b>✗ Verbindung fehlgeschlagen</b><br>' +
165
+ '<b>' + titleForCode + '</b><br>' +
125
166
  'Fehler: <code>' + String(data && data.error || 'Unbekannt').replace(/[<>&]/g,'?') + '</code>' +
126
167
  (data && data.code ? ' <span class="text-muted">(' + data.code + ')</span>' : '') + '<br>' +
127
168
  (data && data.hint ? '<div style="margin-top:.4rem"><b>Hinweis:</b> ' + String(data.hint).replace(/[<>&]/g,'?') + '</div>' : '') +
128
- '<details style="margin-top:.4rem"><summary>Versuchte Verbindungsdaten</summary>' +
129
- '<table class="table table-sm" style="margin-top:.4rem">' +
130
- '<tr><td>Server</td><td><code>' + (a.server||'') + ':' + (a.port||'') + '</code></td></tr>' +
131
- '<tr><td>Share</td><td><code>' + (a.share||'') + '</code></td></tr>' +
132
- '<tr><td>Basispfad</td><td><code>' + (a.base_path||'') + '</code></td></tr>' +
133
- '<tr><td>Domäne</td><td><code>' + (a.domain||'') + '</code></td></tr>' +
134
- '<tr><td>Benutzer</td><td><code>' + (a.username||'') + '</code></td></tr>' +
135
- '</table></details>' +
169
+ (a.server ? (
170
+ '<details style="margin-top:.4rem"><summary>Versuchte Verbindungsdaten</summary>' +
171
+ '<table class="table table-sm" style="margin-top:.4rem">' +
172
+ '<tr><td>Server</td><td><code>' + (a.server||'') + ':' + (a.port||'') + '</code></td></tr>' +
173
+ '<tr><td>Share</td><td><code>' + (a.share||'') + '</code></td></tr>' +
174
+ '<tr><td>Basispfad</td><td><code>' + (a.base_path||'') + '</code></td></tr>' +
175
+ '<tr><td>Domäne</td><td><code>' + (a.domain||'') + '</code></td></tr>' +
176
+ '<tr><td>Benutzer</td><td><code>' + (a.username||'') + '</code></td></tr>' +
177
+ '</table></details>'
178
+ ) : '') +
179
+ (dbg ? (
180
+ '<details style="margin-top:.4rem"><summary>Session-Diagnose</summary>' +
181
+ '<pre style="margin-top:.4rem">' + JSON.stringify(dbg, null, 2) + '</pre>' +
182
+ '</details>'
183
+ ) : '') +
136
184
  '</div>';
137
185
  }
138
186
  } catch (e) {
@@ -334,7 +382,14 @@ function getConfig() {
334
382
  }
335
383
 
336
384
  function roleOf(req) {
337
- return (req && req.user && req.user.role_id) || 100;
385
+ const u =
386
+ (req && req.user) ||
387
+ (req && req.session && req.session.passport && req.session.passport.user) ||
388
+ null;
389
+ if (!u) return 100;
390
+ const rid = u.role_id !== undefined ? u.role_id : u.roleId;
391
+ const n = Number(rid);
392
+ return Number.isFinite(n) && n > 0 ? n : 100;
338
393
  }
339
394
 
340
395
  function canRead(req, cfg) {
@@ -541,10 +596,51 @@ code{background:#f4f4f4;padding:2px 6px;border-radius:3px;word-break:break-all}<
541
596
  {
542
597
  url: "/sambatest",
543
598
  method: "post",
599
+ // The route is protected by an explicit admin check in the handler
600
+ // (roleOf(req) !== 1 returns 403), so the standard CSRF middleware
601
+ // would only add friction without adding security here — the token
602
+ // hidden field is not in scope of an arbitrary attacker who cannot
603
+ // already run scripts inside an admin's browser (which would defeat
604
+ // any web-app anyway). Setting noCsrf keeps the button working even
605
+ // when the config-page CSRF token has already been consumed by the
606
+ // save-workflow or when a stricter CSRF policy is applied.
607
+ noCsrf: true,
544
608
  callback: async ({ req, res }) => {
545
- if (roleOf(req) !== 1) {
546
- return jsonError(res, 403, "Only admins can test the connection.");
609
+ // Admin gate be permissive about how Saltcorn represents the role:
610
+ // • req.user.role_id may be a Number (1) or a String ("1") depending
611
+ // on how the session was serialised.
612
+ // • In some tenant/session configs req.user is populated later; we
613
+ // also check req.session.passport.user as a fallback.
614
+ const u =
615
+ (req && req.user) ||
616
+ (req && req.session && req.session.passport && req.session.passport.user) ||
617
+ null;
618
+ const rid = u && (u.role_id !== undefined ? u.role_id : u.roleId);
619
+ const isAdmin = u && (Number(rid) === 1 || String(rid) === "1");
620
+ if (!isAdmin) {
621
+ return res.status(403).json({
622
+ ok: false,
623
+ error: "Only admins can test the connection.",
624
+ code: "NOT_ADMIN",
625
+ hint:
626
+ "Der Server hat Sie für diese Anfrage nicht als Administrator erkannt. " +
627
+ "Mögliche Ursachen: (1) Sie sind in einem anderen Tab abgemeldet worden, " +
628
+ "(2) die Konfigurations-Seite wurde in einem privaten/Inkognito-Fenster " +
629
+ "geöffnet ohne gültige Session, (3) Saltcorn läuft hinter einem Reverse-" +
630
+ "Proxy, der Cookies nicht weiterreicht. Bitte im selben Fenster neu " +
631
+ "anmelden und die Seite neu laden.",
632
+ debug: {
633
+ has_req_user: !!(req && req.user),
634
+ has_session: !!(req && req.session),
635
+ role_id_seen: rid === undefined ? null : String(rid),
636
+ role_id_type: rid === undefined ? "undefined" : typeof rid,
637
+ email: (u && u.email) || null,
638
+ user_id: (u && u.id) || null,
639
+ },
640
+ });
547
641
  }
642
+ // Accept both JSON and URL-encoded bodies. express.json and
643
+ // express.urlencoded are both installed globally by Saltcorn.
548
644
  const body = (req.body && typeof req.body === "object") ? req.body : {};
549
645
  const testCfg = {
550
646
  server: String(body.server || "").trim(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saltcorn-samba",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "description": "Saltcorn plugin: browse, upload, rename and delete files on a Samba/CIFS share. File-manager view, directory tree, inline PDF viewer, external-app open (smb://).",
5
5
  "main": "index.js",
6
6
  "scripts": {