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 <noreply@anthropic.com>
This commit is contained in:
1 parent
596365b805
commit
0d09882268
2 files changed
+116
-31
No files matched your search
@@ -124,7 +124,9 @@ def parse_smb():
|
|||||||
if m:
|
if m:
|
||||||
if current is not None and current['name'].lower() not in _SMB_SKIP:
|
if current is not None and current['name'].lower() not in _SMB_SKIP:
|
||||||
shares.append(current)
|
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:
|
elif current is not None and '=' in line:
|
||||||
key, _, val = line.partition('=')
|
key, _, val = line.partition('=')
|
||||||
key = key.strip().lower().replace(' ', '_')
|
key = key.strip().lower().replace(' ', '_')
|
||||||
@@ -135,6 +137,12 @@ def parse_smb():
|
|||||||
current['valid_users'] = val
|
current['valid_users'] = val
|
||||||
elif key == 'available':
|
elif key == 'available':
|
||||||
current['enabled'] = val.lower() not in ('no', 'false', '0')
|
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:
|
if current is not None and current['name'].lower() not in _SMB_SKIP:
|
||||||
shares.append(current)
|
shares.append(current)
|
||||||
@@ -202,10 +210,19 @@ def _write_exports(entries):
|
|||||||
|
|
||||||
|
|
||||||
def get_nfs_shares():
|
def get_nfs_shares():
|
||||||
return [
|
shares = []
|
||||||
{'path': e['path'], 'clients': e['rest'], 'enabled': e['enabled']}
|
for e in _parse_exports():
|
||||||
for e in _parse_exports() if e['kind'] == 'export'
|
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):
|
def add_nfs_share(path, clients, ro=False):
|
||||||
@@ -298,29 +315,44 @@ def delete_smb(name):
|
|||||||
@require_auth
|
@require_auth
|
||||||
def edit_smb(name):
|
def edit_smb(name):
|
||||||
data = request.json or {}
|
data = request.json or {}
|
||||||
new_name = data.get('name', name).strip()
|
new_name = data.get('name', name).strip()
|
||||||
path = data.get('path', '')
|
path = data.get('path', '')
|
||||||
valid_users = data.get('validUsers', '')
|
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):
|
if not VALID_NAME.match(new_name):
|
||||||
return jsonify({'error': 'invalid share name'}), 400
|
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:
|
try:
|
||||||
clean_users = _validate_users(valid_users)
|
clean_users = _validate_users(valid_users)
|
||||||
safe_path = validate_path(path)
|
safe_path = validate_path(path)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return jsonify({'error': str(e)}), 400
|
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()
|
sections = _parse_smb_conf()
|
||||||
for s in sections:
|
for s in sections:
|
||||||
if s['name'] == name:
|
if s['name'] == name:
|
||||||
s['name'] = new_name
|
s['name'] = new_name
|
||||||
s['lines'] = [
|
s['lines'] = lines
|
||||||
f'[{new_name}]\n',
|
|
||||||
f' path = {safe_path}\n',
|
|
||||||
f' valid users = {clean_users}\n',
|
|
||||||
' writable = yes\n',
|
|
||||||
' browseable = yes\n',
|
|
||||||
]
|
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
return jsonify({'error': f'share {name!r} not found'}), 404
|
return jsonify({'error': f'share {name!r} not found'}), 404
|
||||||
@@ -402,8 +434,16 @@ def edit_nfs():
|
|||||||
if not VALID_NFS_CLIENT.match(clients):
|
if not VALID_NFS_CLIENT.match(clients):
|
||||||
return jsonify({'error': 'invalid client spec'}), 400
|
return jsonify({'error': 'invalid client spec'}), 400
|
||||||
|
|
||||||
options = 'ro' if ro else 'rw'
|
no_root_squash = data.get('noRootSquash', False)
|
||||||
new_rest = f'{clients}({options},sync,no_subtree_check)'
|
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()
|
entries = _parse_exports()
|
||||||
for e in entries:
|
for e in entries:
|
||||||
|
|||||||
+60
-15
@@ -112,23 +112,31 @@ async function openEditModal(type, share) {
|
|||||||
const users = ur ? await ur.json() : [];
|
const users = ur ? await ur.json() : [];
|
||||||
const groups = gr ? await gr.json() : [];
|
const groups = gr ? await gr.json() : [];
|
||||||
|
|
||||||
const ownerOptions = ['root', ...users.map(u => u.name)]
|
const ownerNames = ['root', ...users.map(u => u.name)];
|
||||||
.map(n => `<option value="${esc(n)}">`).join('');
|
const ownerOptions = ownerNames
|
||||||
const groupOptions = groups.map(g => `<option value="${esc(g.name)}">`).join('');
|
.map(n => `<option value="${esc(n)}"${share.owner === n ? ' selected' : ''}>${esc(n)}</option>`)
|
||||||
|
.join('');
|
||||||
|
const groupOptions = groups
|
||||||
|
.map(g => `<option value="${esc(g.name)}"${share.group === g.name ? ' selected' : ''}>${esc(g.name)}</option>`)
|
||||||
|
.join('');
|
||||||
|
|
||||||
const permSection = `
|
const permSection = `
|
||||||
<datalist id="ownerList">${ownerOptions}</datalist>
|
|
||||||
<datalist id="groupList">${groupOptions}</datalist>
|
|
||||||
<hr style="border-color:var(--border); margin:1.25rem 0 1rem">
|
<hr style="border-color:var(--border); margin:1.25rem 0 1rem">
|
||||||
<div class="fw-semibold mb-3" style="font-size:.9rem; color:var(--text)">Folder Permissions</div>
|
<div class="fw-semibold mb-3" style="font-size:.9rem; color:var(--text)">Folder Permissions</div>
|
||||||
<div class="row g-2 mb-3">
|
<div class="row g-2 mb-3">
|
||||||
<div class="col-5">
|
<div class="col-5">
|
||||||
<label class="form-label" style="color:var(--muted); font-size:.85rem">Owner</label>
|
<label class="form-label" style="color:var(--muted); font-size:.85rem">Owner</label>
|
||||||
<input type="text" id="editOwner" class="form-control" value="${esc(share.owner || '')}" placeholder="root" list="ownerList">
|
<select id="editOwner" class="form-control">
|
||||||
|
<option value="">— no change —</option>
|
||||||
|
${ownerOptions}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-5">
|
<div class="col-5">
|
||||||
<label class="form-label" style="color:var(--muted); font-size:.85rem">Group</label>
|
<label class="form-label" style="color:var(--muted); font-size:.85rem">Group</label>
|
||||||
<input type="text" id="editGroup" class="form-control" value="${esc(share.group || '')}" placeholder="root" list="groupList">
|
<select id="editGroup" class="form-control">
|
||||||
|
<option value="">— no change —</option>
|
||||||
|
${groupOptions}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
<label class="form-label" style="color:var(--muted); font-size:.85rem">Mode</label>
|
<label class="form-label" style="color:var(--muted); font-size:.85rem">Mode</label>
|
||||||
@@ -155,10 +163,34 @@ async function openEditModal(type, share) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label" style="color:var(--muted); font-size:.85rem">Valid Users
|
<label class="form-label" style="color:var(--muted); font-size:.85rem">Valid Users
|
||||||
<span style="font-weight:400"> — use <code>@groupname</code> for a whole group</span>
|
<span style="font-weight:400"> — space or comma separated, use <code>@groupname</code> for a group</span>
|
||||||
</label>
|
</label>
|
||||||
<input type="text" id="editUsers" class="form-control" value="${esc(share.valid_users || '')}">
|
<input type="text" id="editUsers" class="form-control" value="${esc(share.valid_users || '')}">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row g-2 mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label" style="color:var(--muted); font-size:.85rem">Force User
|
||||||
|
<span style="font-weight:400"> — all writes appear as this user</span>
|
||||||
|
</label>
|
||||||
|
<select id="editForceUser" class="form-control">
|
||||||
|
<option value="">— none —</option>
|
||||||
|
${ownerOptions}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label" style="color:var(--muted); font-size:.85rem">Force Group
|
||||||
|
<span style="font-weight:400"> — all writes use this group</span>
|
||||||
|
</label>
|
||||||
|
<select id="editForceGroup" class="form-control">
|
||||||
|
<option value="">— none —</option>
|
||||||
|
${groupOptions}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-check ms-1 mb-3">
|
||||||
|
<input type="checkbox" id="editGuestOk" class="form-check-input" ${share.guest_ok ? 'checked' : ''}>
|
||||||
|
<label class="form-check-label" for="editGuestOk" style="color:var(--muted)">Guest OK — allow access without a password</label>
|
||||||
|
</div>
|
||||||
${permSection}`;
|
${permSection}`;
|
||||||
} else {
|
} else {
|
||||||
const isRo = /\(ro[,)]/.test(share.clients);
|
const isRo = /\(ro[,)]/.test(share.clients);
|
||||||
@@ -172,10 +204,18 @@ async function openEditModal(type, share) {
|
|||||||
<label class="form-label" style="color:var(--muted); font-size:.85rem">Clients</label>
|
<label class="form-label" style="color:var(--muted); font-size:.85rem">Clients</label>
|
||||||
<input type="text" id="editClients" class="form-control" value="${esc(cleanClients)}">
|
<input type="text" id="editClients" class="form-control" value="${esc(cleanClients)}">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check ms-1 mt-2 mb-2">
|
<div class="form-check ms-1 mt-2 mb-1">
|
||||||
<input type="checkbox" id="editReadOnly" class="form-check-input" ${isRo ? 'checked' : ''}>
|
<input type="checkbox" id="editReadOnly" class="form-check-input" ${isRo ? 'checked' : ''}>
|
||||||
<label class="form-check-label" for="editReadOnly" style="color:var(--muted)">Read only</label>
|
<label class="form-check-label" for="editReadOnly" style="color:var(--muted)">Read only</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-check ms-1 mb-1">
|
||||||
|
<input type="checkbox" id="editNoRootSquash" class="form-check-input" ${share.no_root_squash ? 'checked' : ''}>
|
||||||
|
<label class="form-check-label" for="editNoRootSquash" style="color:var(--muted)">no_root_squash — root on client keeps root access on server</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check ms-1 mb-3">
|
||||||
|
<input type="checkbox" id="editAllSquash" class="form-check-input" ${share.all_squash ? 'checked' : ''}>
|
||||||
|
<label class="form-check-label" for="editAllSquash" style="color:var(--muted)">all_squash — map all clients to anonymous user (anonuid)</label>
|
||||||
|
</div>
|
||||||
${permSection}`;
|
${permSection}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,19 +228,24 @@ async function submitEdit() {
|
|||||||
|
|
||||||
let res;
|
let res;
|
||||||
if (ctx.type === 'smb') {
|
if (ctx.type === 'smb') {
|
||||||
const name = document.getElementById('editName').value.trim();
|
const name = document.getElementById('editName').value.trim();
|
||||||
const path = document.getElementById('editPath').value.trim();
|
const path = document.getElementById('editPath').value.trim();
|
||||||
const validUsers = document.getElementById('editUsers').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)}`, {
|
res = await apiFetch(`/api/share/smb/${encodeURIComponent(ctx.original.name)}`, {
|
||||||
method: 'PATCH', headers: {'Content-Type': 'application/json'},
|
method: 'PATCH', headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify({name, path, validUsers}),
|
body: JSON.stringify({name, path, validUsers, forceUser, forceGroup, guestOk}),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const clients = document.getElementById('editClients').value.trim();
|
const clients = document.getElementById('editClients').value.trim();
|
||||||
const readOnly = document.getElementById('editReadOnly').checked;
|
const readOnly = document.getElementById('editReadOnly').checked;
|
||||||
|
const noRootSquash = document.getElementById('editNoRootSquash').checked;
|
||||||
|
const allSquash = document.getElementById('editAllSquash').checked;
|
||||||
res = await apiFetch('/api/share/nfs', {
|
res = await apiFetch('/api/share/nfs', {
|
||||||
method: 'PATCH', headers: {'Content-Type': 'application/json'},
|
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}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user