Add group management and owner/group dropdowns in permission editor

Backend:
- _list_groups(): reads /etc/group, returns groups with gid >= 1000
  plus a small whitelist (root, staff, sambashare, www-data)
- GET/POST /api/groups: list and create groups (groupadd)
- DELETE /api/groups/<name>: delete group (groupdel)
- POST /api/groups/<name>/members: add user to group (gpasswd -a)
- DELETE /api/groups/<name>/members/<user>: remove user (gpasswd -d)

Frontend:
- Users tab now has a Groups section: table with GID, member badges
  with × remove buttons, per-row "add member" select populated with
  users not already in the group, delete group button
- loadUsers() caches result in _cachedUsers for groups table to use
- openEditModal is now async; fetches /api/users + /api/groups before
  rendering so Owner and Group inputs have datalist autocomplete

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Sonnet 4.6 committed 2026-06-20 00:47:43 +02:00
1 parent 302a5cb023
commit 596365b805
5 files changed
+227 -11

No files matched your search

+77
View File
@@ -475,6 +475,83 @@ def set_permissions():
return jsonify({'status': 'ok', 'stat': _path_stat(safe_path)})
# ===== GROUPS =====
_GROUP_WHITELIST = {'root', 'staff', 'sambashare', 'www-data'}
def _list_groups():
groups = []
with open('/etc/group', 'r') as f:
for line in f:
parts = line.strip().split(':')
if len(parts) < 4:
continue
name, gid_str, members_str = parts[0], parts[2], parts[3]
try:
gid = int(gid_str)
except ValueError:
continue
members = [m for m in members_str.split(',') if m]
if gid >= 1000 or name in _GROUP_WHITELIST:
groups.append({'name': name, 'gid': gid, 'members': members})
return sorted(groups, key=lambda g: g['name'])
@app.route('/api/groups', methods=['GET'])
@require_auth
def list_groups():
return jsonify(_list_groups())
@app.route('/api/groups', methods=['POST'])
@require_auth
def create_group():
name = (request.json or {}).get('name', '')
if not VALID_NAME.match(name):
return jsonify({'error': 'invalid group name'}), 400
r = subprocess.run(['groupadd', name], capture_output=True, text=True)
if r.returncode != 0:
return jsonify({'error': r.stderr.strip() or 'groupadd failed'}), 400
return jsonify({'status': 'created'})
@app.route('/api/groups/<name>', methods=['DELETE'])
@require_auth
def delete_group(name):
if not VALID_NAME.match(name):
return jsonify({'error': 'invalid group name'}), 400
r = subprocess.run(['groupdel', name], capture_output=True, text=True)
if r.returncode != 0:
return jsonify({'error': r.stderr.strip() or 'groupdel failed'}), 500
return jsonify({'status': 'deleted'})
@app.route('/api/groups/<name>/members', methods=['POST'])
@require_auth
def add_group_member(name):
if not VALID_NAME.match(name):
return jsonify({'error': 'invalid group name'}), 400
user = (request.json or {}).get('user', '')
if not VALID_NAME.match(user):
return jsonify({'error': 'invalid username'}), 400
r = subprocess.run(['gpasswd', '-a', user, name], capture_output=True, text=True)
if r.returncode != 0:
return jsonify({'error': r.stderr.strip() or 'gpasswd failed'}), 500
return jsonify({'status': 'added'})
@app.route('/api/groups/<name>/members/<user>', methods=['DELETE'])
@require_auth
def remove_group_member(name, user):
if not VALID_NAME.match(name) or not VALID_NAME.match(user):
return jsonify({'error': 'invalid name'}), 400
r = subprocess.run(['gpasswd', '-d', user, name], capture_output=True, text=True)
if r.returncode != 0:
return jsonify({'error': r.stderr.strip() or 'gpasswd failed'}), 500
return jsonify({'status': 'removed'})
# ===== USERS =====
def _smb_users():