Add folder permission editor to share edit modal

Backend:
- Import pwd/grp for uid/gid → name resolution
- _path_stat(path): returns current owner, group, octal mode via os.stat
- get_all_shares: attaches stat info to each share so the modal can
  pre-fill current values
- POST /api/permissions: validates and applies chown/chmod to a share
  path; returns updated stat on success

Frontend:
- Edit modal now has a Permissions section (Owner / Group / Mode)
  pre-filled with current values, with a hint line showing common modes
- Valid Users label notes @groupname syntax for Samba group access
- submitEdit applies permissions after saving the share config if any
  permission field is non-empty; shows specific error on failure

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Sonnet 4.6 committed 2026-06-20 00:26:00 +02:00
1 parent e03905b26d
commit 302a5cb023
2 files changed
+115 -6

No files matched your search

+58 -1
View File
@@ -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():