feat(auth): enable Basic Auth as default authentication method
- Initialize authentication system on Flask app startup - Default to Basic Auth if no [authentication] section in config - Support TISBACKUP_AUTH_USERNAME and TISBACKUP_AUTH_PASSWORD env vars - Generate secure random password if not configured with warning - Protect all Flask routes with @auth.require_auth decorator - Fallback to 'none' auth provider on initialization errors Routes protected: - / (backup_all) - /config_number/ (set_config_number) - /all_json (backup_all_json) - /json (backup_json) - /status.json (export_backup_status) - /backups.json (last_backup_json) - /last_backups (last_backup) - /export_backup (export_backup) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
f12d89f3da
commit
e6ee91babf
@ -40,6 +40,7 @@ from iniparse import ConfigParser, RawConfigParser
|
|||||||
|
|
||||||
from config import huey
|
from config import huey
|
||||||
from libtisbackup.common import *
|
from libtisbackup.common import *
|
||||||
|
from libtisbackup.auth import get_auth_provider
|
||||||
from tasks import get_task, run_export_backup, set_task
|
from tasks import get_task, run_export_backup, set_task
|
||||||
from tisbackup import tis_backup
|
from tisbackup import tis_backup
|
||||||
|
|
||||||
@ -79,6 +80,69 @@ if not SECRET_KEY:
|
|||||||
app.secret_key = SECRET_KEY
|
app.secret_key = SECRET_KEY
|
||||||
app.config["PROPAGATE_EXCEPTIONS"] = True
|
app.config["PROPAGATE_EXCEPTIONS"] = True
|
||||||
|
|
||||||
|
# Initialize authentication
|
||||||
|
auth_config = {}
|
||||||
|
try:
|
||||||
|
# Read authentication config from tisbackup_gui.ini
|
||||||
|
cp_gui = ConfigParser()
|
||||||
|
cp_gui.read("/etc/tis/tisbackup_gui.ini")
|
||||||
|
|
||||||
|
if cp_gui.has_section("authentication"):
|
||||||
|
auth_type = cp_gui.get("authentication", "type", fallback="basic")
|
||||||
|
|
||||||
|
# Load auth provider config
|
||||||
|
for key, value in cp_gui.items("authentication"):
|
||||||
|
if key != "type":
|
||||||
|
auth_config[key] = value
|
||||||
|
else:
|
||||||
|
# Default to Basic Auth if no config section
|
||||||
|
auth_type = "basic"
|
||||||
|
|
||||||
|
# Get credentials from environment or use defaults
|
||||||
|
default_username = os.environ.get("TISBACKUP_AUTH_USERNAME", "admin")
|
||||||
|
default_password = os.environ.get("TISBACKUP_AUTH_PASSWORD")
|
||||||
|
|
||||||
|
if not default_password:
|
||||||
|
# Generate random password if not set
|
||||||
|
import secrets
|
||||||
|
default_password = secrets.token_urlsafe(16)
|
||||||
|
logging.warning(
|
||||||
|
f"TISBACKUP_AUTH_PASSWORD not set. Generated temporary password for user '{default_username}': {default_password}"
|
||||||
|
)
|
||||||
|
logging.warning(
|
||||||
|
"Set TISBACKUP_AUTH_USERNAME and TISBACKUP_AUTH_PASSWORD environment variables, "
|
||||||
|
"or add [authentication] section to tisbackup_gui.ini for production use."
|
||||||
|
)
|
||||||
|
|
||||||
|
auth_config = {
|
||||||
|
"username": default_username,
|
||||||
|
"password": default_password,
|
||||||
|
"use_bcrypt": False, # Plain text for auto-generated password
|
||||||
|
"realm": "TISBackup"
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Fallback to basic auth on error
|
||||||
|
logging.error(f"Error loading authentication config: {e}. Using default Basic Auth.")
|
||||||
|
auth_type = "basic"
|
||||||
|
auth_config = {
|
||||||
|
"username": os.environ.get("TISBACKUP_AUTH_USERNAME", "admin"),
|
||||||
|
"password": os.environ.get("TISBACKUP_AUTH_PASSWORD", "changeme"),
|
||||||
|
"use_bcrypt": False,
|
||||||
|
"realm": "TISBackup"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Initialize auth provider
|
||||||
|
try:
|
||||||
|
auth = get_auth_provider(auth_type, auth_config)
|
||||||
|
auth.init_app(app)
|
||||||
|
logging.info(f"Authentication initialized: {auth_type}")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to initialize authentication: {e}")
|
||||||
|
# Fallback to no auth
|
||||||
|
auth = get_auth_provider("none", {})
|
||||||
|
logging.warning("Authentication disabled due to initialization error")
|
||||||
|
|
||||||
tasks_db = os.path.join(tisbackup_root_dir, "tasks.sqlite")
|
tasks_db = os.path.join(tisbackup_root_dir, "tasks.sqlite")
|
||||||
|
|
||||||
|
|
||||||
@ -263,6 +327,7 @@ def read_config():
|
|||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
|
@auth.require_auth
|
||||||
def backup_all():
|
def backup_all():
|
||||||
backup_dict = read_config()
|
backup_dict = read_config()
|
||||||
return render_template("backups.html", backup_list=backup_dict)
|
return render_template("backups.html", backup_list=backup_dict)
|
||||||
@ -270,6 +335,7 @@ def backup_all():
|
|||||||
|
|
||||||
@app.route("/config_number/")
|
@app.route("/config_number/")
|
||||||
@app.route("/config_number/<int:id>")
|
@app.route("/config_number/<int:id>")
|
||||||
|
@auth.require_auth
|
||||||
def set_config_number(id=None):
|
def set_config_number(id=None):
|
||||||
if id is not None and len(CONFIG) > id:
|
if id is not None and len(CONFIG) > id:
|
||||||
global config_number
|
global config_number
|
||||||
@ -279,6 +345,7 @@ def set_config_number(id=None):
|
|||||||
|
|
||||||
|
|
||||||
@app.route("/all_json")
|
@app.route("/all_json")
|
||||||
|
@auth.require_auth
|
||||||
def backup_all_json():
|
def backup_all_json():
|
||||||
backup_dict = read_all_configs(BASE_DIR)
|
backup_dict = read_all_configs(BASE_DIR)
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
@ -295,6 +362,7 @@ def backup_all_json():
|
|||||||
|
|
||||||
|
|
||||||
@app.route("/json")
|
@app.route("/json")
|
||||||
|
@auth.require_auth
|
||||||
def backup_json():
|
def backup_json():
|
||||||
backup_dict = read_config()
|
backup_dict = read_config()
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
@ -483,6 +551,7 @@ def check_mount_disk(partition_name, refresh):
|
|||||||
|
|
||||||
|
|
||||||
@app.route("/status.json")
|
@app.route("/status.json")
|
||||||
|
@auth.require_auth
|
||||||
def export_backup_status():
|
def export_backup_status():
|
||||||
exports = dbstat.query('select * from stats where TYPE="EXPORT" and backup_start>="%s"' % mindate)
|
exports = dbstat.query('select * from stats where TYPE="EXPORT" and backup_start>="%s"' % mindate)
|
||||||
error = ""
|
error = ""
|
||||||
@ -503,18 +572,21 @@ def runnings_backups():
|
|||||||
|
|
||||||
|
|
||||||
@app.route("/backups.json")
|
@app.route("/backups.json")
|
||||||
|
@auth.require_auth
|
||||||
def last_backup_json():
|
def last_backup_json():
|
||||||
exports = dbstat.query('select * from stats where TYPE="BACKUP" ORDER BY backup_start DESC ')
|
exports = dbstat.query('select * from stats where TYPE="BACKUP" ORDER BY backup_start DESC ')
|
||||||
return Response(response=json.dumps(exports), status=200, mimetype="application/json")
|
return Response(response=json.dumps(exports), status=200, mimetype="application/json")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/last_backups")
|
@app.route("/last_backups")
|
||||||
|
@auth.require_auth
|
||||||
def last_backup():
|
def last_backup():
|
||||||
exports = dbstat.query('select * from stats where TYPE="BACKUP" ORDER BY backup_start DESC LIMIT 20 ')
|
exports = dbstat.query('select * from stats where TYPE="BACKUP" ORDER BY backup_start DESC LIMIT 20 ')
|
||||||
return render_template("last_backups.html", backups=exports)
|
return render_template("last_backups.html", backups=exports)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/export_backup")
|
@app.route("/export_backup")
|
||||||
|
@auth.require_auth
|
||||||
def export_backup():
|
def export_backup():
|
||||||
raise_error("", "")
|
raise_error("", "")
|
||||||
backup_dict = read_config()
|
backup_dict = read_config()
|
||||||
|
Loading…
Reference in New Issue
Block a user