diff --git a/webgui/app.py b/webgui/app.py index 31a9387..ad4ea02 100644 --- a/webgui/app.py +++ b/webgui/app.py @@ -129,6 +129,73 @@ def htpasswd_users(): with open(HTPASSWD) as f: return [line.split(':')[0] for line in f if ':' in line] +# ── Basic Auth prekidac ─────────────────────────────────────────────────────── + +# Lokacije kojima se Basic Auth moze zasebno ukljuciti/iskljuciti. +BASICAUTH_LOCATIONS = { + '/guacamole/': 'Guacamole', + '/guacadmin/': 'Web GUI', +} + +_AUTH_LINE = re.compile( + r'^(?P[ \t]*)(?P#[ \t]*)?' + r'(?Pauth_basic(?:_user_file)?[ \t]+[^\n]*)$' +) + + +def _location_span(txt, location): + """Vraca (start, end) tijela location bloka, ili None ako ga nema.""" + m = re.search(r'^[ \t]*location[ \t]+' + re.escape(location) + r'[ \t]*\{', + txt, re.MULTILINE) + if not m: + return None + depth, i = 1, m.end() + while i < len(txt) and depth: + if txt[i] == '{': + depth += 1 + elif txt[i] == '}': + depth -= 1 + i += 1 + return (m.end(), i - 1) + + +def basicauth_state(txt=None): + """Po lokaciji: True aktivan, False zakomentiran, None nema direktive.""" + if txt is None: + conf = get_nginx_conf() + if not conf: + return {loc: None for loc in BASICAUTH_LOCATIONS} + with open(conf) as f: + txt = f.read() + state = {} + for loc in BASICAUTH_LOCATIONS: + span = _location_span(txt, loc) + if not span: + state[loc] = None + continue + body = txt[span[0]:span[1]] + found = [m for m in (_AUTH_LINE.match(l) for l in body.split('\n')) if m] + state[loc] = None if not found else not any(m.group('off') for m in found) + return state + + +def set_basicauth(txt, location, enabled): + span = _location_span(txt, location) + if not span: + raise ValueError('Lokacija %s nije pronadena u nginx konfiguraciji' % location) + head, body, tail = txt[:span[0]], txt[span[0]:span[1]], txt[span[1]:] + out = [] + for line in body.split('\n'): + m = _AUTH_LINE.match(line) + if m: + if enabled: + line = m.group('indent') + m.group('directive') + elif not m.group('off'): + line = m.group('indent') + '# ' + m.group('directive') + out.append(line) + return head + '\n'.join(out) + tail + + def ssl_active(): conf = get_nginx_conf() if conf and os.path.exists(conf): @@ -192,7 +259,9 @@ def dashboard(): @app.route('/basicauth') @login_required def basicauth(): - return render_template('basicauth.html', users=htpasswd_users()) + return render_template('basicauth.html', users=htpasswd_users(), + auth_state=basicauth_state(), + auth_locations=BASICAUTH_LOCATIONS) @app.route('/geoip') @login_required @@ -300,11 +369,57 @@ def api_ba_update(): @api_auth def api_ba_delete(): user = request.json.get('username', '').strip() + # Prazan htpasswd uz aktivan auth_basic znaci da nitko vise ne moze + # proci — ni kroz Guacamole ni kroz ovaj panel. Vadilo bi se preko SSH-a. + remaining = [u for u in htpasswd_users() if u != user] + if not remaining and any(basicauth_state().values()): + return jsonify(ok=False, msg='Ovo je zadnji korisnik, a Basic Auth je ' + 'jos aktivan — prvo ga iskljucite') ok, out = run(['htpasswd', '-D', HTPASSWD, user]) if ok: nginx_reload() return jsonify(ok=ok, msg=out or 'Korisnik uklonjen') +@app.route('/api/basicauth/toggle', methods=['POST']) +@api_auth +def api_ba_toggle(): + location = request.json.get('location', '') + enabled = bool(request.json.get('enabled')) + if location not in BASICAUTH_LOCATIONS: + return jsonify(ok=False, msg='Nepoznata lokacija') + + conf = get_nginx_conf() + if not conf: + return jsonify(ok=False, msg='Nginx konfiguracija nije pronadena') + + if enabled and not htpasswd_users(): + return jsonify(ok=False, msg='Nema nijednog korisnika — prvo dodajte ' + 'korisnika, inace nitko ne moze proci') + + with open(conf) as f: + original = f.read() + try: + updated = set_basicauth(original, location, enabled) + except ValueError as e: + return jsonify(ok=False, msg=str(e)) + + with open(conf, 'w') as f: + f.write(updated) + + # Ako nginx odbije novu konfiguraciju, vrati staru da server ostane ziv. + ok, out = nginx_reload() + if not ok: + with open(conf, 'w') as f: + f.write(original) + return jsonify(ok=False, msg='Nginx je odbio konfiguraciju, vraceno ' + 'na staro: ' + out) + + what = BASICAUTH_LOCATIONS[location] + return jsonify(ok=True, state=basicauth_state(), + msg='Basic Auth %s za %s' % + ('ukljucen' if enabled else 'iskljucen', what)) + + # ── API – GeoIP ─────────────────────────────────────────────────────────────── def _write_geoip_conf(countries): diff --git a/webgui/templates/basicauth.html b/webgui/templates/basicauth.html index 49ba088..913ac05 100644 --- a/webgui/templates/basicauth.html +++ b/webgui/templates/basicauth.html @@ -4,6 +4,40 @@ {% block content %}

Basic Auth korisnici

+
+
+ Basic Auth zaštita +
+
+ {% for loc, label in auth_locations.items() %} +
+
+
{{ label }}
+ {{ loc }} +
+
+ {% if auth_state[loc] is none %} + nije konfiguriran + {% else %} + + {% endif %} +
+
+ {% endfor %} +
+ + Isključite li zaštitu, na toj lokaciji nestaje prvi sloj obrane i + fail2ban jail nginx-basicauth ostaje bez posla. + Za /guacadmin/ to znači da je ovaj panel izložen samo + s vlastitom prijavom. +
+
+
+
@@ -112,6 +146,21 @@ async function updateUser() { showToast(r.msg, r.ok); } +async function toggleAuth(location, el) { + const enabled = el.checked; + if (!enabled && !confirm( + `Isključiti Basic Auth za ${location}?\n\n` + + `Nakon toga se toj lokaciji pristupa bez prompta.`)) { + el.checked = true; + return; + } + el.disabled = true; + const r = await api('/api/basicauth/toggle', {location, enabled}); + showToast(r.msg, r.ok); + if (!r.ok) el.checked = !enabled; // odbijeno — vrati prekidac + el.disabled = false; +} + async function deleteUser(username) { if (!confirm(`Ukloniti korisnika "${username}"?`)) return; const r = await api('/api/basicauth/delete', {username});