From 4f9e240869a2b22fc0fca6fe1c53ef55a5c57dfe Mon Sep 17 00:00:00 2001 From: boltgolt Date: Fri, 13 Apr 2018 21:19:28 +0200 Subject: [PATCH] Completed CLI rework --- README.md | 24 +++++----- src/cli.py | 115 ++++++++++++++++++++------------------------- src/cli/add.py | 35 ++++++++------ src/cli/clear.py | 23 +++++---- src/cli/config.py | 15 ++++++ src/cli/disable.py | 41 ++++++++++++++++ src/cli/help.py | 17 ------- src/cli/list.py | 3 +- src/cli/remove.py | 43 +++++++++-------- src/config.ini | 6 +++ src/pam.py | 4 ++ 11 files changed, 189 insertions(+), 137 deletions(-) create mode 100644 src/cli/config.py create mode 100644 src/cli/disable.py delete mode 100644 src/cli/help.py diff --git a/README.md b/README.md index 02ab96c..12b54a1 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ sudo apt update sudo apt install howdy ``` -This will guide you through the installation. When that's done run `sudo howdy add $USER` and replace `$USER` with your username to add a face model. +This will guide you through the installation. When that's done run `sudo howdy add` to add a face model. If nothing went wrong we should be able to run sudo by just showing your face. Open a new terminal and run `sudo -i` to see it in action. @@ -22,22 +22,22 @@ If nothing went wrong we should be able to run sudo by just showing your face. O ### Command line -The installer adds a `howdy` command to manage face models for the current user. Use `howdy help` to list the available options. +The installer adds a `howdy` command to manage face models for the current user. Use `howdy --help` to list the available options. Usage: ``` -howdy [user] [argument] +howdy [-U user] [-y] command [argument] ``` -| Command | Description | User required | -|-----------|-----------------------------------------------|---------------| -| `add` | Add a new face model for the given user | Yes | -| `clear` | Remove all face models for the given user | Yes | -| `config` | Open the config file in nano | No | -| `disable` | Disable or enable howdy | No | -| `list` | List all saved face models for the given user | Yes | -| `remove` | Remove a specific model for the given user | Yes | -| `test` | Test the camera and recognition methods | No | +| Command | Description | +|-----------|-----------------------------------------------| +| `add` | Add a new face model for the given user | +| `clear` | Remove all face models for the given user | +| `config` | Open the config file in nano | +| `disable` | Disable or enable howdy | +| `list` | List all saved face models for the given user | +| `remove` | Remove a specific model for the given user | +| `test` | Test the camera and recognition methods | ### Troubleshooting diff --git a/src/cli.py b/src/cli.py index 05d0796..3b1640f 100755 --- a/src/cli.py +++ b/src/cli.py @@ -7,102 +7,91 @@ import os import subprocess import getpass import argparse +import builtins +# Try to get the original username (not "root") from shell user = subprocess.check_output("echo $(logname 2>/dev/null || echo $SUDO_USER)", shell=True).decode("ascii").strip() +# If that fails, try to get the direct user if user == "root" or user == "": env_user = getpass.getuser().strip() - if env_user == "root" or env_user == "": + # If even that fails, error out + if env_user == "": print("Could not determine user, please use the --user flag") sys.exit(1) else: user = env_user +# Check if we have rootish rights +if os.getenv("SUDO_USER") is None: + print("Please run this command with sudo") + sys.exit(1) + +# Basic command setup parser = argparse.ArgumentParser(description="Command line interface for Howdy face authentication.", - formatter_class=argparse.RawDescriptionHelpFormatter, - add_help=False, - prog="howdy", - epilog="For support please visit\nhttps://github.com/Boltgolt/howdy") - + formatter_class=argparse.RawDescriptionHelpFormatter, + add_help=False, + prog="howdy", + epilog="For support please visit\nhttps://github.com/Boltgolt/howdy") +# Add an argument for the command parser.add_argument("command", - help="The command option to execute, can be one of the following: add, clear, config, disable, list, remove or test.", - metavar="command", - choices=["add", "clear", "config", "disable", "list", "remove", "test"]) + help="The command option to execute, can be one of the following: add, clear, config, disable, list, remove or test.", + metavar="command", + choices=["add", "clear", "config", "disable", "list", "remove", "test"]) +# Add an argument for the extra arguments of diable and remove parser.add_argument("argument", - help="Either 0 or 1 for the disable command, or the model ID for the remove command.", - nargs="?") + help="Either 0 (enable) or 1 (disable) for the disable command, or the model ID for the remove command.", + nargs="?") +# Add the user flag parser.add_argument("-U", "--user", - default=user, + default=user, help="Set the user account to use.") +# Add the -y flag parser.add_argument("-y", help="Skip all questions.", - action="store_true") + action="store_true") +# Overwrite the default help message so we can use a uppercase S parser.add_argument("-h", "--help", - action="help", - default=argparse.SUPPRESS, + action="help", + default=argparse.SUPPRESS, help="Show this help message and exit.") +# If we only have 1 argument we print the help text if len(sys.argv) < 2: print("current active user: " + user + "\n") parser.print_help() sys.exit(0) +# Parse all arguments above args = parser.parse_args() -print(args) -sys.exit(0) +# Save the args and user as builtins which can be accessed by the imports +builtins.howdy_args = args +builtins.howdy_user = args.user -# Check if if a command has been given and print help otherwise -if (len(sys.argv) < 2): - print("Howdy IR face recognition help") - import cli.help - sys.exit() +# Beond this point the user can't change anymore, if we still have root as user we need to abort +if args.user == "root": + print("Can't run howdy commands as root, please run this command with the --user flag") + sys.exit(1) -# The command given -cmd = sys.argv[1] - -# Call the right files for commands that don't need root -if cmd == "help": - print("Howdy IR face recognition") - import cli.help -elif cmd == "test": +# Execute the right command +if args.command == "add": + import cli.add +elif args.command == "clear": + import cli.clear +elif args.command == "config": + import cli.config +elif args.command == "disable": + import cli.disable +elif args.command == "list": + import cli.list +elif args.command == "remove": + import cli.remove +elif args.command == "test": import cli.test -else: - # Check if the minimum of 3 arugemnts has been met and print help otherwise - if (len(sys.argv) < 3): - print("Howdy IR face recognition help") - import cli.help - sys.exit() - - # Requre sudo for comamnds that need root rights to read the model files - if os.getenv("SUDO_USER") is None: - print("Please run this command with sudo") - sys.exit(1) - - # Frome here on we require the second argument to be the username, - # switching the command to the 3rd - cmd = sys.argv[2] - - if cmd == "list": - import cli.list - elif cmd == "add": - import cli.add - elif cmd == "remove": - import cli.remove - elif cmd == "clear": - import cli.clear - else: - # If the comand is invalid, check if the user hasn't swapped the username and command - if sys.argv[1] in ["list", "add", "remove", "clear"]: - print("Usage: howdy ") - sys.exit(1) - else: - print('Unknown command "' + cmd + '"') - import cli.help - sys.exit(1) diff --git a/src/cli/add.py b/src/cli/add.py index 427ce4d..c9f0594 100644 --- a/src/cli/add.py +++ b/src/cli/add.py @@ -8,6 +8,7 @@ import sys import json import cv2 import configparser +import builtins # Try to import face_recognition and give a nice error if we can't # Add should be the first point where import issues show up @@ -18,7 +19,7 @@ except ImportError as err: print("\nCan't import the face_recognition module, check the output of") print("pip3 show face_recognition") - sys.exit() + sys.exit(1) # Get the absolute path to the current file path = os.path.dirname(os.path.abspath(__file__)) @@ -27,8 +28,7 @@ path = os.path.dirname(os.path.abspath(__file__)) config = configparser.ConfigParser() config.read(path + "/../config.ini") -# The current user -user = sys.argv[1] +user = builtins.howdy_user # The permanent file to store the encoded model in enc_file = path + "/../models/" + user + ".dat" # Known encodings @@ -46,11 +46,11 @@ except FileNotFoundError: encodings = [] # Print a warning if too many encodings are being added -if len(encodings) > 2: +if len(encodings) > 3: print("WARNING: Every additional model slows down the face recognition engine") - print("Press ctrl+C to cancel") + print("Press ctrl+C to cancel\n") -print("Adding face model for the user account " + user) +print("Adding face model for the user " + user) # Set the default label label = "Initial model" @@ -59,12 +59,16 @@ label = "Initial model" if len(encodings) > 0: label = "Model #" + str(len(encodings) + 1) -# Ask the user for a custom label -label_in = input("Enter a label for this new model [" + label + "]: ") +# Keep de default name if we can't ask questions +if builtins.howdy_args.y: + print("Using default label \"" + label + "\" because of -y flag") +else: + # Ask the user for a custom label + label_in = input("Enter a label for this new model [" + label + "]: ") -# Set the custom label (if any) and limit it to 24 characters -if label_in != "": - label = label_in[:24] + # Set the custom label (if any) and limit it to 24 characters + if label_in != "": + label = label_in[:24] # Prepare the metadata for insertion insert_model = { @@ -106,12 +110,12 @@ while frames < 60: # If 0 faces are detected we can't continue if len(enc) == 0: print("No face detected, aborting") - sys.exit() + sys.exit(1) # If more than 1 faces are detected we can't know wich one belongs to the user if len(enc) > 1: print("Multiple faces detected, aborting") - sys.exit() + sys.exit(1) # Totally clean array that can be exported as JSON clean_enc = [] @@ -130,5 +134,6 @@ with open(enc_file, "w") as datafile: json.dump(encodings, datafile) # Give let the user know how it went -print("Scan complete") -print("\nAdded a new model to " + user) +print("""Scan complete + +Added a new model to """ + user) diff --git a/src/cli/clear.py b/src/cli/clear.py index 4cd56c2..817de48 100644 --- a/src/cli/clear.py +++ b/src/cli/clear.py @@ -3,30 +3,33 @@ # Import required modules import os import sys +import builtins # Get the full path to this file path = os.path.dirname(os.path.abspath(__file__)) # Get the passed user -user = sys.argv[1] +user = builtins.howdy_user # Check if the models folder is there if not os.path.exists(path + "/../models"): print("No models created yet, can't clear them if they don't exist") - sys.exit() + sys.exit(1) # Check if the user has a models file to delete if not os.path.isfile(path + "/../models/" + user + ".dat"): print(user + " has no models or they have been cleared already") - sys.exit() + sys.exit(1) -# Double check with the user -print("This will clear all models for " + user) -ans = input("Do you want to continue [y/N]: ") +# Only ask the user if there's no -y flag +if not builtins.howdy_args.y: + # Double check with the user + print("This will clear all models for " + user) + ans = input("Do you want to continue [y/N]: ") -# Abort if they don't answer y or Y -if (ans.lower() != "y"): - print('\nInerpeting as a "NO"') - sys.exit() + # Abort if they don't answer y or Y + if (ans.lower() != "y"): + print('\nInerpeting as a "NO"') + sys.exit(1) # Delete otherwise os.remove(path + "/../models/" + user + ".dat") diff --git a/src/cli/config.py b/src/cli/config.py new file mode 100644 index 0000000..d0471a5 --- /dev/null +++ b/src/cli/config.py @@ -0,0 +1,15 @@ +# Open the config file in gedit + +# Import required modules +import os +import time +import subprocess + +# Let the user know what we're doing +print("Opening condig.ini in gedit") + +# Open gedit as a subprocess and fork it +subprocess.Popen(["gedit", os.path.dirname(os.path.realpath(__file__)) + "/../config.ini"], + cwd="/", + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) diff --git a/src/cli/disable.py b/src/cli/disable.py new file mode 100644 index 0000000..2c2b03b --- /dev/null +++ b/src/cli/disable.py @@ -0,0 +1,41 @@ +# Set the disable flag + +# Import required modules +import sys +import os +import json +import builtins +import fileinput +import configparser + +# Get the absolute filepath +config_path = os.path.dirname(os.path.abspath(__file__)) + "/../config.ini" + +# Read config from disk +config = configparser.ConfigParser() +config.read(config_path) + +# Check if enough arguments have been passed +if builtins.howdy_args.argument == None: + print("Please add a 0 (enable) or a 1 (disable) as an argument") + sys.exit(1) + +# Translate the argument to the right string +if builtins.howdy_args.argument == "1": + out_value = "true" +elif builtins.howdy_args.argument == "0": + out_value = "false" +else: + # Of it's not a 0 or a 1, it's invalid + print("Please only use a 0 (enable) or a 1 (disable) as an argument") + sys.exit(1) + +# Loop though the config file and only replace the line containing the disable config +for line in fileinput.input([config_path], inplace=1): + print(line.replace("disabled = " + config.get("core", "disabled"), "disabled = " + out_value), end="") + +# Print what we just did +if builtins.howdy_args.argument == "1": + print("Howdy has been disabled") +else: + print("Howdy has been enabled") diff --git a/src/cli/help.py b/src/cli/help.py deleted file mode 100644 index 2bbd879..0000000 --- a/src/cli/help.py +++ /dev/null @@ -1,17 +0,0 @@ -# Prints a simple help page for the CLI - -print(""" -Usage: - howdy [argument] - -Commands: - help Show this help page - list List all saved face models for the current user - add Add a new face model for the current user - remove [id] Remove a specific model - clear Remove all face models for the current user - test Test the camera and recognition methods - -For support please visit -https://github.com/Boltgolt/howdy\ -""") diff --git a/src/cli/list.py b/src/cli/list.py index ff37b10..061bf49 100644 --- a/src/cli/list.py +++ b/src/cli/list.py @@ -5,10 +5,11 @@ import sys import os import json import time +import builtins # Get the absolute path and the username path = os.path.dirname(os.path.realpath(__file__)) + "/.." -user = sys.argv[1] +user = builtins.howdy_user # Check if the models file has been created yet if not os.path.exists(path + "/models"): diff --git a/src/cli/remove.py b/src/cli/remove.py index 4322839..deef825 100644 --- a/src/cli/remove.py +++ b/src/cli/remove.py @@ -4,22 +4,23 @@ import sys import os import json +import builtins # Get the absolute path and the username path = os.path.dirname(os.path.realpath(__file__)) + "/.." -user = sys.argv[1] +user = builtins.howdy_user # Check if enough arguments have been passed -if len(sys.argv) == 3: - print("Please add the ID of the model to remove as an argument") +if builtins.howdy_args.argument == None: + print("Please add the ID of the model you want to remove as an argument") print("You can find the IDs by running:") - print("\n\thowdy " + user + " list\n") + print("\n\thowdy list\n") sys.exit(1) # Check if the models file has been created yet if not os.path.exists(path + "/models"): print("Face models have not been initialized yet, please run:") - print("\n\thowdy " + user + " add\n") + print("\n\thowdy add\n") sys.exit(1) # Path to the models file @@ -30,7 +31,7 @@ try: encodings = json.load(open(enc_file)) except FileNotFoundError: print("No face model known for the user " + user + ", please run:") - print("\n\thowdy " + user + " add\n") + print("\n\thowdy add\n") sys.exit(1) # Tracks if a encoding with that id has been found @@ -38,24 +39,28 @@ found = False # Loop though all encodings and check if they match the argument for enc in encodings: - if str(enc["id"]) == sys.argv[3]: - # Double check with the user - print('This will remove the model called "' + enc["label"] + '" for ' + user) - ans = input("Do you want to continue [y/N]: ") + if str(enc["id"]) == builtins.howdy_args.argument: + # Only ask the user if there's no -y flag + if not builtins.howdy_args.y: + # Double check with the user + print('This will remove the model called "' + enc["label"] + '" for ' + user) + ans = input("Do you want to continue [y/N]: ") - # Abort if the answer isn't yes - if (ans.lower() != "y"): - print('\nInerpeting as a "NO"') - sys.exit() + # Abort if the answer isn't yes + if (ans.lower() != "y"): + print('\nInerpeting as a "NO"') + sys.exit() + + # Add a padding empty line + print() # Mark as found and print an enter found = True - print() break # Abort if no matching id was found if not found: - print("No model with ID " + sys.argv[3] + " exists for " + user) + print("No model with ID " + builtins.howdy_args.argument + " exists for " + user) sys.exit() # Remove the entire file if this encoding is the only one @@ -68,11 +73,11 @@ else: # Loop though all encodin and only add thos that don't need to be removed for enc in encodings: - if str(enc["id"]) != sys.argv[3]: + if str(enc["id"]) != builtins.howdy_args.argument: new_encodings.append(enc) - # Save this new set to disk + # Save this new set to disk with open(enc_file, "w") as datafile: json.dump(new_encodings, datafile) - print("Removed model " + sys.argv[3]) + print("Removed model " + builtins.howdy_args.argument) diff --git a/src/config.ini b/src/config.ini index 3076f96..b80f2f3 100644 --- a/src/config.ini +++ b/src/config.ini @@ -1,3 +1,5 @@ +# Howdy config file + [core] # Do not print anything when a face verification succeeds no_confirmation = false @@ -11,6 +13,10 @@ suppress_unknown = false # Expirimental, can behave incorrectly on some systems dismiss_lockscreen = false +# Disable howdy in the PAM +# The howdy command will still function +disabled = false + [video] # The certainty of the detected face belonging to the user of the account # On a scale from 1 to 10, values above 5 are not recommended diff --git a/src/pam.py b/src/pam.py index 8c4eabc..13e571b 100644 --- a/src/pam.py +++ b/src/pam.py @@ -15,6 +15,10 @@ config.read(os.path.dirname(os.path.abspath(__file__)) + "/config.ini") def doAuth(pamh): """Start authentication in a seperate process""" + # Abort is Howdy is disabled + if config.get("core", "disabled") == "true": + sys.exit(0) + # Run compare as python3 subprocess to circumvent python version and import issues status = subprocess.call(["python3", os.path.dirname(os.path.abspath(__file__)) + "/compare.py", pamh.get_user()])