feat(quick-261009-dkv): Teilen mit Personen und Gruppen - Durchstich, Schnittstelle

- Freigaben-Schicht (nextcloud-shares.ts): eigener OCS-Aufruf, der Fehlertexte liest,
  Parser fuer Freigaben, Empfaenger und Freigaberegeln, Berechtigungsabbildung
- Dienst mit Vorpruefung aus den Faehigkeiten, Dublettenpruefung und Fehlermatrix
  (nie 401/403 nach aussen), Begrenzung auf 15 neue Freigaben in 10 Minuten
- Routen shares/policy, shares/by-path, sharees, POST/PUT/DELETE shares
- oc:share-types als shareTypes an jedem Eintrag
- Live-Test e2e-shares.sh (Abschnitt people)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-09 10:40:39 +02:00
parent 43a6a83347
commit 207d37b8d6
15 changed files with 2584 additions and 5 deletions
@@ -0,0 +1,231 @@
#!/usr/bin/env bash
# quick-261009-dkv: Teilen-Schnittstelle gegen die Test-Nextcloud (tessera-nc-test).
# Aufruf: e2e-shares.sh [people|links|received|version|all] (Vorgabe: all)
# Aufgabe 1 liefert Aufbau, Aufraeumen und den Abschnitt "people"; links und received
# folgen in Aufgabe 2, version in Aufgabe 3.
# Setzt einen laufenden Stack (api neu gebaut: docker compose up -d --build api) und
# nc-test-setup.sh voraus. Nur Testwerte; liest keine .env-Dateien.
#
# Die Test-Nextcloud bekommt den Benutzer ben (Passwort User3-Pass-12345, ohne Zwei-Faktor) und die
# Gruppe tessera-team (mit ben). Die Ratenbegrenzung der Nextcloud wird NUR fuer diesen Lauf
# ausgeschaltet (sonst koennte wiederholtes Testen deren 429 ausloesen, das die Aufrufsperre fuer
# den ganzen Ursprung anhaelt). Die Begrenzung von Tessera (15 in 10 Minuten) beweisen die
# Einheitstests. Alles, was das Skript anfasst, wird im trap zurueckgesetzt.
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$HERE/../../261008-mzu-modul-nextcloud-dateien-eigenstaendiger-/e2e/e2e-lib.sh"
SECTION=${1:-all}
case "$SECTION" in people | links | received | version | all) ;; *) e2e_fail "unbekannter Abschnitt: $SECTION" ;; esac
FILES="$API/modules/nextcloud-files"
NC_DAV="http://localhost:18080/remote.php/dav"
NC_OCS="http://localhost:18080/ocs/v2.php/apps/files_sharing/api/v1"
ANNA="anna:User1-Pass-12345"
BEN="ben:User3-Pass-12345"
EPOCH=$(date +%s)
FIX="Tessera-Teilen-$EPOCH"
START_ISO=$(date -u +%Y-%m-%dT%H:%M:%SZ)
# --- Aufraeumen: alles zuruecksetzen, was das Skript angefasst hat ----------------------------
cleanup() {
local rc=$?
set +e
curl -s -o /dev/null -u "$ANNA" -X DELETE "$NC_DAV/files/anna/$FIX" 2>/dev/null
NC_OCC config:system:delete ratelimit.protection.enabled >/dev/null 2>&1
for key in shareapi_enforce_links_password shareapi_default_expire_date \
shareapi_enforce_expire_date shareapi_expire_after_n_days; do
NC_OCC config:app:delete core "$key" >/dev/null 2>&1
done
NC_OCC user:setting --delete anna files_sharing default_accept >/dev/null 2>&1
rm -rf "$E2E_TMP"
exit $rc
}
trap cleanup EXIT
# --- Hilfen -------------------------------------------------------------------------------------
ADMIN="$E2E_TMP/admin.jar"
SEEN="$E2E_TMP/seen.txt"
: >"$SEEN"
# call <methode> <pfad-nach-/modules/nextcloud-files/> [json] — gibt den Status aus und merkt ihn in
# $SEEN fuer die Pruefung "nie 401/403" (Datei, weil $(...) in einer Unterschale laeuft). Antwortkoerper: $E2E_TMP/body.out
call() {
local code
code=$(e2e_status "$ADMIN" "$1" "$FILES/$2" "${3:-}")
echo "$code" >>"$SEEN"
echo "$code"
}
# qget <endpoint> <schluessel> <wert> [<schluessel> <wert> ...] — GET mit sauber codierter Query.
qget() {
local endpoint=$1 args=() code
shift
while [ $# -gt 0 ]; do
args+=(--data-urlencode "$1=$2")
shift 2
done
code=$(curl -s -o "$E2E_TMP/body.out" -w '%{http_code}' -b "$ADMIN" -G "${args[@]}" "$FILES/$endpoint")
echo "$code" >>"$SEEN"
echo "$code"
}
# jq_py <ausdruck> — wertet einen Python-Ausdruck auf der JSON-Datei $E2E_TMP/body.out aus (d = Daten).
jq_py() {
python3 -I -c 'import json,sys
d=json.load(open(sys.argv[1]))
print(eval(sys.argv[2]))' "$E2E_TMP/body.out" "$1"
}
# Direkt bei der Nextcloud: eingehende Freigaben von ben (JSON in $E2E_TMP/ben.out)
ben_shares() {
curl -s -u "$BEN" -H 'OCS-APIRequest: true' -H 'Accept: application/json' \
"$NC_OCS/shares?shared_with_me=true" -o "$E2E_TMP/ben.out"
}
# ben_count <ordnername> — Anzahl der eingehenden Freigaben von ben mit diesem Ziel
ben_count() {
ben_shares
python3 -I -c 'import json,sys
d=json.load(open(sys.argv[1]))["ocs"]["data"]
print(len([s for s in d if s.get("file_target","").lstrip("/")==sys.argv[2]]))' "$E2E_TMP/ben.out" "$1"
}
ben_perm() {
ben_shares
python3 -I -c 'import json,sys
d=json.load(open(sys.argv[1]))["ocs"]["data"]
print([s["permissions"] for s in d if s.get("file_target","").lstrip("/")==sys.argv[2]][0])' "$E2E_TMP/ben.out" "$1"
}
# --- Aufbau --------------------------------------------------------------------------------------
e2e_wait_health
e2e_login "$ADMIN"
e2e_activate "$ADMIN"
e2e_set_address "$ADMIN"
code=$(e2e_connect_anna "$ADMIN")
e2e_expect 200 "$code" "anna verbinden"
code=$(call GET shares/policy)
[ "$code" = "200" ] || e2e_fail "GET shares/policy -> $code. API-Container neu bauen: docker compose up -d --build api"
# ben und die Gruppe (idempotent)
if ! NC_OCC user:info ben >/dev/null 2>&1; then
docker exec -u www-data -e OC_PASS='User3-Pass-12345' tessera-nc-test \
php occ user:add --password-from-env --display-name="Ben Beispiel" ben >/dev/null
fi
GROUPS_NOW=$(NC_OCC group:list 2>/dev/null || true)
case "$GROUPS_NOW" in *tessera-team*) ;; *) NC_OCC group:add tessera-team >/dev/null ;; esac
NC_OCC group:adduser tessera-team ben >/dev/null 2>&1 || true
NC_OCC config:system:set ratelimit.protection.enabled --value=false --type=boolean >/dev/null
# Ordner mit Datei und Unterordner in annas Konto
code=$(curl -s -o /dev/null -w '%{http_code}' -u "$ANNA" -X MKCOL "$NC_DAV/files/anna/$FIX")
e2e_expect 201 "$code" "Fixture-Ordner"
code=$(curl -s -o /dev/null -w '%{http_code}' -u "$ANNA" -X PUT --data-binary 'Bericht' "$NC_DAV/files/anna/$FIX/Bericht.txt")
e2e_expect 201 "$code" "Fixture Bericht.txt"
code=$(curl -s -o /dev/null -w '%{http_code}' -u "$ANNA" -X MKCOL "$NC_DAV/files/anna/$FIX/Briefkasten")
e2e_expect 201 "$code" "Fixture Briefkasten"
# --- Abschnitt people ----------------------------------------------------------------------------
section_people() {
local code share_id group_id
code=$(call GET shares/policy)
e2e_expect 200 "$code" "Richtlinie lesen"
e2e_expect True "$(jq_py 'd["enabled"] is True and d["links"]["passwordRequired"] is False')" "Richtlinie: enabled, Passwort nicht erzwungen"
code=$(qget sharees term ben itemType folder)
e2e_expect 200 "$code" "Empfaengersuche ben"
e2e_expect True "$(jq_py 'any(s["kind"]=="user" and s["id"]=="ben" for s in d["sharees"])')" "Suche ben findet die Person"
code=$(qget sharees term tessera itemType folder)
e2e_expect 200 "$code" "Empfaengersuche tessera"
e2e_expect True "$(jq_py 'any(s["kind"]=="group" and s["id"]=="tessera-team" for s in d["sharees"])')" "Suche tessera findet die Gruppe"
code=$(qget sharees term '' itemType folder)
e2e_expect 200 "$code" "leere Suche"
e2e_expect '[]' "$(jq_py 'd["sharees"]')" "leere Suche liefert nichts"
# Personenfreigabe: Ordner an ben, Bearbeiten
code=$(call POST shares "{\"path\":\"/$FIX\",\"kind\":\"user\",\"shareWith\":\"ben\",\"access\":\"edit\"}")
e2e_expect 201 "$code" "Personenfreigabe anlegen"
e2e_expect True "$(jq_py 'd["kind"]=="user" and d["access"]=="edit" and d["permissions"]==15 and d["shareWith"]=="ben"')" "Freigabe: Person, Bearbeiten, 15"
share_id=$(jq_py 'd["id"]')
e2e_expect 1 "$(ben_count "$FIX")" "ben sieht die Freigabe"
e2e_expect 15 "$(ben_perm "$FIX")" "ben: Berechtigung 15"
# Dieselbe Freigabe nochmal: abgelehnt, kein zweiter Eintrag
code=$(call POST shares "{\"path\":\"/$FIX\",\"kind\":\"user\",\"shareWith\":\"ben\",\"access\":\"edit\"}")
e2e_expect 409 "$code" "doppelte Freigabe"
e2e_contains "$E2E_TMP/body.out" '"code":"shareAlreadyExists"' "doppelt: shareAlreadyExists"
e2e_expect 1 "$(ben_count "$FIX")" "ben sieht weiterhin genau eine Freigabe"
# Hochladen ist fuer Personen nicht moeglich, und die Berechtigung kommt nur als Auswahl
code=$(call POST shares "{\"path\":\"/$FIX/Briefkasten\",\"kind\":\"user\",\"shareWith\":\"ben\",\"access\":\"upload\"}")
e2e_expect 400 "$code" "Hochladen fuer eine Person"
e2e_contains "$E2E_TMP/body.out" '"code":"shareAccessInvalid"' "upload: shareAccessInvalid"
code=$(call POST shares "{\"path\":\"/$FIX/Briefkasten\",\"kind\":\"user\",\"shareWith\":\"ben\",\"access\":\"abc\"}")
e2e_expect 400 "$code" "unbekannte Auswahl"
# Auf Ansehen aendern
code=$(call PUT "shares/$share_id" '{"access":"view"}')
e2e_expect 200 "$code" "auf Ansehen aendern"
e2e_expect True "$(jq_py 'd["access"]=="view" and d["permissions"]==1')" "Freigabe: Ansehen, 1"
e2e_expect 1 "$(ben_perm "$FIX")" "ben: Berechtigung 1"
# Gruppenfreigabe einer Datei
code=$(call POST shares "{\"path\":\"/$FIX/Bericht.txt\",\"kind\":\"group\",\"shareWith\":\"tessera-team\",\"access\":\"view\"}")
e2e_expect 201 "$code" "Gruppenfreigabe anlegen"
e2e_expect True "$(jq_py 'd["kind"]=="group" and d["itemType"]=="file" and d["permissions"]==1')" "Gruppenfreigabe: Gruppe, Datei, 1"
group_id=$(jq_py 'd["id"]')
# Freigaben des Ordners: genau die Personenfreigabe
code=$(qget shares/by-path path "/$FIX")
e2e_expect 200 "$code" "Freigaben des Ordners"
e2e_expect True "$(jq_py 'len(d["shares"])==1 and d["shares"][0]["id"]=="'"$share_id"'"')" "by-path: genau die Personenfreigabe"
# Kennzeichen in der Dateiliste
code=$(qget files path /)
e2e_expect 200 "$code" "Wurzel auflisten"
e2e_expect True "$(jq_py 'any(e["name"]=="'"$FIX"'" and 0 in e["shareTypes"] for e in d["entries"])')" "Wurzel: Ordner traegt shareTypes mit 0"
code=$(qget files path "/$FIX")
e2e_expect 200 "$code" "Ordner auflisten"
e2e_expect True "$(jq_py 'any(e["name"]=="Bericht.txt" and 1 in e["shareTypes"] for e in d["entries"])')" "Ordner: Bericht.txt traegt shareTypes mit 1"
e2e_expect True "$(jq_py 'any(e["name"]=="Briefkasten" and e["shareTypes"]==[] for e in d["entries"])')" "Ordner: Briefkasten ist nicht geteilt"
# Entfernen
code=$(call DELETE "shares/$share_id")
e2e_expect 200 "$code" "Personenfreigabe entfernen"
e2e_expect 0 "$(ben_count "$FIX")" "ben sieht die Freigabe nicht mehr"
code=$(qget shares/by-path path "/$FIX")
e2e_expect 200 "$code" "Freigaben des Ordners nach dem Entfernen"
e2e_expect True "$(jq_py 'd["shares"]==[]')" "by-path: leer"
code=$(call DELETE "shares/$group_id")
e2e_expect 200 "$code" "Gruppenfreigabe entfernen"
code=$(call DELETE "shares/$share_id")
e2e_expect 404 "$code" "bereits entfernte Freigabe"
e2e_contains "$E2E_TMP/body.out" '"code":"shareNotFound"' "entfernt: shareNotFound"
code=$(call DELETE shares/abc)
e2e_expect 404 "$code" "ungueltige Kennung"
e2e_contains "$E2E_TMP/body.out" '"code":"shareNotFound"' "abc: shareNotFound"
echo "e2e shares people ok"
}
section_links() { echo "e2e shares links: folgt in Aufgabe 2"; }
section_received() { echo "e2e shares received: folgt in Aufgabe 2"; }
section_version() { echo "e2e shares version: folgt in Aufgabe 3"; }
case "$SECTION" in
people) section_people ;;
links) section_links ;;
received) section_received ;;
version) section_version ;;
all)
section_people
section_links
section_received
section_version
;;
esac
# Nie 401 oder 403 an den Browser (Fehlervertrag D-D)
if grep -qE '^(401|403)$' "$SEEN"; then e2e_fail "ein Aufruf endete mit 401 oder 403"; fi
: "$START_ISO"