mirror of
https://github.com/ChrispyBacon-dev/DockFlare.git
synced 2026-04-28 03:39:32 +00:00
optional disable of password auth
This commit is contained in:
parent
2189e77b74
commit
6e4d1a0920
5 changed files with 148 additions and 74 deletions
18
dockflare/app/templates/auth/login_disabled.html
Normal file
18
dockflare/app/templates/auth/login_disabled.html
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="flex justify-center items-center min-h-screen">
|
||||
<div class="card w-96 bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">{{ title }}</h2>
|
||||
<p>Password login has been disabled by the administrator.</p>
|
||||
<p>Please use an alternative login method, such as a Cloudflare Access policy.</p>
|
||||
<div class="card-actions justify-end">
|
||||
<a href="{{ url_for('web.status_page') }}" class="btn btn-primary">Go to Status Page</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -287,19 +287,42 @@
|
|||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-lg mb-2">Password Reset</h3>
|
||||
<div role="alert" class="alert alert-info text-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||
<span>
|
||||
To reset your password, you must have filesystem access to the DockFlare container.
|
||||
<br>
|
||||
1. Stop the DockFlare container.
|
||||
<br>
|
||||
2. Delete the `dockflare_config.dat` and `dockflare.key` files from your persistent data volume.
|
||||
<br>
|
||||
3. Restart the container. You will be prompted to go through the initial setup again.
|
||||
</span>
|
||||
</div>
|
||||
<h3 class="font-semibold text-lg mb-2">Disable Password Login</h3>
|
||||
<form method="POST" action="{{ url_for('web.settings_page') }}" class="space-y-4 protocol-aware-form">
|
||||
{{ security_settings_form.hidden_tag() }}
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer p-0">
|
||||
<span class="label-text">{{ security_settings_form.disable_password_login.label.text }}</span>
|
||||
{{ security_settings_form.disable_password_login(class="toggle toggle-primary") }}
|
||||
</label>
|
||||
<div class="text-xs opacity-70 mt-1">{{ security_settings_form.disable_password_login.description }}</div>
|
||||
</div>
|
||||
|
||||
<div role="alert" class="alert alert-warning text-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
|
||||
<span><strong>Security Warning:</strong>When disabling password login, you become responsible for securing DockFlare access. Best practice is to use a Cloudflare Tunnel with an Access Policy and ensure Docker ports are not exposed, preventing access from the local network (LAN).</span>
|
||||
</div>
|
||||
|
||||
<div class="card-actions">
|
||||
{{ security_settings_form.submit_security_settings(class="btn btn-warning") }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8">
|
||||
<h3 class="font-semibold text-lg mb-2">Password Reset</h3>
|
||||
<div role="alert" class="alert alert-info text-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||
<span>
|
||||
To reset your password, you must have filesystem access to the DockFlare container.
|
||||
<br>
|
||||
1. Stop the DockFlare container.
|
||||
<br>
|
||||
2. Delete the `dockflare_config.dat` and `dockflare.key` files from your persistent data volume.
|
||||
<br>
|
||||
3. Restart the container. You will be prompted to go through the initial setup again.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,11 @@ class LoginForm(FlaskForm):
|
|||
@auth_bp.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
"""Handles the user login process."""
|
||||
if current_app.config.get('DISABLE_PASSWORD_LOGIN'):
|
||||
flash('Password login is disabled. Please use an alternative login method.', 'warning')
|
||||
# Still render a basic page, but without the form.
|
||||
return render_template('auth/login_disabled.html', title="Login Disabled")
|
||||
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('web.status_page'))
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
# app/web/forms.py
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import PasswordField, SubmitField, StringField, IntegerField
|
||||
from wtforms import PasswordField, SubmitField, StringField, IntegerField, BooleanField
|
||||
from wtforms.validators import DataRequired, EqualTo, Length, Optional
|
||||
|
||||
class SettingsForm(FlaskForm):
|
||||
|
|
@ -39,6 +39,14 @@ class SettingsForm(FlaskForm):
|
|||
)
|
||||
submit_settings = SubmitField('Save General Settings')
|
||||
|
||||
class SecuritySettingsForm(FlaskForm):
|
||||
"""Form for editing security settings."""
|
||||
disable_password_login = BooleanField(
|
||||
'Disable Password Login',
|
||||
description="If selected, password-based login will be disabled. Access to DockFlare will only be possible through a Cloudflare Access policy."
|
||||
)
|
||||
submit_security_settings = SubmitField('Save Security Settings')
|
||||
|
||||
class ChangePasswordForm(FlaskForm):
|
||||
"""Form for changing the user's password."""
|
||||
current_password = PasswordField(
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ def status_page():
|
|||
CF_ZONE_ID_CONFIGURED=bool(current_app.config.get('CF_ZONE_ID'))
|
||||
)
|
||||
|
||||
from app.web.forms import ChangePasswordForm, SettingsForm
|
||||
from app.web.forms import ChangePasswordForm, SecuritySettingsForm, SettingsForm
|
||||
from werkzeug.security import check_password_hash, generate_password_hash
|
||||
from cryptography.fernet import Fernet
|
||||
|
||||
|
|
@ -225,88 +225,107 @@ from cryptography.fernet import Fernet
|
|||
@login_required
|
||||
def settings_page():
|
||||
"""Renders and handles the main settings page."""
|
||||
settings_form = SettingsForm()
|
||||
settings_form = SettingsForm(prefix='general')
|
||||
change_password_form = ChangePasswordForm()
|
||||
|
||||
if settings_form.submit_settings.data and settings_form.validate_on_submit():
|
||||
data_path = os.path.dirname(config.STATE_FILE_PATH)
|
||||
key_file = os.path.join(data_path, 'dockflare.key')
|
||||
config_file = os.path.join(data_path, 'dockflare_config.dat')
|
||||
security_settings_form = SecuritySettingsForm(prefix='security')
|
||||
|
||||
try:
|
||||
with open(key_file, 'rb') as f:
|
||||
key = f.read()
|
||||
fernet = Fernet(key)
|
||||
# Distinguish between form submissions
|
||||
if request.method == 'POST':
|
||||
if settings_form.submit_settings.data and settings_form.validate():
|
||||
data_path = os.path.dirname(config.STATE_FILE_PATH)
|
||||
key_file = os.path.join(data_path, 'dockflare.key')
|
||||
config_file = os.path.join(data_path, 'dockflare_config.dat')
|
||||
|
||||
with open(config_file, 'rb') as f:
|
||||
decrypted_data = fernet.decrypt(f.read())
|
||||
config_data = json.loads(decrypted_data)
|
||||
try:
|
||||
with open(key_file, 'rb') as f:
|
||||
key = f.read()
|
||||
fernet = Fernet(key)
|
||||
|
||||
with open(config_file, 'rb') as f:
|
||||
decrypted_data = fernet.decrypt(f.read())
|
||||
config_data = json.loads(decrypted_data)
|
||||
|
||||
original_tunnel_name = config_data.get('tunnel_name')
|
||||
new_tunnel_name = settings_form.tunnel_name.data
|
||||
tunnel_name_changed = original_tunnel_name != new_tunnel_name
|
||||
original_tunnel_name = config_data.get('tunnel_name')
|
||||
new_tunnel_name = settings_form.tunnel_name.data
|
||||
tunnel_name_changed = original_tunnel_name != new_tunnel_name
|
||||
|
||||
config_data['tunnel_name'] = new_tunnel_name
|
||||
config_data['cf_zone_id'] = settings_form.cf_zone_id.data
|
||||
config_data['tunnel_dns_scan_zone_names'] = settings_form.tunnel_dns_scan_zone_names.data
|
||||
config_data['grace_period_seconds'] = settings_form.grace_period_seconds.data
|
||||
|
||||
config_data['tunnel_name'] = new_tunnel_name
|
||||
config_data['cf_zone_id'] = settings_form.cf_zone_id.data
|
||||
config_data['tunnel_dns_scan_zone_names'] = settings_form.tunnel_dns_scan_zone_names.data
|
||||
config_data['grace_period_seconds'] = settings_form.grace_period_seconds.data
|
||||
encrypted_payload = fernet.encrypt(json.dumps(config_data).encode('utf-8'))
|
||||
with open(config_file, 'wb') as f:
|
||||
f.write(encrypted_payload)
|
||||
|
||||
from app import config as config_module
|
||||
current_app.config['TUNNEL_NAME'] = new_tunnel_name
|
||||
config_module.TUNNEL_NAME = new_tunnel_name
|
||||
current_app.config['CLOUDFLARED_CONTAINER_NAME'] = f"cloudflared-agent-{new_tunnel_name}"
|
||||
config_module.CLOUDFLARED_CONTAINER_NAME = f"cloudflared-agent-{new_tunnel_name}"
|
||||
current_app.config['CF_ZONE_ID'] = config_data['cf_zone_id']
|
||||
config_module.CF_ZONE_ID = config_data['cf_zone_id']
|
||||
scan_zones_str = config_data.get('tunnel_dns_scan_zone_names', '')
|
||||
current_app.config['TUNNEL_DNS_SCAN_ZONE_NAMES'] = [name.strip() for name in scan_zones_str.split(',') if name.strip()]
|
||||
config_module.TUNNEL_DNS_SCAN_ZONE_NAMES = current_app.config['TUNNEL_DNS_SCAN_ZONE_NAMES']
|
||||
current_app.config['GRACE_PERIOD_SECONDS'] = int(config_data.get('grace_period_seconds', 28800))
|
||||
config_module.GRACE_PERIOD_SECONDS = current_app.config['GRACE_PERIOD_SECONDS']
|
||||
|
||||
encrypted_payload = fernet.encrypt(json.dumps(config_data).encode('utf-8'))
|
||||
with open(config_file, 'wb') as f:
|
||||
f.write(encrypted_payload)
|
||||
flash('General settings updated successfully.', 'success')
|
||||
|
||||
if tunnel_name_changed and not config.USE_EXTERNAL_CLOUDFLARED:
|
||||
flash('Tunnel name changed. Restarting the agent to apply changes...', 'info')
|
||||
logging.info(f"Tunnel name changed from '{original_tunnel_name}' to '{new_tunnel_name}'. Triggering agent restart.")
|
||||
|
||||
def restart_agent_task():
|
||||
stop_cloudflared_container()
|
||||
time.sleep(5)
|
||||
initialize_tunnel()
|
||||
start_cloudflared_container()
|
||||
|
||||
from app import config as config_module
|
||||
current_app.config['TUNNEL_NAME'] = new_tunnel_name
|
||||
config_module.TUNNEL_NAME = new_tunnel_name
|
||||
current_app.config['CLOUDFLARED_CONTAINER_NAME'] = f"cloudflared-agent-{new_tunnel_name}"
|
||||
config_module.CLOUDFLARED_CONTAINER_NAME = f"cloudflared-agent-{new_tunnel_name}"
|
||||
from threading import Thread
|
||||
restart_thread = Thread(target=restart_agent_task)
|
||||
restart_thread.start()
|
||||
|
||||
current_app.config['CF_ZONE_ID'] = config_data['cf_zone_id']
|
||||
config_module.CF_ZONE_ID = config_data['cf_zone_id']
|
||||
|
||||
scan_zones_str = config_data.get('tunnel_dns_scan_zone_names', '')
|
||||
current_app.config['TUNNEL_DNS_SCAN_ZONE_NAMES'] = [name.strip() for name in scan_zones_str.split(',') if name.strip()]
|
||||
config_module.TUNNEL_DNS_SCAN_ZONE_NAMES = current_app.config['TUNNEL_DNS_SCAN_ZONE_NAMES']
|
||||
return redirect(url_for('web.settings_page'))
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to update settings in config file: {e}", exc_info=True)
|
||||
flash('An error occurred while saving settings.', 'danger')
|
||||
|
||||
elif security_settings_form.submit_security_settings.data and security_settings_form.validate():
|
||||
data_path = os.path.dirname(config.STATE_FILE_PATH)
|
||||
key_file = os.path.join(data_path, 'dockflare.key')
|
||||
config_file = os.path.join(data_path, 'dockflare_config.dat')
|
||||
try:
|
||||
with open(key_file, 'rb') as f:
|
||||
key = f.read()
|
||||
fernet = Fernet(key)
|
||||
|
||||
current_app.config['GRACE_PERIOD_SECONDS'] = int(config_data.get('grace_period_seconds', 28800))
|
||||
config_module.GRACE_PERIOD_SECONDS = current_app.config['GRACE_PERIOD_SECONDS']
|
||||
with open(config_file, 'rb') as f:
|
||||
decrypted_data = fernet.decrypt(f.read())
|
||||
config_data = json.loads(decrypted_data)
|
||||
|
||||
flash('General settings updated successfully.', 'success')
|
||||
config_data['disable_password_login'] = security_settings_form.disable_password_login.data
|
||||
|
||||
encrypted_payload = fernet.encrypt(json.dumps(config_data).encode('utf-8'))
|
||||
with open(config_file, 'wb') as f:
|
||||
f.write(encrypted_payload)
|
||||
|
||||
if tunnel_name_changed and not config.USE_EXTERNAL_CLOUDFLARED:
|
||||
flash('Tunnel name changed. Restarting the agent to apply changes...', 'info')
|
||||
logging.info(f"Tunnel name changed from '{original_tunnel_name}' to '{new_tunnel_name}'. Triggering agent restart.")
|
||||
current_app.config['DISABLE_PASSWORD_LOGIN'] = config_data['disable_password_login']
|
||||
|
||||
flash('Security settings updated successfully.', 'success')
|
||||
return redirect(url_for('web.settings_page'))
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to update security settings in config file: {e}", exc_info=True)
|
||||
flash('An error occurred while saving security settings.', 'danger')
|
||||
|
||||
def restart_agent_task():
|
||||
stop_cloudflared_container()
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
initialize_tunnel()
|
||||
start_cloudflared_container()
|
||||
|
||||
from threading import Thread
|
||||
restart_thread = Thread(target=restart_agent_task)
|
||||
restart_thread.start()
|
||||
|
||||
return redirect(url_for('web.settings_page'))
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to update settings in config file: {e}", exc_info=True)
|
||||
flash('An error occurred while saving settings.', 'danger')
|
||||
|
||||
|
||||
# Populate forms for GET request
|
||||
if request.method == 'GET':
|
||||
settings_form.tunnel_name.data = current_app.config.get('TUNNEL_NAME')
|
||||
settings_form.cf_zone_id.data = current_app.config.get('CF_ZONE_ID')
|
||||
settings_form.tunnel_dns_scan_zone_names.data = ','.join(current_app.config.get('TUNNEL_DNS_SCAN_ZONE_NAMES', []))
|
||||
settings_form.grace_period_seconds.data = current_app.config.get('GRACE_PERIOD_SECONDS')
|
||||
|
||||
security_settings_form.disable_password_login.data = current_app.config.get('DISABLE_PASSWORD_LOGIN', False)
|
||||
|
||||
groups_for_template = {}
|
||||
used_group_ids = set()
|
||||
|
|
@ -330,6 +349,7 @@ def settings_page():
|
|||
'settings.html',
|
||||
settings_form=settings_form,
|
||||
change_password_form=change_password_form,
|
||||
security_settings_form=security_settings_form,
|
||||
access_groups=groups_for_template,
|
||||
used_group_ids=used_group_ids,
|
||||
all_account_tunnels=all_account_tunnels_list,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue