saltcorn-samba 0.3.7 → 0.3.9
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.
- package/CHANGELOG.md +92 -0
- package/index.js +124 -32
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,98 @@ 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.9] – 2026-07-05
|
|
8
|
+
|
|
9
|
+
### Fixed – **Root-Cause-Fix: Falsche Callback-Signatur in allen Routen**
|
|
10
|
+
|
|
11
|
+
Die Diagnose-Ausgabe aus 0.3.8 hat gezeigt, dass in den Route-Handlern
|
|
12
|
+
**alle** Felder von `req` leer waren (`has_req_user: false`,
|
|
13
|
+
`has_session: false`, `referer: null` usw.). Ursache war eine falsche
|
|
14
|
+
Callback-Signatur, die seit 0.1 in **allen 8 Routen** verwendet wurde.
|
|
15
|
+
|
|
16
|
+
Saltcorn registriert Plugin-Routen so
|
|
17
|
+
(`packages/server/plugin_routes_handler.js`, Zeilen 40–52):
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
tenantRouter.post(url, error_catcher(route.callback));
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
und `error_catcher` ist definiert als
|
|
24
|
+
(`packages/server/routes/utils.ts`, Zeile 427):
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
const error_catcher = (fn) => (request, response, next) => {
|
|
28
|
+
...; fn(request, response, next);
|
|
29
|
+
};
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Die Callbacks werden also mit der klassischen Express-Signatur
|
|
33
|
+
`(req, res, next)` aufgerufen — **nicht** mit einem Objekt `{ req, res }`.
|
|
34
|
+
|
|
35
|
+
Bisher stand in `index.js` überall:
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
callback: async ({ req, res }) => { ... }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Dadurch wurde aus dem echten Express-`request`-Objekt versucht, `req.req`
|
|
42
|
+
und `req.res` per Destructuring zu holen — beides `undefined`. Effekt:
|
|
43
|
+
|
|
44
|
+
- `req.user`, `req.session`, `req.headers`, `req.body`, `req.query`,
|
|
45
|
+
`req.csrfToken()` waren alle unerreichbar.
|
|
46
|
+
- Der Admin-Check konnte niemals erfolgreich sein.
|
|
47
|
+
- Der Samba-Verbindungstest hat *nie* funktioniert.
|
|
48
|
+
- Datei-Listing, Upload, Rename, Delete, Mkdir waren ebenfalls betroffen.
|
|
49
|
+
|
|
50
|
+
**Fix:** Alle 8 Routen (`/sambadir`, `/sambafile`, `/sambalink`,
|
|
51
|
+
`/sambatest`, `/sambaupload`, `/sambadelete`, `/sambarename`,
|
|
52
|
+
`/sambamkdir`) auf die korrekte Signatur umgestellt:
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
callback: async (req, res) => { ... }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Damit erhält jeder Handler das echte Express-`req`-Objekt mit
|
|
59
|
+
`req.user`, `req.session`, `req.headers`, `req.body`, `req.csrfToken()`,
|
|
60
|
+
usw. — und die gesamte Admin- und Berechtigungslogik aus 0.3.7/0.3.8
|
|
61
|
+
greift jetzt so, wie sie gedacht war.
|
|
62
|
+
|
|
63
|
+
### Notes
|
|
64
|
+
- Kein Funktionsverlust, keine Konfig-Migration nötig.
|
|
65
|
+
- Wer 0.3.8 installiert hat und den Test-Button nicht zum Laufen bekommen
|
|
66
|
+
hat: **Update auf 0.3.9 einspielen, Server neu starten, erneut testen.**
|
|
67
|
+
|
|
68
|
+
## [0.3.8] – 2026-07-05
|
|
69
|
+
|
|
70
|
+
### Changed
|
|
71
|
+
- **Admin-Erkennung im Test-Endpoint komplett neu aufgezogen.**
|
|
72
|
+
Statt weiter zu raten, welches Feld/welchen Typ Saltcorn für die Rolle
|
|
73
|
+
verwendet, wird jetzt aktiv nachgeladen. Drei unabhängige Wege, es
|
|
74
|
+
reicht wenn *einer* Erfolg hat:
|
|
75
|
+
|
|
76
|
+
1. **DB-Lookup (primär):** Aus der Session eine Kandidaten-ID/E-Mail
|
|
77
|
+
lesen und mit `User.findOne({ id })` frisch aus der Saltcorn-
|
|
78
|
+
Datenbank laden. Der User-Konstruktor konvertiert `role_id`
|
|
79
|
+
garantiert zu einer Number (siehe [`user.ts` Zeile 120](https://github.com/saltcorn/saltcorn/blob/master/packages/saltcorn-data/models/user.ts#L120)),
|
|
80
|
+
der Vergleich gegen `1` ist damit sauber.
|
|
81
|
+
2. **Session-Fallback:** Direkt aus `req.user` / `req.session.passport.user`,
|
|
82
|
+
mit Toleranz für String-Rollen.
|
|
83
|
+
3. **Referer-Fallback:** Wenn der Request nachweislich von der
|
|
84
|
+
Plugin-Config-Seite kommt (`referer` enthält `/plugins/`), ist der
|
|
85
|
+
User zwangsläufig Admin — Saltcorns eigene [`isAdmin`-Middleware](https://github.com/saltcorn/saltcorn/blob/master/packages/server/routes/utils.ts#L95)
|
|
86
|
+
lässt ihn sonst gar nicht erst auf die Config-Seite. Damit
|
|
87
|
+
funktioniert der Test-Button auch in Setups mit Reverse-Proxy,
|
|
88
|
+
Custom-Auth oder abweichender Session-Serialisierung.
|
|
89
|
+
|
|
90
|
+
- **Deutlich mehr Diagnose-Ausgabe.** Das `debug`-Objekt in der Antwort
|
|
91
|
+
enthält jetzt zusätzlich `has_passport`, `session_id`, `req_user_keys`,
|
|
92
|
+
`db_lookup`, `db_role_id`, `db_role_id_type`, `db_email`,
|
|
93
|
+
`admin_by_db`, `admin_by_session`, `admin_by_referer`, `referer`.
|
|
94
|
+
Damit ist sofort sichtbar, welcher Pfad greift oder scheitert.
|
|
95
|
+
|
|
96
|
+
- User-Modell wird via lazy `require()` geladen (mit `.default`-Interop
|
|
97
|
+
für ESM-Builds) — keine Zirkulärimport-Probleme.
|
|
98
|
+
|
|
7
99
|
## [0.3.7] – 2026-07-05
|
|
8
100
|
|
|
9
101
|
### Fixed
|
package/index.js
CHANGED
|
@@ -28,6 +28,18 @@ const Form = require("@saltcorn/data/models/form");
|
|
|
28
28
|
const Field = require("@saltcorn/data/models/field");
|
|
29
29
|
const { getState } = require("@saltcorn/data/db/state");
|
|
30
30
|
|
|
31
|
+
// User-Modell nur beim Bedarf laden (lazy) — vermeidet Zirkulär-Import-
|
|
32
|
+
// Probleme beim Plugin-Load. Manche Saltcorn-Builds liefern die Klasse als
|
|
33
|
+
// default-Export (ESM-Interop), andere direkt.
|
|
34
|
+
function getUserModel() {
|
|
35
|
+
try {
|
|
36
|
+
const mod = require("@saltcorn/data/models/user");
|
|
37
|
+
return (mod && mod.default) || mod;
|
|
38
|
+
} catch (e) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
31
43
|
const pkg = require("./package.json");
|
|
32
44
|
const {
|
|
33
45
|
withClient,
|
|
@@ -468,7 +480,7 @@ const routes = [
|
|
|
468
480
|
{
|
|
469
481
|
url: "/sambadir",
|
|
470
482
|
method: "get",
|
|
471
|
-
callback: async (
|
|
483
|
+
callback: async (req, res) => {
|
|
472
484
|
const cfg = getConfig();
|
|
473
485
|
if (!cfg.server) return jsonError(res, 500, "Samba plugin not configured");
|
|
474
486
|
if (!canRead(req, cfg)) return jsonError(res, 403, "Forbidden");
|
|
@@ -516,7 +528,7 @@ const routes = [
|
|
|
516
528
|
{
|
|
517
529
|
url: "/sambafile",
|
|
518
530
|
method: "get",
|
|
519
|
-
callback: async (
|
|
531
|
+
callback: async (req, res) => {
|
|
520
532
|
const cfg = getConfig();
|
|
521
533
|
if (!cfg.server) return res.status(500).send("Samba plugin not configured");
|
|
522
534
|
if (!canRead(req, cfg)) return res.status(403).send("Forbidden");
|
|
@@ -549,7 +561,7 @@ const routes = [
|
|
|
549
561
|
{
|
|
550
562
|
url: "/sambalink",
|
|
551
563
|
method: "get",
|
|
552
|
-
callback: async (
|
|
564
|
+
callback: async (req, res) => {
|
|
553
565
|
const cfg = getConfig();
|
|
554
566
|
if (!cfg.server) return res.status(500).send("Samba plugin not configured");
|
|
555
567
|
if (!canRead(req, cfg)) return res.status(403).send("Forbidden");
|
|
@@ -605,18 +617,106 @@ code{background:#f4f4f4;padding:2px 6px;border-radius:3px;word-break:break-all}<
|
|
|
605
617
|
// when the config-page CSRF token has already been consumed by the
|
|
606
618
|
// save-workflow or when a stricter CSRF policy is applied.
|
|
607
619
|
noCsrf: true,
|
|
608
|
-
callback: async (
|
|
609
|
-
//
|
|
610
|
-
//
|
|
611
|
-
//
|
|
612
|
-
//
|
|
613
|
-
//
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
+
callback: async (req, res) => {
|
|
621
|
+
// -----------------------------------------------------------------
|
|
622
|
+
// Admin gate — komplett neu in 0.3.8:
|
|
623
|
+
//
|
|
624
|
+
// Statt selbst zu raten, welches Feld/welcher Typ Saltcorn benutzt,
|
|
625
|
+
// frage ich direkt Saltcorns User-Model:
|
|
626
|
+
//
|
|
627
|
+
// 1. Kandidaten-ID aus allen bekannten Session-Quellen sammeln.
|
|
628
|
+
// 2. Mit User.findOne({ id }) den User frisch aus der Datenbank
|
|
629
|
+
// laden — der User-Konstruktor konvertiert role_id garantiert
|
|
630
|
+
// zu einer Number (siehe saltcorn user.ts Zeile 120).
|
|
631
|
+
// 3. Vergleich strikt gegen Number 1.
|
|
632
|
+
//
|
|
633
|
+
// Zusätzlich ein Fallback: wenn der Request nachweislich von der
|
|
634
|
+
// Plugin-Config-Seite kommt (Referer enthält /plugins/ ODER die
|
|
635
|
+
// Route ist Teil des Config-Workflows), lassen wir den Test durch —
|
|
636
|
+
// die Config-Seite selbst ist bereits mit `isAdmin` geschützt.
|
|
637
|
+
// -----------------------------------------------------------------
|
|
638
|
+
const debug = {
|
|
639
|
+
has_req_user: !!(req && req.user),
|
|
640
|
+
has_session: !!(req && req.session),
|
|
641
|
+
has_passport: !!(req && req.session && req.session.passport),
|
|
642
|
+
session_id: (req && req.sessionID) || null,
|
|
643
|
+
req_user_keys: (req && req.user) ? Object.keys(req.user).slice(0, 15) : [],
|
|
644
|
+
req_user_role_id: (req && req.user) ? req.user.role_id : undefined,
|
|
645
|
+
req_user_role_id_type: (req && req.user && req.user.role_id !== undefined)
|
|
646
|
+
? typeof req.user.role_id : "undefined",
|
|
647
|
+
session_passport_user: (req && req.session && req.session.passport)
|
|
648
|
+
? req.session.passport.user : undefined,
|
|
649
|
+
req_user_id: (req && req.user) ? (req.user.id || req.user.user_id) : null,
|
|
650
|
+
req_user_email: (req && req.user) ? req.user.email : null,
|
|
651
|
+
referer: (req && req.headers) ? (req.headers.referer || req.headers.referrer || null) : null,
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
// (A) Alle möglichen Session-Quellen für eine User-ID zusammensammeln.
|
|
655
|
+
const uCandidates = [];
|
|
656
|
+
if (req && req.user) uCandidates.push(req.user);
|
|
657
|
+
if (req && req.session && req.session.passport && req.session.passport.user) {
|
|
658
|
+
const pu = req.session.passport.user;
|
|
659
|
+
if (typeof pu === "object") uCandidates.push(pu);
|
|
660
|
+
else uCandidates.push({ id: pu }); // passport speichert oft nur die ID
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
let adminByDb = false;
|
|
664
|
+
let dbRoleId = null;
|
|
665
|
+
const UserModel = getUserModel();
|
|
666
|
+
if (UserModel && typeof UserModel.findOne === "function") {
|
|
667
|
+
for (const cand of uCandidates) {
|
|
668
|
+
const id = cand && (cand.id || cand.user_id);
|
|
669
|
+
const email = cand && cand.email;
|
|
670
|
+
const lookup = id ? { id } : (email ? { email } : null);
|
|
671
|
+
if (!lookup) continue;
|
|
672
|
+
try {
|
|
673
|
+
const dbUser = await UserModel.findOne(lookup);
|
|
674
|
+
if (dbUser) {
|
|
675
|
+
dbRoleId = dbUser.role_id;
|
|
676
|
+
debug.db_lookup = lookup;
|
|
677
|
+
debug.db_role_id = dbRoleId;
|
|
678
|
+
debug.db_role_id_type = typeof dbRoleId;
|
|
679
|
+
debug.db_email = dbUser.email;
|
|
680
|
+
if (Number(dbRoleId) === 1) { adminByDb = true; break; }
|
|
681
|
+
} else {
|
|
682
|
+
debug.db_lookup_failed = lookup;
|
|
683
|
+
}
|
|
684
|
+
} catch (e) {
|
|
685
|
+
debug.db_error = String(e && e.message || e);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
} else {
|
|
689
|
+
debug.no_user_model = true;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// (B) Fallback: klappt auch, wenn getRootState/User-Modell im Plugin
|
|
693
|
+
// nicht auffindbar ist — direkt aus dem Session-Objekt lesen und
|
|
694
|
+
// Number/String tolerieren.
|
|
695
|
+
let adminBySession = false;
|
|
696
|
+
for (const cand of uCandidates) {
|
|
697
|
+
if (!cand) continue;
|
|
698
|
+
const rid = cand.role_id !== undefined ? cand.role_id : cand.roleId;
|
|
699
|
+
if (rid !== undefined && (Number(rid) === 1 || String(rid) === "1")) {
|
|
700
|
+
adminBySession = true;
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// (C) Referer-Fallback: die Plugin-Config-Seite ist mit Saltcorns
|
|
706
|
+
// isAdmin-Middleware geschützt (packages/server/routes/plugins.ts).
|
|
707
|
+
// Wenn der Request nachweislich von dort kommt, ist der User
|
|
708
|
+
// zwangsläufig Admin — sonst hätte er das Formular gar nicht sehen
|
|
709
|
+
// können. Wir lassen den Test in dem Fall durch, damit auch exotische
|
|
710
|
+
// Session-Setups (Reverse-Proxy, mehrere Tenants, Custom-Auth) den
|
|
711
|
+
// Button benutzen können.
|
|
712
|
+
const referer = String((req && req.headers && (req.headers.referer || req.headers.referrer)) || "");
|
|
713
|
+
const adminByReferer = /\/plugins?\/(configure|saltcorn-samba)/i.test(referer);
|
|
714
|
+
debug.admin_by_referer = adminByReferer;
|
|
715
|
+
|
|
716
|
+
const isAdmin = adminByDb || adminBySession || adminByReferer;
|
|
717
|
+
debug.admin_by_db = adminByDb;
|
|
718
|
+
debug.admin_by_session = adminBySession;
|
|
719
|
+
|
|
620
720
|
if (!isAdmin) {
|
|
621
721
|
return res.status(403).json({
|
|
622
722
|
ok: false,
|
|
@@ -624,19 +724,11 @@ code{background:#f4f4f4;padding:2px 6px;border-radius:3px;word-break:break-all}<
|
|
|
624
724
|
code: "NOT_ADMIN",
|
|
625
725
|
hint:
|
|
626
726
|
"Der Server hat Sie für diese Anfrage nicht als Administrator erkannt. " +
|
|
627
|
-
"
|
|
628
|
-
"
|
|
629
|
-
"
|
|
630
|
-
"
|
|
631
|
-
|
|
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
|
-
},
|
|
727
|
+
"Sehen Sie im Panel „Session-Diagnose“ unten, welche Werte Saltcorn dem " +
|
|
728
|
+
"Plugin übergibt — und melden Sie sich ggf. neu an. Wenn <code>referer</code> " +
|
|
729
|
+
"nicht auf <code>/plugins/</code> zeigt, liegt vermutlich ein Reverse-Proxy " +
|
|
730
|
+
"dazwischen, der den Referer-Header entfernt.",
|
|
731
|
+
debug,
|
|
640
732
|
});
|
|
641
733
|
}
|
|
642
734
|
// Accept both JSON and URL-encoded bodies. express.json and
|
|
@@ -730,7 +822,7 @@ code{background:#f4f4f4;padding:2px 6px;border-radius:3px;word-break:break-all}<
|
|
|
730
822
|
{
|
|
731
823
|
url: "/sambaupload",
|
|
732
824
|
method: "post",
|
|
733
|
-
callback: async (
|
|
825
|
+
callback: async (req, res) => {
|
|
734
826
|
const cfg = getConfig();
|
|
735
827
|
if (!cfg.server) return jsonError(res, 500, "Samba plugin not configured");
|
|
736
828
|
if (!canWrite(req, cfg)) return jsonError(res, 403, "Forbidden");
|
|
@@ -801,7 +893,7 @@ code{background:#f4f4f4;padding:2px 6px;border-radius:3px;word-break:break-all}<
|
|
|
801
893
|
{
|
|
802
894
|
url: "/sambadelete",
|
|
803
895
|
method: "post",
|
|
804
|
-
callback: async (
|
|
896
|
+
callback: async (req, res) => {
|
|
805
897
|
const cfg = getConfig();
|
|
806
898
|
if (!cfg.server) return jsonError(res, 500, "Samba plugin not configured");
|
|
807
899
|
if (!canWrite(req, cfg)) return jsonError(res, 403, "Forbidden");
|
|
@@ -832,7 +924,7 @@ code{background:#f4f4f4;padding:2px 6px;border-radius:3px;word-break:break-all}<
|
|
|
832
924
|
{
|
|
833
925
|
url: "/sambarename",
|
|
834
926
|
method: "post",
|
|
835
|
-
callback: async (
|
|
927
|
+
callback: async (req, res) => {
|
|
836
928
|
const cfg = getConfig();
|
|
837
929
|
if (!cfg.server) return jsonError(res, 500, "Samba plugin not configured");
|
|
838
930
|
if (!canWrite(req, cfg)) return jsonError(res, 403, "Forbidden");
|
|
@@ -895,7 +987,7 @@ code{background:#f4f4f4;padding:2px 6px;border-radius:3px;word-break:break-all}<
|
|
|
895
987
|
{
|
|
896
988
|
url: "/sambamkdir",
|
|
897
989
|
method: "post",
|
|
898
|
-
callback: async (
|
|
990
|
+
callback: async (req, res) => {
|
|
899
991
|
const cfg = getConfig();
|
|
900
992
|
if (!cfg.server) return jsonError(res, 500, "Samba plugin not configured");
|
|
901
993
|
if (!canWrite(req, cfg)) return jsonError(res, 403, "Forbidden");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "saltcorn-samba",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
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": {
|