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:
Daniel Markstedt 2021-11-26 20:41:10 -08:00 committed by GitHub
parent 1ad1242fad
commit 5346d110a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 229 additions and 8 deletions

View File

@ -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\""

View File

@ -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": ""}

View File

@ -6,3 +6,4 @@ Jinja2==3.0.1
MarkupSafe==2.0.1
protobuf==3.17.3
requests==2.26.0
simplepam==0.1.5

View File

@ -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"

View File

@ -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> &#8211; <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 &#8211; 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>

View File

@ -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