Add htpasswd password support

This commit is contained in:
Tim Van Baak 2023-06-19 11:34:52 -07:00
parent 0c86a78d51
commit 1dbf7ddb60
4 changed files with 88 additions and 6 deletions

View File

@ -33,7 +33,12 @@
default = let
pythonEnv = pkgs.python38.withPackages (pypkgs: with pypkgs; [ flask black pytest ]);
in pkgs.mkShell {
packages = [ pythonEnv pkgs.nixos-shell ];
packages = [
pythonEnv
pkgs.nixos-shell
# We only take this dependency for htpasswd, which is a little unfortunate
pkgs.apacheHttpd
];
shellHook = ''
PS1="(develop) $PS1"
'';

View File

@ -41,19 +41,21 @@ def auth_check(route):
"""
Checks the HTTP Basic Auth header against the stored credential.
"""
@wraps(route)
def _route(*args, **kwargs):
data_path = intake_data_dir()
auth_path = data_path / "credentials.json"
if auth_path.exists():
if not request.authorization:
abort(403)
abort(401)
auth = json.load(auth_path.open(encoding="utf8"))
if request.authorization.username != auth["username"]:
abort(403)
if request.authorization.password != auth["secret"]:
abort(403)
return route(*args, **kwargs)
return _route

View File

@ -2,9 +2,11 @@ from datetime import datetime
from pathlib import Path
from shutil import get_terminal_size
import argparse
import getpass
import json
import os
import os.path
import pwd
import subprocess
import sys
@ -263,6 +265,44 @@ def cmd_feed(cmd_args):
print()
def cmd_passwd(cmd_args):
"""Update password for the web interface."""
parser = argparse.ArgumentParser(
prog="intake passwd",
description=cmd_passwd.__doc__,
)
parser.add_argument(
"--data",
"-d",
default=intake_data_dir(),
help="Path to the intake data directory",
)
args = parser.parse_args(cmd_args)
command_exists = subprocess.run(["command", "-v" "htpasswd"], shell=True)
if command_exists.returncode:
print("Could not find htpasswd, cannot update password")
return 1
creds = Path(args.data) / "credentials.json"
if not creds.parent.exists():
creds.parent.mkdir(parents=True)
user = pwd.getpwuid(os.getuid()).pw_name
password = getpass.getpass(f"intake password for {user}: ")
update_pwd = subprocess.run(
["htpasswd", "-b", "/etc/intake/htpasswd", user, password]
)
if update_pwd.returncode:
print("Could not update password file")
return 1
new_creds = {"username": user, "secret": password}
creds.write_text(json.dumps(new_creds, indent=2))
return 0
def cmd_run(cmd_args):
"""Run the default Flask server."""
parser = argparse.ArgumentParser(

View File

@ -9,19 +9,22 @@ in {
listen.addr = mkOption {
type = types.str;
default = "0.0.0.0";
description = "The listen address for the entry point to intake services. This endpoint will redirect to a local port based on the request's HTTP Basic Auth credentials.";
description = "The listen address for the entry point to intake services. This endpoint will redirect to a "
"local port based on the request's HTTP Basic Auth credentials.";
};
listen.port = mkOption {
type = types.port;
default = 80;
description = "The listen port for the entry point to intake services. This endpoint will redirect to a local port based on the request's HTTP Basic Auth credentials.";
description = "The listen port for the entry point to intake services. This endpoint will redirect to a local "
"port based on the request's HTTP Basic Auth credentials.";
};
internalPortStart = mkOption {
type = types.port;
default = 24130;
description = "The first port to use for internal service endpoints. A number of ports will be continguously allocated equal to the number of users with enabled intake services.";
description = "The first port to use for internal service endpoints. A number of ports will be continguously "
"allocated equal to the number of users with enabled intake services.";
};
users = mkOption {
@ -53,7 +56,39 @@ in {
enabledUserNames = mapAttrsToList (userName: userCfg: userName) enabledUsers;
userPortList = imap1 (i: userName: { ${userName} = i + intakeCfg.internalPortStart; }) enabledUserNames;
userPort = foldl (acc: val: acc // val) {} userPortList;
# To avoid polluting PATH with httpd programs, define an htpasswd wrapper
htpasswdWrapper = pkgs.writeShellScriptBin "htpasswd" ''
${pkgs.apacheHttpd}/bin/htpasswd $@
'';
# File locations
intakeDir = "/etc/intake";
intakePwd = "${intakeDir}/htpasswd";
in {
# Define a user group for access to the htpasswd file.
users.groups.intake.members = mkIf (enabledUsers != {}) (enabledUserNames ++ [ "nginx" ]);
# Define an activation script that ensures that the htpasswd file exists.
system.activationScripts.etc-intake = ''
if [ ! -e ${intakeDir} ]; then
${pkgs.coreutils}/bin/mkdir -p ${intakeDir};
fi
${pkgs.coreutils}/bin/chown root:root ${intakeDir}
${pkgs.coreutils}/bin/chmod 755 ${intakeDir}
if [ ! -e ${intakePwd} ]; then
${pkgs.coreutils}/bin/touch ${intakePwd}
fi
${pkgs.coreutils}/bin/chown root:intake ${intakePwd}
${pkgs.coreutils}/bin/chmod 660 ${intakePwd}
'';
# Give the htpasswd wrapper to every intake user
users.users =
let
addWrapperToUser = userName: { ${userName}.packages = [ htpasswdWrapper ]; };
in mkMerge (map addWrapperToUser enabledUserNames);
# Define a user service for each configured user
systemd.services =
let
@ -84,7 +119,7 @@ in {
listen = [ intakeCfg.listen ];
locations."/" = {
proxyPass = "http://127.0.0.1:$target_port";
basicAuth = { alice = "alpha"; bob = "beta"; };
basicAuthFile = intakePwd;
};
extraConfig = foldl (acc: val: acc + val) "" (mapAttrsToList (userName: port: ''
if ($remote_user = "${userName}") {