From 0d098822680b31552dfb10c2ed2c2dee7b69ec28 Mon Sep 17 00:00:00 2001 From: marc Date: Sat, 20 Jun 2026 00:55:40 +0200 Subject: [PATCH] Add extra SMB/NFS settings to edit modal SMB: force user, force group, guest ok fields + dropdowns for owner/group NFS: no_root_squash and all_squash toggles exposed in edit modal Co-Authored-By: Claude Sonnet 4.6 --- app.py | 72 +++++++++++++++++++++++++++++++++---------- static/js/shares.js | 75 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 116 insertions(+), 31 deletions(-) diff --git a/app.py b/app.py index cb8c7a6..d7085c8 100644 --- a/app.py +++ b/app.py @@ -124,7 +124,9 @@ def parse_smb(): if m: if current is not None and current['name'].lower() not in _SMB_SKIP: shares.append(current) - current = {'name': m.group(1), 'path': '', 'valid_users': '', 'enabled': True} + current = {'name': m.group(1), 'path': '', 'valid_users': '', + 'enabled': True, 'guest_ok': False, + 'force_user': '', 'force_group': ''} elif current is not None and '=' in line: key, _, val = line.partition('=') key = key.strip().lower().replace(' ', '_') @@ -135,6 +137,12 @@ def parse_smb(): current['valid_users'] = val elif key == 'available': current['enabled'] = val.lower() not in ('no', 'false', '0') + elif key == 'guest_ok': + current['guest_ok'] = val.lower() in ('yes', 'true', '1') + elif key == 'force_user': + current['force_user'] = val + elif key == 'force_group': + current['force_group'] = val if current is not None and current['name'].lower() not in _SMB_SKIP: shares.append(current) @@ -202,10 +210,19 @@ def _write_exports(entries): def get_nfs_shares(): - return [ - {'path': e['path'], 'clients': e['rest'], 'enabled': e['enabled']} - for e in _parse_exports() if e['kind'] == 'export' - ] + shares = [] + for e in _parse_exports(): + if e['kind'] != 'export': + continue + rest = e['rest'] + shares.append({ + 'path': e['path'], + 'clients': rest, + 'enabled': e['enabled'], + 'no_root_squash': 'no_root_squash' in rest, + 'all_squash': 'all_squash' in rest, + }) + return shares def add_nfs_share(path, clients, ro=False): @@ -298,29 +315,44 @@ def delete_smb(name): @require_auth def edit_smb(name): data = request.json or {} - new_name = data.get('name', name).strip() - path = data.get('path', '') + new_name = data.get('name', name).strip() + path = data.get('path', '') valid_users = data.get('validUsers', '') + force_user = data.get('forceUser', '').strip() + force_group = data.get('forceGroup', '').strip() + guest_ok = data.get('guestOk', False) if not VALID_NAME.match(new_name): return jsonify({'error': 'invalid share name'}), 400 + if force_user and not VALID_NAME.match(force_user): + return jsonify({'error': 'invalid force user'}), 400 + if force_group and not VALID_NAME.match(force_group): + return jsonify({'error': 'invalid force group'}), 400 try: clean_users = _validate_users(valid_users) safe_path = validate_path(path) except ValueError as e: return jsonify({'error': str(e)}), 400 + lines = [ + f'[{new_name}]\n', + f' path = {safe_path}\n', + f' valid users = {clean_users}\n', + ' writable = yes\n', + ' browseable = yes\n', + ] + if force_user: + lines.append(f' force user = {force_user}\n') + if force_group: + lines.append(f' force group = {force_group}\n') + if guest_ok: + lines.append(' guest ok = yes\n') + sections = _parse_smb_conf() for s in sections: if s['name'] == name: s['name'] = new_name - s['lines'] = [ - f'[{new_name}]\n', - f' path = {safe_path}\n', - f' valid users = {clean_users}\n', - ' writable = yes\n', - ' browseable = yes\n', - ] + s['lines'] = lines break else: return jsonify({'error': f'share {name!r} not found'}), 404 @@ -402,8 +434,16 @@ def edit_nfs(): if not VALID_NFS_CLIENT.match(clients): return jsonify({'error': 'invalid client spec'}), 400 - options = 'ro' if ro else 'rw' - new_rest = f'{clients}({options},sync,no_subtree_check)' + no_root_squash = data.get('noRootSquash', False) + all_squash = data.get('allSquash', False) + options = ','.join(filter(None, [ + 'ro' if ro else 'rw', + 'sync', + 'no_subtree_check', + 'no_root_squash' if no_root_squash else None, + 'all_squash' if all_squash else None, + ])) + new_rest = f'{clients}({options})' entries = _parse_exports() for e in entries: diff --git a/static/js/shares.js b/static/js/shares.js index 5a44113..edf96f6 100644 --- a/static/js/shares.js +++ b/static/js/shares.js @@ -112,23 +112,31 @@ async function openEditModal(type, share) { const users = ur ? await ur.json() : []; const groups = gr ? await gr.json() : []; - const ownerOptions = ['root', ...users.map(u => u.name)] - .map(n => ``) + .join(''); + const groupOptions = groups + .map(g => ``) + .join(''); const permSection = ` - ${ownerOptions} - ${groupOptions}
Folder Permissions
- +
- +
@@ -155,10 +163,34 @@ async function openEditModal(type, share) {
+
+
+ + +
+
+ + +
+
+
+ + +
${permSection}`; } else { const isRo = /\(ro[,)]/.test(share.clients); @@ -172,10 +204,18 @@ async function openEditModal(type, share) {
-
+
+
+ + +
+
+ + +
${permSection}`; } @@ -188,19 +228,24 @@ async function submitEdit() { let res; if (ctx.type === 'smb') { - const name = document.getElementById('editName').value.trim(); - const path = document.getElementById('editPath').value.trim(); + const name = document.getElementById('editName').value.trim(); + const path = document.getElementById('editPath').value.trim(); const validUsers = document.getElementById('editUsers').value.trim(); + const forceUser = document.getElementById('editForceUser').value; + const forceGroup = document.getElementById('editForceGroup').value; + const guestOk = document.getElementById('editGuestOk').checked; res = await apiFetch(`/api/share/smb/${encodeURIComponent(ctx.original.name)}`, { method: 'PATCH', headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({name, path, validUsers}), + body: JSON.stringify({name, path, validUsers, forceUser, forceGroup, guestOk}), }); } else { - const clients = document.getElementById('editClients').value.trim(); - const readOnly = document.getElementById('editReadOnly').checked; + const clients = document.getElementById('editClients').value.trim(); + const readOnly = document.getElementById('editReadOnly').checked; + const noRootSquash = document.getElementById('editNoRootSquash').checked; + const allSquash = document.getElementById('editAllSquash').checked; res = await apiFetch('/api/share/nfs', { method: 'PATCH', headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({path: ctx.original.path, clients, readOnly}), + body: JSON.stringify({path: ctx.original.path, clients, readOnly, noRootSquash, allSquash}), }); }