mirror of
https://github.com/akuker/RASCSI.git
synced 2024-11-26 13:49:21 +00:00
User authentication in the Web Interface (#483)
* Add flask-login library * Add simplepam lib to requirements * We don't use flask-login after all * User authentication in the web app using simplepam * Allow only users in the sudo group to log in * Tweak string * This way to enforce authenticated state doesn't work here * Open links to github in new tab * Disallow uploads when not authenticated * Check for the rascsi group on the system to enable webapp auth. Allow only users in the rascsi group to authenticate. * Make the AUTH_GROUP a global constant. * Add easyinstall option for web interface auth * Make AUTH_GROUP a constant * More accurate change scope
This commit is contained in:
parent
1ad1242fad
commit
5346d110a9
@ -700,6 +700,21 @@ function notifyBackup {
|
||||
fi
|
||||
}
|
||||
|
||||
# Creates the group and modifies current user for Web Interface auth
|
||||
function enableWebInterfaceAuth {
|
||||
AUTH_GROUP="rascsi"
|
||||
|
||||
if [ $(getent group "$AUTH_GROUP") ]; then
|
||||
echo "The '$AUTH_GROUP' group already exists."
|
||||
else
|
||||
echo "Creating the '$AUTH_GROUP' group."
|
||||
sudo groupadd "$AUTH_GROUP"
|
||||
fi
|
||||
|
||||
echo "Adding user '$USER' to the '$AUTH_GROUP' group."
|
||||
sudo usermod -a -G "$AUTH_GROUP" "$USER"
|
||||
}
|
||||
|
||||
# Executes the keyword driven scripts for a particular action in the main menu
|
||||
function runChoice() {
|
||||
case $1 in
|
||||
@ -855,6 +870,15 @@ function runChoice() {
|
||||
echo "Configuring RaSCSI Web Interface stand-alone - Complete!"
|
||||
echo "Launch the Web Interface with the 'start.sh' script. To use a custom port for the web server: 'start.sh --port=8081"
|
||||
;;
|
||||
12)
|
||||
echo "Enabling authentication for the RaSCSI Web Interface"
|
||||
echo "This script will make the following changes to your system:"
|
||||
echo "- Modify users and user groups"
|
||||
sudoCheck
|
||||
enableWebInterfaceAuth
|
||||
echo "Enabling authentication for the RaSCSI Web Interface - Complete!"
|
||||
echo "Use the credentials for user '$USER' to log in to the Web Interface."
|
||||
;;
|
||||
-h|--help|h|help)
|
||||
showMenu
|
||||
;;
|
||||
@ -868,7 +892,7 @@ function runChoice() {
|
||||
function readChoice() {
|
||||
choice=-1
|
||||
|
||||
until [ $choice -ge "0" ] && [ $choice -le "11" ]; do
|
||||
until [ $choice -ge "0" ] && [ $choice -le "12" ]; do
|
||||
echo -n "Enter your choice (0-9) or CTRL-C to exit: "
|
||||
read -r choice
|
||||
done
|
||||
@ -897,6 +921,7 @@ function showMenu() {
|
||||
echo "ADVANCED OPTIONS"
|
||||
echo " 10) compile and install RaSCSI stand-alone"
|
||||
echo " 11) configure the RaSCSI Web Interface stand-alone"
|
||||
echo " 12) enable authentication for the RaSCSI Web Interface"
|
||||
}
|
||||
|
||||
# parse arguments passed to the script
|
||||
@ -919,7 +944,7 @@ while [ "$1" != "" ]; do
|
||||
;;
|
||||
esac
|
||||
case $VALUE in
|
||||
FULLSPEC | STANDARD | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11)
|
||||
FULLSPEC | STANDARD | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12)
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: unknown option \"$VALUE\""
|
||||
|
@ -3,6 +3,7 @@ Module for methods controlling and getting information about the Pi's Linux syst
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
from settings import AUTH_GROUP
|
||||
|
||||
|
||||
def systemd_service(service, action):
|
||||
@ -115,4 +116,20 @@ def introspect_file(file_path, re_term):
|
||||
for line in ifile:
|
||||
if match(re_term, line):
|
||||
return True
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
def auth_active():
|
||||
"""
|
||||
Inspects if the group defined in AUTH_GROUP exists on the system.
|
||||
If it exists, tell the webapp to enable authentication.
|
||||
Returns a (dict) with (bool) status and (str) msg
|
||||
"""
|
||||
from grp import getgrall
|
||||
groups = [g.gr_name for g in getgrall()]
|
||||
if AUTH_GROUP in groups:
|
||||
return {
|
||||
"status": True,
|
||||
"msg": "You must log in to use this function!",
|
||||
}
|
||||
return {"status": False, "msg": ""}
|
||||
|
@ -6,3 +6,4 @@ Jinja2==3.0.1
|
||||
MarkupSafe==2.0.1
|
||||
protobuf==3.17.3
|
||||
requests==2.26.0
|
||||
simplepam==0.1.5
|
||||
|
@ -27,3 +27,6 @@ REMOVABLE_DEVICE_TYPES = ("SCCD", "SCRM", "SCMO")
|
||||
# The RESERVATIONS list is used to keep track of the reserved ID memos.
|
||||
# Initialize with a list of 8 empty strings.
|
||||
RESERVATIONS = ["" for x in range(0, 8)]
|
||||
|
||||
# The user group that is used for webapp authentication
|
||||
AUTH_GROUP = "rascsi"
|
||||
|
@ -31,12 +31,27 @@
|
||||
<body>
|
||||
<div class="content">
|
||||
<div class="header">
|
||||
<span style="display: inline-block; width: 100%; color: white; background-color: green; text-align: center; vertical-align: center; font-family: Arial, Helvetica, sans-serif;">Service Running</span>
|
||||
{% if auth_active %}
|
||||
{% if username %}
|
||||
<span style="display: inline-block; width: 100%; color: white; background-color: green; text-align: center; vertical-align: center; font-family: Arial, Helvetica, sans-serif;">Logged in as <em>{{ username }}</em> – <a href="/logout">Log Out</a></span>
|
||||
{% else %}
|
||||
<span style="display: inline-block; width: 100%; color: white; background-color: red; text-align: center; vertical-align: center; font-family: Arial, Helvetica, sans-serif;">
|
||||
<form method="POST" action="/login">
|
||||
<div>Log In to Use Web Interface</div>
|
||||
<input type="text" name="username" placeholder="Username">
|
||||
<input type="password" name="password" placeholder="Password">
|
||||
<input type="submit" value="Login">
|
||||
</form>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span style="display: inline-block; width: 100%; color: white; background-color: green; text-align: center; vertical-align: center; font-family: Arial, Helvetica, sans-serif;">Web Interface Authentication Disabled – See <a href="https://github.com/akuker/RASCSI/wiki/Web-Interface#Security_Notice" target="_blank">Wiki</a> for more information</span>
|
||||
{% endif %}
|
||||
<table width="100%">
|
||||
<tbody>
|
||||
<tr style="background-color: black;">
|
||||
<td style="background-color: black;">
|
||||
<a href="http://github.com/akuker/RASCSI">
|
||||
<a href="http://github.com/akuker/RASCSI" target="_blank">
|
||||
<h1>RaSCSI - 68kmla Edition</h1>
|
||||
</a>
|
||||
</td>
|
||||
@ -57,7 +72,7 @@
|
||||
{% block content %}{% endblock content %}
|
||||
</div>
|
||||
<div class="footer">
|
||||
<center><tt>RaSCSI version: <strong>{{ version }} <a href="https://github.com/akuker/RASCSI/commit/{{ running_env['git'] }}">{{ running_env["git"][:7] }}</a></strong></tt></center>
|
||||
<center><tt>RaSCSI version: <strong>{{ version }} <a href="https://github.com/akuker/RASCSI/commit/{{ running_env['git'] }}" target="_blank">{{ running_env["git"][:7] }}</a></strong></tt></center>
|
||||
<center><tt>Pi environment: {{ running_env["env"] }}</tt></center>
|
||||
</div>
|
||||
</div>
|
||||
|
164
src/web/web.py
164
src/web/web.py
@ -16,6 +16,7 @@ from flask import (
|
||||
send_file,
|
||||
send_from_directory,
|
||||
make_response,
|
||||
session,
|
||||
)
|
||||
|
||||
from file_cmds import (
|
||||
@ -42,6 +43,7 @@ from pi_cmds import (
|
||||
disk_space,
|
||||
get_ip_address,
|
||||
introspect_file,
|
||||
auth_active,
|
||||
)
|
||||
from ractl_cmds import (
|
||||
attach_image,
|
||||
@ -71,6 +73,7 @@ from settings import (
|
||||
DRIVE_PROPERTIES_FILE,
|
||||
REMOVABLE_DEVICE_TYPES,
|
||||
RESERVATIONS,
|
||||
AUTH_GROUP,
|
||||
)
|
||||
|
||||
APP = Flask(__name__)
|
||||
@ -111,6 +114,11 @@ def index():
|
||||
[ARCHIVE_FILE_SUFFIX]
|
||||
)
|
||||
|
||||
if "username" in session:
|
||||
username = session["username"]
|
||||
else:
|
||||
username = None
|
||||
|
||||
return render_template(
|
||||
"index.html",
|
||||
bridge_configured=is_bridge_setup(),
|
||||
@ -141,6 +149,8 @@ def index():
|
||||
cdrom_file_suffix=tuple(server_info["sccd"]),
|
||||
removable_file_suffix=tuple(server_info["scrm"]),
|
||||
mo_file_suffix=tuple(server_info["scmo"]),
|
||||
username=username,
|
||||
auth_active=auth_active()["status"],
|
||||
ARCHIVE_FILE_SUFFIX=ARCHIVE_FILE_SUFFIX,
|
||||
PROPERTIES_SUFFIX=PROPERTIES_SUFFIX,
|
||||
REMOVABLE_DEVICE_TYPES=REMOVABLE_DEVICE_TYPES,
|
||||
@ -192,6 +202,11 @@ def drive_list():
|
||||
cd_conf = sorted(cd_conf, key=lambda x: x["name"].lower())
|
||||
rm_conf = sorted(rm_conf, key=lambda x: x["name"].lower())
|
||||
|
||||
if "username" in session:
|
||||
username = session["username"]
|
||||
else:
|
||||
username = None
|
||||
|
||||
return render_template(
|
||||
"drives.html",
|
||||
files=sorted_image_files,
|
||||
@ -203,15 +218,46 @@ def drive_list():
|
||||
version=server_info["version"],
|
||||
free_disk=int(disk["free"] / 1024 / 1024),
|
||||
cdrom_file_suffix=tuple(server_info["sccd"]),
|
||||
username=username,
|
||||
auth_active=auth_active()["status"],
|
||||
)
|
||||
|
||||
|
||||
@APP.route('/pwa/<path:path>')
|
||||
@APP.route("/login", methods=["POST"])
|
||||
def login():
|
||||
"""
|
||||
Uses simplepam to authenticate against Linux users
|
||||
"""
|
||||
username = request.form["username"]
|
||||
password = request.form["password"]
|
||||
|
||||
from simplepam import authenticate
|
||||
from grp import getgrall
|
||||
|
||||
groups = [g.gr_name for g in getgrall() if username in g.gr_mem]
|
||||
if AUTH_GROUP in groups:
|
||||
if authenticate(str(username), str(password)):
|
||||
session["username"] = request.form["username"]
|
||||
return redirect(url_for("index"))
|
||||
flash(f"You must log in with credentials for a user in the '{AUTH_GROUP}' group!", "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
|
||||
@APP.route("/logout")
|
||||
def logout():
|
||||
"""
|
||||
Removes the logged in user from the session
|
||||
"""
|
||||
session.pop("username", None)
|
||||
return redirect(url_for("index"))
|
||||
|
||||
|
||||
@APP.route("/pwa/<path:path>")
|
||||
def send_pwa_files(path):
|
||||
"""
|
||||
Sets up mobile web resources
|
||||
"""
|
||||
return send_from_directory('pwa', path)
|
||||
return send_from_directory("pwa", path)
|
||||
|
||||
|
||||
@APP.route("/drive/create", methods=["POST"])
|
||||
@ -219,6 +265,11 @@ def drive_create():
|
||||
"""
|
||||
Creates the image and properties file pair
|
||||
"""
|
||||
auth = auth_active()
|
||||
if auth["status"] and "username" not in session:
|
||||
flash(auth["msg"], "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
vendor = request.form.get("vendor")
|
||||
product = request.form.get("product")
|
||||
revision = request.form.get("revision")
|
||||
@ -257,6 +308,11 @@ def drive_cdrom():
|
||||
"""
|
||||
Creates a properties file for a CD-ROM image
|
||||
"""
|
||||
auth = auth_active()
|
||||
if auth["status"] and "username" not in session:
|
||||
flash(auth["msg"], "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
vendor = request.form.get("vendor")
|
||||
product = request.form.get("product")
|
||||
revision = request.form.get("revision")
|
||||
@ -285,6 +341,11 @@ def config_save():
|
||||
"""
|
||||
Saves a config file to disk
|
||||
"""
|
||||
auth = auth_active()
|
||||
if auth["status"] and "username" not in session:
|
||||
flash(auth["msg"], "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
file_name = request.form.get("name") or "default"
|
||||
file_name = f"{file_name}.{CONFIG_FILE_SUFFIX}"
|
||||
|
||||
@ -302,6 +363,11 @@ def config_load():
|
||||
"""
|
||||
Loads a config file from disk
|
||||
"""
|
||||
auth = auth_active()
|
||||
if auth["status"] and "username" not in session:
|
||||
flash(auth["msg"], "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
file_name = request.form.get("name")
|
||||
|
||||
if "load" in request.form:
|
||||
@ -354,6 +420,11 @@ def log_level():
|
||||
"""
|
||||
Sets RaSCSI backend log level
|
||||
"""
|
||||
auth = auth_active()
|
||||
if auth["status"] and "username" not in session:
|
||||
flash(auth["msg"], "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
level = request.form.get("level") or "info"
|
||||
|
||||
process = set_log_level(level)
|
||||
@ -370,6 +441,11 @@ def daynaport_attach():
|
||||
"""
|
||||
Attaches a DaynaPORT ethernet adapter device
|
||||
"""
|
||||
auth = auth_active()
|
||||
if auth["status"] and "username" not in session:
|
||||
flash(auth["msg"], "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
scsi_id = request.form.get("scsi_id")
|
||||
interface = request.form.get("if")
|
||||
ip_addr = request.form.get("ip")
|
||||
@ -414,6 +490,11 @@ def attach():
|
||||
"""
|
||||
Attaches a file image as a device
|
||||
"""
|
||||
auth = auth_active()
|
||||
if auth["status"] and "username" not in session:
|
||||
flash(auth["msg"], "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
file_name = request.form.get("file_name")
|
||||
file_size = request.form.get("file_size")
|
||||
scsi_id = request.form.get("scsi_id")
|
||||
@ -466,6 +547,11 @@ def detach_all_devices():
|
||||
"""
|
||||
Detaches all currently attached devices
|
||||
"""
|
||||
auth = auth_active()
|
||||
if auth["status"] and "username" not in session:
|
||||
flash(auth["msg"], "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
process = detach_all()
|
||||
if process["status"]:
|
||||
flash("Detached all SCSI devices")
|
||||
@ -480,6 +566,11 @@ def detach():
|
||||
"""
|
||||
Detaches a specified device
|
||||
"""
|
||||
auth = auth_active()
|
||||
if auth["status"] and "username" not in session:
|
||||
flash(auth["msg"], "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
scsi_id = request.form.get("scsi_id")
|
||||
unit = request.form.get("unit")
|
||||
process = detach_by_id(scsi_id, unit)
|
||||
@ -497,6 +588,11 @@ def eject():
|
||||
"""
|
||||
Ejects a specified removable device image, but keeps the device attached
|
||||
"""
|
||||
auth = auth_active()
|
||||
if auth["status"] and "username" not in session:
|
||||
flash(auth["msg"], "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
scsi_id = request.form.get("scsi_id")
|
||||
unit = request.form.get("unit")
|
||||
|
||||
@ -549,6 +645,11 @@ def reserve_id():
|
||||
"""
|
||||
Reserves a SCSI ID and stores the memo for that reservation
|
||||
"""
|
||||
auth = auth_active()
|
||||
if auth["status"] and "username" not in session:
|
||||
flash(auth["msg"], "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
scsi_id = request.form.get("scsi_id")
|
||||
memo = request.form.get("memo")
|
||||
reserved_ids = get_reserved_ids()["ids"]
|
||||
@ -567,6 +668,11 @@ def unreserve_id():
|
||||
"""
|
||||
Removes the reservation of a SCSI ID as well as the memo for the reservation
|
||||
"""
|
||||
auth = auth_active()
|
||||
if auth["status"] and "username" not in session:
|
||||
flash(auth["msg"], "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
scsi_id = request.form.get("scsi_id")
|
||||
reserved_ids = get_reserved_ids()["ids"]
|
||||
reserved_ids.remove(scsi_id)
|
||||
@ -584,6 +690,11 @@ def restart():
|
||||
"""
|
||||
Restarts the Pi
|
||||
"""
|
||||
auth = auth_active()
|
||||
if auth["status"] and "username" not in session:
|
||||
flash(auth["msg"], "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
detach_all()
|
||||
flash("Safely detached all devices.")
|
||||
flash("Rebooting the Pi momentarily...")
|
||||
@ -596,6 +707,11 @@ def rascsi_restart():
|
||||
"""
|
||||
Restarts the RaSCSI backend service
|
||||
"""
|
||||
auth = auth_active()
|
||||
if auth["status"] and "username" not in session:
|
||||
flash(auth["msg"], "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
detach_all()
|
||||
flash("Safely detached all devices.")
|
||||
flash("Restarting RaSCSI Service...")
|
||||
@ -609,6 +725,11 @@ def shutdown():
|
||||
"""
|
||||
Shuts down the Pi
|
||||
"""
|
||||
auth = auth_active()
|
||||
if auth["status"] and "username" not in session:
|
||||
flash(auth["msg"], "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
detach_all()
|
||||
flash("Safely detached all devices.")
|
||||
flash("Shutting down the Pi momentarily...")
|
||||
@ -621,6 +742,11 @@ def download_to_iso():
|
||||
"""
|
||||
Downloads a remote file and creates a CD-ROM image formatted with HFS that contains the file
|
||||
"""
|
||||
auth = auth_active()
|
||||
if auth["status"] and "username" not in session:
|
||||
flash(auth["msg"], "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
scsi_id = request.form.get("scsi_id")
|
||||
url = request.form.get("url")
|
||||
|
||||
@ -647,6 +773,11 @@ def download_img():
|
||||
"""
|
||||
Downloads a remote file onto the images dir on the Pi
|
||||
"""
|
||||
auth = auth_active()
|
||||
if auth["status"] and "username" not in session:
|
||||
flash(auth["msg"], "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
url = request.form.get("url")
|
||||
server_info = get_server_info()
|
||||
process = download_to_dir(url, server_info["image_dir"])
|
||||
@ -664,6 +795,11 @@ def download_afp():
|
||||
"""
|
||||
Downloads a remote file onto the AFP shared dir on the Pi
|
||||
"""
|
||||
auth = auth_active()
|
||||
if auth["status"] and "username" not in session:
|
||||
flash(auth["msg"], "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
url = request.form.get("url")
|
||||
process = download_to_dir(url, AFP_DIR)
|
||||
if process["status"]:
|
||||
@ -681,6 +817,10 @@ def upload_file():
|
||||
Uploads a file from the local computer to the images dir on the Pi
|
||||
Depending on the Dropzone.js JavaScript library
|
||||
"""
|
||||
auth = auth_active()
|
||||
if auth["status"] and "username" not in session:
|
||||
return make_response(auth["msg"], 403)
|
||||
|
||||
from werkzeug.utils import secure_filename
|
||||
from os import path
|
||||
|
||||
@ -729,6 +869,11 @@ def create_file():
|
||||
"""
|
||||
Creates an empty image file in the images dir
|
||||
"""
|
||||
auth = auth_active()
|
||||
if auth["status"] and "username" not in session:
|
||||
flash(auth["msg"], "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
file_name = request.form.get("file_name")
|
||||
size = (int(request.form.get("size")) * 1024 * 1024)
|
||||
file_type = request.form.get("type")
|
||||
@ -750,6 +895,11 @@ def download():
|
||||
"""
|
||||
Downloads a file from the Pi to the local computer
|
||||
"""
|
||||
auth = auth_active()
|
||||
if auth["status"] and "username" not in session:
|
||||
flash(auth["msg"], "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
image = request.form.get("file")
|
||||
return send_file(image, as_attachment=True)
|
||||
|
||||
@ -759,6 +909,11 @@ def delete():
|
||||
"""
|
||||
Deletes a specified file in the images dir
|
||||
"""
|
||||
auth = auth_active()
|
||||
if auth["status"] and "username" not in session:
|
||||
flash(auth["msg"], "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
file_name = request.form.get("image")
|
||||
|
||||
process = delete_image(file_name)
|
||||
@ -787,6 +942,11 @@ def unzip():
|
||||
"""
|
||||
Unzips a specified zip file
|
||||
"""
|
||||
auth = auth_active()
|
||||
if auth["status"] and "username" not in session:
|
||||
flash(auth["msg"], "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
zip_file = request.form.get("zip_file")
|
||||
zip_member = request.form.get("zip_member") or False
|
||||
zip_members = request.form.get("zip_members") or False
|
||||
|
Loading…
Reference in New Issue
Block a user