Add token authentication to Web UI and OLED monitor script (#554)

* Throw 403 forbidden if token auth is enabled

* Use argparse to parse port and password parameters; use password token to authenticate

* Move config file loading to before_first_request block

* Add token auth to monitor script

* Install script for enabling token auth

* Token config flow for oled monitor installation

* Correct permissions

* Fix bug

* Cleanup
This commit is contained in:
Daniel Markstedt 2021-12-23 09:12:16 -08:00 committed by GitHub
parent 5817ca6df5
commit ab55d95fd4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 192 additions and 57 deletions

View File

@ -57,6 +57,7 @@ HFDISK_BIN=/usr/bin/hfdisk
LIDO_DRIVER=$BASE/lido-driver.img
GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
GIT_REMOTE=${GIT_REMOTE:-origin}
TOKEN=""
set -e
@ -141,6 +142,14 @@ function installRaScsiScreen() {
SCREEN_HEIGHT="32"
fi
echo ""
echo "Is RaSCSI using token-based authentication? [y/N]"
read -r REPLY
if [ "$REPLY" == "y" ] || [ "$REPLY" == "Y" ]; then
echo -n "Enter the passphrase that you configured: "
read -r TOKEN
fi
stopRaScsiScreen
updateRaScsiGit
@ -166,7 +175,13 @@ function installRaScsiScreen() {
echo "Installing the monitor_rascsi.service configuration..."
sudo cp -f "$BASE/src/oled_monitor/monitor_rascsi.service" "$SYSTEMD_PATH/monitor_rascsi.service"
sudo sed -i /^ExecStart=/d "$SYSTEMD_PATH/monitor_rascsi.service"
sudo sed -i "8 i ExecStart=$BASE/src/oled_monitor/start.sh --rotation=$ROTATION --height=$SCREEN_HEIGHT" "$SYSTEMD_PATH/monitor_rascsi.service"
if [ ! -z "$TOKEN" ]; then
sudo sed -i "8 i ExecStart=$BASE/src/oled_monitor/start.sh --rotation=$ROTATION --height=$SCREEN_HEIGHT --password=$TOKEN" "$SYSTEMD_PATH/monitor_rascsi.service"
sudo chmod 600 "$SYSTEMD_PATH/monitor_rascsi.service"
echo "Granted access to the OLED Monitor with the token passphrase that you configured for RaSCSI."
else
sudo sed -i "8 i ExecStart=$BASE/src/oled_monitor/start.sh --rotation=$ROTATION --height=$SCREEN_HEIGHT" "$SYSTEMD_PATH/monitor_rascsi.service"
fi
sudo systemctl daemon-reload
sudo systemctl enable monitor_rascsi
@ -264,7 +279,27 @@ function backupRaScsiService() {
# Modifies and installs the rascsi service
function enableRaScsiService() {
sudo sed -i "s@^ExecStart.*@& -F $VIRTUAL_DRIVER_PATH@" "$SYSTEMD_PATH/rascsi.service"
echo ""
echo "Do you want to enable token-based access control for RaSCSI? [y/N]"
read REPLY
if [ "$REPLY" == "y" ] || [ "$REPLY" == "Y" ]; then
echo -n "Enter the passphrase that you want to use: "
read -r TOKEN
if [ -f "$HOME/.rascsi_secret" ]; then
sudo rm "$HOME/.rascsi_secret"
echo "Removed old RaSCSI token file"
fi
echo "$TOKEN" > "$HOME/.rascsi_secret"
sudo chown root:root "$HOME/.rascsi_secret"
sudo chmod 600 "$HOME/.rascsi_secret"
sudo sed -i "s@^ExecStart.*@& -F $VIRTUAL_DRIVER_PATH -P $HOME/.rascsi_secret@" "$SYSTEMD_PATH/rascsi.service"
sudo chmod 600 "$SYSTEMD_PATH/rascsi.service"
echo "Configured to use $HOME/.rascsi_secret to secure RaSCSI. This file is readable by root only."
echo "Make note of your passphrase; you will need it to use rasctl, and other RaSCSI clients."
else
sudo sed -i "s@^ExecStart.*@& -F $VIRTUAL_DRIVER_PATH@" "$SYSTEMD_PATH/rascsi.service"
fi
echo "Configured rascsi.service to use $VIRTUAL_DRIVER_PATH as default image dir."
sudo systemctl daemon-reload
@ -279,7 +314,14 @@ function installWebInterfaceService() {
echo "Installing the rascsi-web.service configuration..."
sudo cp -f "$BASE/src/web/service-infra/rascsi-web.service" "$SYSTEMD_PATH/rascsi-web.service"
sudo sed -i /^ExecStart=/d "$SYSTEMD_PATH/rascsi-web.service"
sudo sed -i "8 i ExecStart=$WEB_INSTALL_PATH/start.sh" "$SYSTEMD_PATH/rascsi-web.service"
echo "$TOKEN"
if [ ! -z "$TOKEN" ]; then
sudo sed -i "8 i ExecStart=$WEB_INSTALL_PATH/start.sh --password=$TOKEN" "$SYSTEMD_PATH/rascsi-web.service"
sudo chmod 600 "$SYSTEMD_PATH/rascsi-web.service"
echo "Granted access to the Web Interface with the token passphrase that you configured for RaSCSI."
else
sudo sed -i "8 i ExecStart=$WEB_INSTALL_PATH/start.sh" "$SYSTEMD_PATH/rascsi-web.service"
fi
sudo systemctl daemon-reload
sudo systemctl enable rascsi-web
@ -475,12 +517,12 @@ function setupWiredNetworking() {
echo "WARNING: If you continue, the IP address of your Pi may change upon reboot."
echo "Please make sure you will not lose access to the Pi system."
echo ""
echo "Do you want to proceed with network configuration using the default settings? Y/n"
echo "Do you want to proceed with network configuration using the default settings? [Y/n]"
read REPLY
if [ "$REPLY" == "N" ] || [ "$REPLY" == "n" ]; then
echo "Available wired interfaces on this system:"
echo `ip -o addr show scope link | awk '{split($0, a); print $2}' | grep eth`
echo `ip -o addr show scope link | awk '{split($0, a); print $2}' | grep eth`
echo "Please type the wired interface you want to use and press Enter:"
read SELECTED
LAN_INTERFACE=$SELECTED
@ -530,12 +572,12 @@ function setupWirelessNetworking() {
echo "Subnet Mask: $NETWORK_MASK"
echo "DNS Server: Any public DNS server"
echo ""
echo "Do you want to proceed with network configuration using the default settings? Y/n"
echo "Do you want to proceed with network configuration using the default settings? [Y/n]"
read REPLY
if [ "$REPLY" == "N" ] || [ "$REPLY" == "n" ]; then
echo "Available wireless interfaces on this system:"
echo `ip -o addr show scope link | awk '{split($0, a); print $2}' | grep wlan`
echo `ip -o addr show scope link | awk '{split($0, a); print $2}' | grep wlan`
echo "Please type the wireless interface you want to use and press Enter:"
read -r WLAN_INTERFACE
echo "Base IP address (ex. 10.10.20):"
@ -553,7 +595,7 @@ function setupWirelessNetworking() {
read REPLY
else
sudo bash -c 'echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf'
echo "Modified /etc/sysctl.conf"
echo "Modified /etc/sysctl.conf"
fi
# Check if iptables is installed
@ -966,7 +1008,7 @@ while [ "$1" != "" ]; do
;;
*)
echo "ERROR: unknown option \"$VALUE\""
exit 1
exit 1
;;
esac
shift

View File

@ -6,13 +6,14 @@ from unidecode import unidecode
from socket_cmds import send_pb_command
import rascsi_interface_pb2 as proto
def device_list():
def device_list(token):
"""
Sends a DEVICES_INFO command to the server.
Returns a list of dicts with info on all attached devices.
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.DEVICES_INFO
command.params["token"] = token
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
@ -51,3 +52,19 @@ def device_list():
i += 1
return dlist
def is_token_auth(token):
"""
Sends a CHECK_AUTHENTICATION command to the server.
Tells you whether RaSCSI backend is protected by a token password or not.
Returns (bool) status and (str) msg.
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.CHECK_AUTHENTICATION
command.params["token"] = token
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}

View File

@ -30,6 +30,7 @@
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
import argparse
from time import sleep
from sys import argv
from collections import deque
@ -38,29 +39,47 @@ from adafruit_ssd1306 import SSD1306_I2C
from PIL import Image, ImageDraw, ImageFont
from interrupt_handler import GracefulInterruptHandler
from pi_cmds import get_ip_and_host
from ractl_cmds import device_list
from ractl_cmds import device_list, is_token_auth
# Read positional arguments; expecting exactly two, or none
# Arg 1 is the rotation in degrees, arg 2 is the screen height in pixels
# Valid values are 0/180 for ROTATION, 32/64 for HEIGHT
if len(argv) == 3:
if int(argv[1]) == 0:
ROTATION = 0
else:
# 2 means 180 degrees
ROTATION = 2
if int(argv[2]) == 64:
HEIGHT = 64
LINES = 8
else:
HEIGHT = 32
LINES = 4
else:
# Default settings
parser = argparse.ArgumentParser(description="RaSCSI OLED Monitor script")
parser.add_argument(
"--rotation",
type=int,
choices=[0, 180],
default=180,
action="store",
help="The rotation of the screen buffer in degrees",
)
parser.add_argument(
"--height",
type=int,
choices=[32, 64],
default=32,
action="store",
help="The pixel height of the screen buffer",
)
parser.add_argument(
"--password",
type=str,
default="",
action="store",
help="Token password string for authenticating with RaSCSI",
)
args = parser.parse_args()
if args.rotation == 0:
ROTATION = 0
elif args.rotation == 180:
ROTATION = 2
if args.height == 64:
HEIGHT = 64
LINES = 8
elif args.height == 32:
HEIGHT = 32
LINES = 4
print("No valid parameters detected; defaulting to 32 px height, 180 degrees rotation.")
TOKEN = args.password
WIDTH = 128
BORDER = 5
@ -132,10 +151,12 @@ def formatted_output():
Formats the strings to be displayed on the Screen
Returns a (list) of (str) output
"""
rascsi_list = device_list()
rascsi_list = device_list(TOKEN)
output = []
if rascsi_list:
if not TOKEN and not is_token_auth(TOKEN)["status"]:
output.append("Permission denied!")
elif rascsi_list:
for line in rascsi_list:
if line["device_type"] in ("SCCD", "SCRM", "SCMO"):
# Print image file name only when there is an image attached

View File

@ -112,24 +112,19 @@ while [ "$1" != "" ]; do
VALUE=$(echo "$1" | awk -F= '{print $2}')
case $PARAM in
-r | --rotation)
ROTATION=$VALUE
ROTATION="--rotation $VALUE"
;;
-h | --height)
HEIGHT=$VALUE
HEIGHT="--height $VALUE"
;;
-P | --password)
PASSWORD="--password $VALUE"
;;
*)
echo "ERROR: unknown parameter \"$PARAM\""
exit 1
;;
esac
case $VALUE in
0 | 180 | 32 | 64 )
;;
*)
echo "ERROR: invalid option \"$VALUE\""
exit 1
;;
esac
shift
done
@ -137,11 +132,11 @@ echo "Starting OLED Screen..."
if [ -z ${ROTATION+x} ]; then
echo "No screen rotation parameter given; falling back to the default."
else
echo "Screen rotation set to $ROTATION degrees."
echo "Starting with parameter $ROTATION"
fi
if [ -z ${HEIGHT+x} ]; then
echo "No screen height parameter given; falling back to the default."
else
echo "Screen height set to $HEIGHT px."
echo "Starting with parameter $HEIGHT"
fi
python3 rascsi_oled_monitor.py ${ROTATION} ${HEIGHT}
python3 rascsi_oled_monitor.py ${ROTATION} ${HEIGHT} ${PASSWORD}

View File

@ -5,6 +5,7 @@ Module for methods reading from and writing to the file system
import os
import logging
from pathlib import PurePath
from flask import current_app
from ractl_cmds import (
get_server_info,
@ -63,6 +64,7 @@ def list_images():
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.DEFAULT_IMAGE_FILES_INFO
command.params["token"] = current_app.config["TOKEN"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
@ -118,6 +120,7 @@ def create_new_image(file_name, file_type, size):
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.CREATE_IMAGE
command.params["token"] = current_app.config["TOKEN"]
command.params["file"] = file_name + "." + file_type
command.params["size"] = str(size)
@ -137,6 +140,7 @@ def delete_image(file_name):
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.DELETE_IMAGE
command.params["token"] = current_app.config["TOKEN"]
command.params["file"] = file_name
@ -154,6 +158,7 @@ def rename_image(file_name, new_file_name):
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.RENAME_IMAGE
command.params["token"] = current_app.config["TOKEN"]
command.params["from"] = file_name
command.params["to"] = new_file_name

View File

@ -4,6 +4,7 @@ Module for commands sent to the RaSCSI backend service.
from settings import REMOVABLE_DEVICE_TYPES
from socket_cmds import send_pb_command
from flask import current_app
import rascsi_interface_pb2 as proto
@ -20,6 +21,7 @@ def get_server_info():
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.SERVER_INFO
command.params["token"] = current_app.config["TOKEN"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
@ -75,6 +77,7 @@ def get_reserved_ids():
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.RESERVED_IDS_INFO
command.params["token"] = current_app.config["TOKEN"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
@ -95,6 +98,7 @@ def get_network_info():
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.NETWORK_INTERFACES_INFO
command.params["token"] = current_app.config["TOKEN"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
@ -112,6 +116,7 @@ def get_device_types():
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.DEVICE_TYPES_INFO
command.params["token"] = current_app.config["TOKEN"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
@ -134,6 +139,7 @@ def attach_image(scsi_id, **kwargs):
"""
command = proto.PbCommand()
command.params["token"] = current_app.config["TOKEN"]
devices = proto.PbDeviceDefinition()
devices.id = int(scsi_id)
@ -204,6 +210,7 @@ def detach_by_id(scsi_id, unit=None):
command = proto.PbCommand()
command.operation = proto.PbOperation.DETACH
command.devices.append(devices)
command.params["token"] = current_app.config["TOKEN"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
@ -218,6 +225,7 @@ def detach_all():
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.DETACH_ALL
command.params["token"] = current_app.config["TOKEN"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
@ -239,6 +247,7 @@ def eject_by_id(scsi_id, unit=None):
command = proto.PbCommand()
command.operation = proto.PbOperation.EJECT
command.devices.append(devices)
command.params["token"] = current_app.config["TOKEN"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
@ -258,6 +267,7 @@ def list_devices(scsi_id=None, unit=None):
from os import path
command = proto.PbCommand()
command.operation = proto.PbOperation.DEVICES_INFO
command.params["token"] = current_app.config["TOKEN"]
# If method is called with scsi_id parameter, return the info on those devices
# Otherwise, return the info on all attached devices
@ -333,6 +343,7 @@ def reserve_scsi_ids(reserved_scsi_ids):
command = proto.PbCommand()
command.operation = proto.PbOperation.RESERVE_IDS
command.params["ids"] = ",".join(reserved_scsi_ids)
command.params["token"] = current_app.config["TOKEN"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
@ -349,6 +360,7 @@ def set_log_level(log_level):
command = proto.PbCommand()
command.operation = proto.PbOperation.LOG_LEVEL
command.params["level"] = str(log_level)
command.params["token"] = current_app.config["TOKEN"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
@ -365,6 +377,23 @@ def shutdown_pi(mode):
command = proto.PbCommand()
command.operation = proto.PbOperation.SHUT_DOWN
command.params["mode"] = str(mode)
command.params["token"] = current_app.config["TOKEN"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
def is_token_auth():
"""
Sends a CHECK_AUTHENTICATION command to the server.
Tells you whether RaSCSI backend is protected by a token password or not.
Returns (bool) status and (str) msg.
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.CHECK_AUTHENTICATION
command.params["token"] = current_app.config["TOKEN"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()

View File

@ -80,7 +80,10 @@ while [ "$1" != "" ]; do
VALUE=$(echo "$1" | awk -F= '{print $2}')
case $PARAM in
-p | --port)
PORT=$VALUE
PORT="--port $VALUE"
;;
-P | --password)
PASSWORD="--password $VALUE"
;;
*)
echo "ERROR: unknown parameter \"$PARAM\""
@ -90,5 +93,5 @@ while [ "$1" != "" ]; do
shift
done
echo "Starting web server on port ${PORT:-8080}..."
python3 web.py ${PORT}
echo "Starting web server for RaSCSI Web Interface..."
python3 web.py ${PORT} ${PASSWORD}

View File

@ -3,6 +3,7 @@ Module for the Flask app rendering and endpoints
"""
import logging
import argparse
from sys import argv
from pathlib import Path
from functools import wraps
@ -18,6 +19,7 @@ from flask import (
send_from_directory,
make_response,
session,
abort,
)
from file_cmds import (
@ -58,6 +60,7 @@ from ractl_cmds import (
reserve_scsi_ids,
set_log_level,
shutdown_pi,
is_token_auth,
)
from device_utils import (
sort_and_format_devices,
@ -79,12 +82,14 @@ from settings import (
APP = Flask(__name__)
@APP.route("/")
def index():
"""
Sets up data structures for and renders the index page
"""
if not is_token_auth()["status"] and not APP.config["TOKEN"]:
abort(403, "RaSCSI is password protected. Start the Web Interface with the --password parameter.")
server_info = get_server_info()
disk = disk_space()
devices = list_devices()
@ -922,20 +927,38 @@ def unzip():
return redirect(url_for("index"))
@APP.before_first_request
def load_default_config():
"""
Load the default configuration file, if found
"""
if Path(f"{CFG_DIR}/{DEFAULT_CONFIG}").is_file():
read_config(DEFAULT_CONFIG)
if __name__ == "__main__":
APP.secret_key = "rascsi_is_awesome_insecure_secret_key"
APP.config["SESSION_TYPE"] = "filesystem"
APP.config["MAX_CONTENT_LENGTH"] = int(MAX_FILE_SIZE)
# Load the default configuration file, if found
if Path(f"{CFG_DIR}/{DEFAULT_CONFIG}").is_file():
read_config(DEFAULT_CONFIG)
if len(argv) > 1:
PORT = int(argv[1])
else:
PORT = 8080
parser = argparse.ArgumentParser(description="RaSCSI Web Interface arguments")
parser.add_argument(
"--port",
type=int,
default=8080,
action="store",
help="Port number the web server will run on",
)
parser.add_argument(
"--password",
type=str,
default="",
action="store",
help="Token password string for authenticating with RaSCSI",
)
args = parser.parse_args()
APP.config["TOKEN"] = args.password
import bjoern
print("Serving rascsi-web...")
bjoern.run(APP, "0.0.0.0", PORT)
bjoern.run(APP, "0.0.0.0", args.port)