feat: Benutzerfreigabe - Weiterteilen-Recht + Lesezugriff wird erzwungen

Neues Berechtigungs-Modell fuer Benutzerfreigaben:

* FilePermission bekommt zwei neue Spalten:
  - can_reshare (bool): darf dieser Nutzer die Freigabe weiterverteilen?
  - granted_by (user_id): wer hat diese Freigabe erstellt?

* set_permission / create_share_link erlauben jetzt auch Nicht-Owner,
  sofern sie can_reshare haben. Dabei gilt:
  - Lesend + reshare -> kann nur lesend weiterteilen
  - Schreibend + reshare -> kann lesend ODER schreibend weiterteilen
  - Admin kann nur der Eigentuemer vergeben
  - Jeder Re-Sharer kann wiederum can_reshare weitergeben

* remove_permission: Owner kann alle Freigaben entfernen; Re-Sharer
  nur die von ihnen selbst erstellten.

* get_permissions: Owner sieht alle; Re-Sharer nur selbst-erstellte.

* list_files liefert my_permission + my_can_reshare pro Eintrag -
  Frontend kann Rename/Delete/Share-Buttons gezielt ein- und
  ausblenden statt blind alle anzuzeigen.

Frontend:
* Rename/Delete-Buttons nur fuer Write-Zugriff
* Share-Button nur fuer Owner oder Re-Sharer
* "darf weiterteilen" Checkbox neben Permission-Dropdown im Dialog
* Dropdown-Optionen nach eigenem Level gefiltert (Re-Sharer sieht
  keine hoeheren Stufen als seine eigene)
