diff --git a/app.py b/app.py index 84084f7..35fd368 100644 --- a/app.py +++ b/app.py @@ -1,5 +1,7 @@ import os import re +import pwd +import grp import shutil import hashlib import subprocess @@ -55,6 +57,19 @@ def validate_path(path_str): return str(p) +def _path_stat(path): + """Return owner, group, and octal mode for a path. Empty strings on error.""" + try: + s = os.stat(path) + return { + 'owner': pwd.getpwuid(s.st_uid).pw_name, + 'group': grp.getgrgid(s.st_gid).gr_name, + 'mode': oct(s.st_mode)[-3:], + } + except Exception: + return {'owner': '', 'group': '', 'mode': ''} + + def _validate_users(valid_users): """Validate and return a cleaned comma-separated user list, or raise ValueError.""" users = [] @@ -235,7 +250,13 @@ def index(): @app.route('/api/shares', methods=['GET']) @require_auth def get_all_shares(): - return jsonify({'smb': parse_smb(), 'nfs': get_nfs_shares()}) + smb = parse_smb() + for s in smb: + s.update(_path_stat(s['path'])) + nfs = get_nfs_shares() + for s in nfs: + s.update(_path_stat(s['path'])) + return jsonify({'smb': smb, 'nfs': nfs}) # ===== SMB CRUD ===== @@ -418,6 +439,42 @@ def toggle_nfs(): return jsonify({'status': 'enabled' if enabled else 'disabled'}) +# ===== PERMISSIONS ===== + +@app.route('/api/permissions', methods=['POST']) +@require_auth +def set_permissions(): + data = request.json or {} + path = data.get('path', '') + owner = data.get('owner', '').strip() + group = data.get('group', '').strip() + mode = data.get('mode', '').strip() + + try: + safe_path = validate_path(path) + except ValueError as e: + return jsonify({'error': str(e)}), 400 + if owner and not VALID_NAME.match(owner): + return jsonify({'error': 'invalid owner name'}), 400 + if group and not VALID_NAME.match(group): + return jsonify({'error': 'invalid group name'}), 400 + if mode and not re.match(r'^[0-7]{3,4}$', mode): + return jsonify({'error': 'mode must be 3-4 octal digits (e.g. 770)'}), 400 + + if owner or group: + chown_arg = f'{owner}:{group}' if (owner and group) else (owner or f':{group}') + r = subprocess.run(['chown', chown_arg, safe_path], capture_output=True, text=True) + if r.returncode != 0: + return jsonify({'error': r.stderr.strip() or 'chown failed'}), 500 + + if mode: + r = subprocess.run(['chmod', mode, safe_path], capture_output=True, text=True) + if r.returncode != 0: + return jsonify({'error': r.stderr.strip() or 'chmod failed'}), 500 + + return jsonify({'status': 'ok', 'stat': _path_stat(safe_path)}) + + # ===== USERS ===== def _smb_users(): diff --git a/static/js/shares.js b/static/js/shares.js index 0eed50c..28d896d 100644 --- a/static/js/shares.js +++ b/static/js/shares.js @@ -105,6 +105,30 @@ function openEditModal(type, share) { document.getElementById('editModalTitle').textContent = type === 'smb' ? `Edit SMB Share — ${esc(share.name)}` : `Edit NFS Export — ${esc(share.path)}`; + const permSection = ` +
770 owner+group rw ·
+ 750 group read-only ·
+ 755 world read ·
+ Use @groupname in Valid Users to allow a whole group.
+