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:
+105
-13
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user