* Hinweis-Text "Du hast X - du kannst maximal X weiterteilen"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Hacker
2026-04-12 11:54:36 +02:00
parent 035923834b
commit 9369c851a0
3 changed files with 165 additions and 18 deletions
+105 -13
View File
@@ -33,6 +33,29 @@ def _share_recipients(file_obj):
return list(ids)
def _effective_permission(file_obj, user):
"""Returns (permission_level, can_reshare) for the given user on this file,
walking up the folder tree. Owner gets ('admin', True). Returns
(None, False) if no access."""
if file_obj.owner_id == user.id:
return ('admin', True)
levels = {'read': 0, 'write': 1, 'admin': 2}
best_level = -1
best_perm = None
best_reshare = False
cur = file_obj
while cur is not None:
perm = FilePermission.query.filter_by(file_id=cur.id, user_id=user.id).first()
if perm:
lvl = levels.get(perm.permission, -1)
if lvl > best_level:
best_level = lvl
best_perm = perm.permission
best_reshare = perm.can_reshare
cur = cur.parent
return (best_perm, best_reshare)
def _user_upload_dir(user_id):
base = Path(current_app.config['UPLOAD_PATH'])
user_dir = base / str(user_id)
@@ -120,6 +143,9 @@ def list_files():
d = f.to_dict()
d['has_shares'] = ShareLink.query.filter_by(file_id=f.id).count() > 0
d['has_permissions'] = FilePermission.query.filter_by(file_id=f.id).count() > 0
my_perm, my_reshare = _effective_permission(f, user)
d['my_permission'] = my_perm
d['my_can_reshare'] = bool(my_reshare)
lock = FileLock.get_lock(f.id)
if lock:
d['locked'] = True
@@ -129,6 +155,9 @@ def list_files():
for f in shared:
d = f.to_dict()
d['shared'] = True
my_perm, my_reshare = _effective_permission(f, user)
d['my_permission'] = my_perm
d['my_can_reshare'] = bool(my_reshare)
result.append(d)
# Build breadcrumb
@@ -543,12 +572,21 @@ def empty_trash():
@token_required
def get_permissions(file_id):
user = request.current_user
f, err = _get_file_or_403(file_id, user, 'admin')
if err:
if not (f := db.session.get(File, file_id)) or f.owner_id != user.id:
return jsonify({'error': 'Zugriff verweigert'}), 403
f = db.session.get(File, file_id)
if not f:
return jsonify({'error': 'Datei nicht gefunden'}), 404
is_owner = (f.owner_id == user.id)
my_perm, my_reshare = _effective_permission(f, user)
if not is_owner and not my_reshare:
return jsonify({'error': 'Zugriff verweigert'}), 403
# Owners see everyone; re-sharers only see perms they granted themselves.
if is_owner:
perms = FilePermission.query.filter_by(file_id=file_id).all()
else:
perms = FilePermission.query.filter_by(file_id=file_id, granted_by=user.id).all()
perms = FilePermission.query.filter_by(file_id=file_id).all()
from app.models.user import User
result = []
for p in perms:
@@ -558,6 +596,8 @@ def get_permissions(file_id):
'user_id': p.user_id,
'username': u.username if u else None,
'permission': p.permission,
'can_reshare': bool(p.can_reshare),
'granted_by': p.granted_by,
})
return jsonify(result), 200
@@ -567,29 +607,60 @@ def get_permissions(file_id):
def set_permission(file_id):
user = request.current_user
f = db.session.get(File, file_id)
if not f or f.owner_id != user.id:
return jsonify({'error': 'Nur der Eigentuemer kann Berechtigungen setzen'}), 403
if not f:
return jsonify({'error': 'Datei nicht gefunden'}), 404
is_owner = (f.owner_id == user.id)
my_perm, my_reshare = _effective_permission(f, user)
if not is_owner and not my_reshare:
return jsonify({'error': 'Keine Berechtigung zum Weiterteilen'}), 403
data = request.get_json()
target_user_id = data.get('user_id')
permission = data.get('permission', 'read')
can_reshare_req = bool(data.get('can_reshare', False))
if permission not in ('read', 'write', 'admin'):
return jsonify({'error': 'Ungueltige Berechtigung'}), 400
# Re-sharers can't hand out more than they have themselves.
levels = {'read': 0, 'write': 1, 'admin': 2}
if not is_owner:
max_allowed = levels.get(my_perm, -1)
if levels.get(permission, -1) > max_allowed:
return jsonify({
'error': f'Du kannst hoechstens "{my_perm}" weiterverteilen'
}), 403
if permission == 'admin':
return jsonify({'error': 'Admin-Recht kann nur der Eigentuemer vergeben'}), 403
from app.models.user import User
target = db.session.get(User, target_user_id)
if not target:
return jsonify({'error': 'Benutzer nicht gefunden'}), 404
if target.id == f.owner_id:
return jsonify({'error': 'Eigentuemer hat bereits Vollzugriff'}), 400
existing = FilePermission.query.filter_by(
file_id=file_id, user_id=target_user_id
).first()
is_new = not existing
if existing:
# Re-sharers may only modify perms they themselves granted
if not is_owner and existing.granted_by != user.id:
return jsonify({'error': 'Diese Freigabe wurde von jemand anderem erstellt'}), 403
existing.permission = permission
existing.can_reshare = can_reshare_req
if is_new or existing.granted_by is None:
existing.granted_by = user.id
else:
perm = FilePermission(file_id=file_id, user_id=target_user_id, permission=permission)
perm = FilePermission(
file_id=file_id,
user_id=target_user_id,
permission=permission,
can_reshare=can_reshare_req,
granted_by=user.id,
)
db.session.add(perm)
db.session.commit()
@@ -610,13 +681,17 @@ def set_permission(file_id):
def remove_permission(file_id, perm_id):
user = request.current_user
f = db.session.get(File, file_id)
if not f or f.owner_id != user.id:
return jsonify({'error': 'Nur der Eigentuemer kann Berechtigungen entfernen'}), 403
if not f:
return jsonify({'error': 'Datei nicht gefunden'}), 404
perm = db.session.get(FilePermission, perm_id)
if not perm or perm.file_id != file_id:
return jsonify({'error': 'Berechtigung nicht gefunden'}), 404
is_owner = (f.owner_id == user.id)
if not is_owner and perm.granted_by != user.id:
return jsonify({'error': 'Du kannst nur selbst erstellte Freigaben entfernen'}), 403
db.session.delete(perm)
db.session.commit()
return jsonify({'message': 'Berechtigung entfernt'}), 200
@@ -628,9 +703,14 @@ def remove_permission(file_id, perm_id):
@token_required
def create_share_link(file_id):
user = request.current_user
f, err = _get_file_or_403(file_id, user, 'read')
if err:
return err
f = db.session.get(File, file_id)
if not f:
return jsonify({'error': 'Datei nicht gefunden'}), 404
is_owner = (f.owner_id == user.id)
my_perm, my_reshare = _effective_permission(f, user)
if not is_owner and not my_reshare:
return jsonify({'error': 'Keine Berechtigung zum Weiterteilen'}), 403
data = request.get_json() or {}
password = data.get('password')
@@ -641,6 +721,18 @@ def create_share_link(file_id):
if permission not in ('read', 'write', 'upload_only'):
return jsonify({'error': 'Berechtigung muss "read", "write" oder "upload_only" sein'}), 400
# Re-sharers can only hand out what they have themselves.
if not is_owner:
levels = {'read': 0, 'write': 1}
max_allowed = levels.get(my_perm, -1)
requested = levels.get(permission, 99)
if requested > max_allowed:
return jsonify({
'error': f'Du hast selbst nur "{my_perm}" - kannst nicht schreibend weiterteilen'
}), 403
if permission == 'upload_only' and my_perm not in ('write', 'admin'):
return jsonify({'error': 'Upload-Links nur mit Schreibrecht moeglich'}), 403
token = secrets.token_urlsafe(32)
password_hash = None
if password: