From a9b5d5eb0fe72931757d3d989ec0a74986f36315 Mon Sep 17 00:00:00 2001 From: Sam Chudnick Date: Sat, 2 Jul 2022 15:45:09 -0400 Subject: Read options from config file and more Read options from standardized configuration file but still prioritize command line options. Added several more commands: --get-app - list provisioned applications, can be filtered by additionally specifying any of --user,--host,--service,--alias --delete-client - delete a provisioned client --delete-app - delete a provisioned application, works the same way as --get-app so calling just --delete-app would request to delete all applications (confirmation is always requested first) Modified --add-client to accept arguments directly. Multiple aliases can be specified for bulk provisioning (--delete-client works the same way). Change --get-client so that no additional options lists all clients. Do not show TOTP secret by default and require --show-secret to do so. --- server/mfac.py | 192 +++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 151 insertions(+), 41 deletions(-) (limited to 'server') diff --git a/server/mfac.py b/server/mfac.py index 884c61c..b5837cf 100755 --- a/server/mfac.py +++ b/server/mfac.py @@ -4,8 +4,9 @@ import argparse import sqlite3 import pyotp import sys +import os +import configparser -DB_NAME = "mfa.db" CLIENT_KEY_LENGTH = 64 TOTP_KEY_LENGTH = 24 @@ -19,28 +20,38 @@ def die(msg): def parse_arguments(): parser = argparse.ArgumentParser() - parser.add_argument("--alias",type=str,help="Alias for new client") + parser.add_argument("--config",type=str,help="Config file location",\ + default="/etc/mfa/mfa.conf") + parser.add_argument("--database",type=str,help="Database location") - parser.add_argument("--add-client",action="store_true",help="Add a client") + parser.add_argument("--add-client",type=str,nargs="+",help="Add a client") parser.add_argument("--add-app",action="store_true",help="Add an application") parser.add_argument("--user",type=str,help="Application username") parser.add_argument("--host",type=str,help="Application hostname") parser.add_argument("--service",type=str,help="Application service name") + parser.add_argument("--alias",type=str,help="Alias for new client") parser.add_argument("--methods",type=str,nargs="+",help="Allowed MFA methods") parser.add_argument("--get-client",action="store_true",help="Get a client key") + parser.add_argument("--show-secret",action="store_true",help="Show TOTP secrets") + + parser.add_argument("--delete-client",nargs="+",help="Delete given clients") parser.add_argument("--update-totp",action="store_true",help="Update a TOTP") parser.add_argument("--get-totp",action="store_true",help="Get client's TOTP") + parser.add_argument("--get-app",action="store_true",help="Get provisioned apps") + + parser.add_argument("--delete-app",action="store_true",help="Delete application") + return parser.parse_args() -def alias_exists(alias): +def alias_exists(db,alias): client = None - with sqlite3.connect(DB_NAME) as conn: + with sqlite3.connect(db) as conn: c = conn.cursor() c.execute("SELECT * FROM clients WHERE alias=?",(alias,)) client = c.fetchall() @@ -50,12 +61,68 @@ def alias_exists(alias): return True -def get_client_info(alias,info): - if not alias_exists(alias): +def delete_app(db,alias,user,host,service): + get_app(db,alias,user,host,service,None) + confirm = input("These applications will be deleted. Continue [y/N]") + if confirm != "y": + die("operation cancelled") + with sqlite3.connect(db) as conn: + data = [alias,user,host,service] + for var in data: + if var == None: + data[data.index(var)] = '%' + c = conn.cursor() + c.execute("""DELETE FROM applications WHERE alias LIKE ? AND + username LIKE ? AND hostname LIKE ? AND service LIKE ? """,\ + (data[0],data[1],data[2],data[3])) + print("applications deleted") + + +def get_app(db,alias,user,host,service,methods): + # If variable is not defined set to % to get all results + if methods != None: + methods = " ".join(methods) + data = [alias,user,host,service,methods] + for var in data: + if var == None: + data[data.index(var)] = '%' + with sqlite3.connect(db) as conn: + c = conn.cursor() + c.execute("""SELECT * FROM applications WHERE alias LIKE ? AND + username LIKE ? AND hostname LIKE ? AND service LIKE ? and + mfa_methods LIKE ?""",\ + (data[0],data[1],data[2],data[3],data[4])) + apps = c.fetchall() + for app in apps: + print("username: " + app[0]) + print("hostname: " + app[1]) + print("service: " + app[2]) + print("alias: " + app[3]) + print("mfa methods: " + app[4]) + print("") + + + +def get_clients(db,show=False): + with sqlite3.connect(db) as conn: + c = conn.cursor() + c.execute("SELECT * FROM clients") + clients = c.fetchall() + for client in clients: + print("alias: " + client[0]) + print("key: " + client[1]) + if show: + print("totp secret: " + client[2]) + print("") + + + +def get_client_info(db,alias,info): + if not alias_exists(db, alias): die("Error: alias does not exist") else: client = None - with sqlite3.connect(DB_NAME) as conn: + with sqlite3.connect(db) as conn: c = conn.cursor() c.execute("SELECT * FROM clients WHERE alias=?",(alias,)) client = c.fetchone() @@ -65,10 +132,10 @@ def get_client_info(alias,info): return str(client[CLIENT_TOTP_INDEX]) -def update_totp(alias): - if alias_exists(alias): +def update_totp(db,alias): + if alias_exists(db, alias): totp_secret = pyotp.random_base32(TOTP_KEY_LENGTH) - with sqlite3.connect(DB_NAME) as conn: + with sqlite3.connect(db) as conn: c = conn.cursor() c.execute("UPDATE clients SET totp_secret=? WHERE alias=?",\ (totp_secret,alias)) @@ -77,42 +144,76 @@ def update_totp(alias): die("error: alias does not exist") -def add_client(alias): - if not alias_exists(alias): - client_key = pyotp.random_base32(CLIENT_KEY_LENGTH) - totp_secret = pyotp.random_base32(TOTP_KEY_LENGTH) - with sqlite3.connect(DB_NAME) as conn: - c = conn.cursor() - c.execute("INSERT INTO clients VALUES (?,?,?)",\ - (alias,client_key,totp_secret)) +def add_client(db,aliases): + for alias in aliases: + if not alias_exists(db, alias): + client_key = pyotp.random_base32(CLIENT_KEY_LENGTH) + totp_secret = pyotp.random_base32(TOTP_KEY_LENGTH) + with sqlite3.connect(db) as conn: + c = conn.cursor() + c.execute("INSERT INTO clients VALUES (?,?,?)",\ + (alias,client_key,totp_secret)) - print("client key: " + client_key) - print("totp secret: " + totp_secret) - print("uri: " + pyotp.TOTP(totp_secret).provisioning_uri(alias+"@mfad")) - else: - die("Error: alias already used") + print("alias: " + alias) + print("client key: " + client_key) + print("totp secret: " + totp_secret) + print("uri: " + pyotp.TOTP(totp_secret).provisioning_uri(alias+"@mfad")) + print("") + else: + print("error: " + alias + " already used") -def add_app(username, hostname, service, alias, mfa_methods): - if not alias_exists(alias): + +def delete_client(db,aliases): + for alias in aliases: + if alias_exists(db,alias): + with sqlite3.connect(db) as conn: + c = conn.cursor() + c.execute("DELETE FROM clients WHERE alias=?",(alias,)) + print(alias + " deleted") + else: + die("error: alias does not exist") + + +def add_app(db,username, hostname, service, alias, mfa_methods): + if not alias_exists(db, alias): die("Error: alias does not exist") else: - client_key = get_client_info(alias, "key") - with sqlite3.connect(DB_NAME) as conn: + client_key = get_client_info(db, alias, "key") + with sqlite3.connect(db) as conn: c = conn.cursor() c.execute("INSERT INTO applications VALUES (?,?,?,?,?)", (username,hostname,service,alias,mfa_methods)) +def read_config(config): + parser = configparser.ConfigParser(inline_comment_prefixes="#") + parser.read(config) + return parser + + +def get_vars(args,confparser): + if not os.path.exists(args.config): + die("Unable to open config file") + + database = None + # Set values from config file first + if confparser.has_section("mfad"): + database = confparser.get("mfad","database",fallback=None) + # Let command line args overwrite any values + if args.database: + database = args.database + # Exit if any value is null + if database == None: + die("error: no database file given") + + return database + def main(): args = parse_arguments() + confparser = read_config(args.config) + db = get_vars(args,confparser) + # Sanity checks - if (args.add_client and args.add_app) or (args.add_client and args.get_client) \ - or (args.get_client and args.add_app): - die("Error: cannot specify multiple actions") - if args.add_client and args.alias == None: - die("Error: must specify alias to provision a client") - if args.get_client and args.alias == None: - die("Error: no alias specified") if args.update_totp and args.alias == None: die("Error: no alias specified") if args.get_totp and args.alias == None: @@ -122,20 +223,29 @@ def main(): or args.methods == None): die("Error: --add-app requires all of --user,--host,--service,--alias,--methods") - if args.add_client: - add_client(args.alias) + + if args.add_client != None: + add_client(db,args.add_client) + elif args.get_client and args.alias == None: + key = get_clients(db,args.show_secret) elif args.get_client: - key = get_client_info(args.alias,"key") + key = get_client_info(db,args.alias,"key") print(key) elif args.add_app: methods = " ".join(args.methods) - add_app(args.user,args.host,args.service,args.alias,methods) + add_app(db,args.user,args.host,args.service,args.alias,methods) elif args.update_totp: - update_totp(args.alias) + update_totp(db,args.alias) elif args.get_totp: - secret = get_client_info(args.alias,"totp") + secret = get_client_info(db,args.alias,"totp") print(secret) print(pyotp.TOTP(secret).provisioning_uri(args.alias+"@mfad")) + elif args.get_app: + get_app(db,args.alias,args.user,args.host,args.service,args.methods) + elif args.delete_client: + delete_client(db,args.delete_client) + elif args.delete_app: + delete_app(db,args.alias,args.user,args.host,args.service) -- cgit v1.2.3