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

+57 -5
View File
@@ -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 = `
<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="row g-2 mb-3">
<div class="col-5">
<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">
</div>
<div class="col-5">
<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">
</div>
<div class="col-2">
<label class="form-label" style="color:var(--muted); font-size:.85rem">Mode</label>
<input type="text" id="editMode" class="form-control" value="${esc(share.mode || '')}" placeholder="770" maxlength="4">
</div>
</div>
<div style="font-size:.8rem; color:var(--muted)">
Common: <code>770</code> owner+group rw &nbsp;·&nbsp;
<code>750</code> group read-only &nbsp;·&nbsp;
<code>755</code> world read &nbsp;·&nbsp;
Use <code>@groupname</code> in Valid Users to allow a whole group.
</div>`;
let body = '';
if (type === 'smb') {
body = `
@@ -117,9 +141,12 @@ function openEditModal(type, share) {
<input type="text" id="editPath" class="form-control" value="${esc(share.path)}">
</div>
<div class="mb-3">
<label class="form-label" style="color:var(--muted); font-size:.85rem">Valid Users</label>
<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>
</label>
<input type="text" id="editUsers" class="form-control" value="${esc(share.valid_users || '')}">
</div>`;
</div>
${permSection}`;
} else {
const isRo = /\(ro[,)]/.test(share.clients);
const cleanClients = share.clients.replace(/\s*\(.*?\)/g, '').trim();
@@ -132,10 +159,11 @@ function openEditModal(type, share) {
<label class="form-label" style="color:var(--muted); font-size:.85rem">Clients</label>
<input type="text" id="editClients" class="form-control" value="${esc(cleanClients)}">
</div>
<div class="form-check ms-1 mt-2">
<div class="form-check ms-1 mt-2 mb-2">
<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>
</div>`;
</div>
${permSection}`;
}
document.getElementById('editModalBody').innerHTML = body;
@@ -166,8 +194,32 @@ async function submitEdit() {
if (!res) return;
const data = await res.json();
if (!res.ok) {
bootstrap.Modal.getInstance(document.getElementById('editModal')).hide();
return showToast('Error: ' + (data.error || res.status), 'danger');
}
// Apply permissions if any field is filled
const owner = document.getElementById('editOwner')?.value.trim();
const group = document.getElementById('editGroup')?.value.trim();
const mode = document.getElementById('editMode')?.value.trim();
const permPath = ctx.type === 'smb'
? document.getElementById('editPath')?.value.trim()
: ctx.original.path;
if (owner || group || mode) {
const pr = await apiFetch('/api/permissions', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({path: permPath, owner, group, mode}),
});
if (pr && !pr.ok) {
const pd = await pr.json();
bootstrap.Modal.getInstance(document.getElementById('editModal')).hide();
return showToast('Permission error: ' + (pd.error || pr.status), 'danger');
}
}
bootstrap.Modal.getInstance(document.getElementById('editModal')).hide();
if (!res.ok) return showToast('Error: ' + (data.error || res.status), 'danger');
showToast('Share updated.');
loadShares(); loadDashboard();
}