diff options
| author | Sam Chudnick <sam@chudnick.com> | 2022-07-02 15:45:09 -0400 |
|---|---|---|
| committer | Sam Chudnick <sam@chudnick.com> | 2022-07-02 15:45:09 -0400 |
| commit | a9b5d5eb0fe72931757d3d989ec0a74986f36315 (patch) | |
| tree | c9fa29ec9ba9e4329fdc7be411c0c00f30efe585 | |
| parent | 8472b394ee44cd46cc36fd4fe0a4882364cab602 (diff) | |
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.
| -rwxr-xr-x | server/mfac.py | 192 |
1 files changed, 151 insertions, 41 deletions
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 | |||
| 4 | import sqlite3 | 4 | import sqlite3 |
| 5 | import pyotp | 5 | import pyotp |
| 6 | import sys | 6 | import sys |
| 7 | import os | ||
| 8 | import configparser | ||
| 7 | 9 | ||
| 8 | DB_NAME = "mfa.db" | ||
| 9 | CLIENT_KEY_LENGTH = 64 | 10 | CLIENT_KEY_LENGTH = 64 |
| 10 | TOTP_KEY_LENGTH = 24 | 11 | TOTP_KEY_LENGTH = 24 |
| 11 | 12 | ||
| @@ -19,28 +20,38 @@ def die(msg): | |||
| 19 | 20 | ||
| 20 | def parse_arguments(): | 21 | def parse_arguments(): |
| 21 | parser = argparse.ArgumentParser() | 22 | parser = argparse.ArgumentParser() |
| 22 | parser.add_argument("--alias",type=str,help="Alias for new client") | 23 | parser.add_argument("--config",type=str,help="Config file location",\ |
| 24 | default="/etc/mfa/mfa.conf") | ||
| 25 | parser.add_argument("--database",type=str,help="Database location") | ||
| 23 | 26 | ||
| 24 | parser.add_argument("--add-client",action="store_true",help="Add a client") | 27 | parser.add_argument("--add-client",type=str,nargs="+",help="Add a client") |
| 25 | 28 | ||
| 26 | parser.add_argument("--add-app",action="store_true",help="Add an application") | 29 | parser.add_argument("--add-app",action="store_true",help="Add an application") |
| 27 | parser.add_argument("--user",type=str,help="Application username") | 30 | parser.add_argument("--user",type=str,help="Application username") |
| 28 | parser.add_argument("--host",type=str,help="Application hostname") | 31 | parser.add_argument("--host",type=str,help="Application hostname") |
| 29 | parser.add_argument("--service",type=str,help="Application service name") | 32 | parser.add_argument("--service",type=str,help="Application service name") |
| 33 | parser.add_argument("--alias",type=str,help="Alias for new client") | ||
| 30 | parser.add_argument("--methods",type=str,nargs="+",help="Allowed MFA methods") | 34 | parser.add_argument("--methods",type=str,nargs="+",help="Allowed MFA methods") |
| 31 | 35 | ||
| 32 | parser.add_argument("--get-client",action="store_true",help="Get a client key") | 36 | parser.add_argument("--get-client",action="store_true",help="Get a client key") |
| 37 | parser.add_argument("--show-secret",action="store_true",help="Show TOTP secrets") | ||
| 38 | |||
| 39 | parser.add_argument("--delete-client",nargs="+",help="Delete given clients") | ||
| 33 | 40 | ||
| 34 | parser.add_argument("--update-totp",action="store_true",help="Update a TOTP") | 41 | parser.add_argument("--update-totp",action="store_true",help="Update a TOTP") |
| 35 | 42 | ||
| 36 | parser.add_argument("--get-totp",action="store_true",help="Get client's TOTP") | 43 | parser.add_argument("--get-totp",action="store_true",help="Get client's TOTP") |
| 37 | 44 | ||
| 45 | parser.add_argument("--get-app",action="store_true",help="Get provisioned apps") | ||
| 46 | |||
| 47 | parser.add_argument("--delete-app",action="store_true",help="Delete application") | ||
| 48 | |||
| 38 | return parser.parse_args() | 49 | return parser.parse_args() |
| 39 | 50 | ||
| 40 | 51 | ||
| 41 | def alias_exists(alias): | 52 | def alias_exists(db,alias): |
| 42 | client = None | 53 | client = None |
| 43 | with sqlite3.connect(DB_NAME) as conn: | 54 | with sqlite3.connect(db) as conn: |
| 44 | c = conn.cursor() | 55 | c = conn.cursor() |
| 45 | c.execute("SELECT * FROM clients WHERE alias=?",(alias,)) | 56 | c.execute("SELECT * FROM clients WHERE alias=?",(alias,)) |
| 46 | client = c.fetchall() | 57 | client = c.fetchall() |
| @@ -50,12 +61,68 @@ def alias_exists(alias): | |||
| 50 | return True | 61 | return True |
| 51 | 62 | ||
| 52 | 63 | ||
| 53 | def get_client_info(alias,info): | 64 | def delete_app(db,alias,user,host,service): |
| 54 | if not alias_exists(alias): | 65 | get_app(db,alias,user,host,service,None) |
| 66 | confirm = input("These applications will be deleted. Continue [y/N]") | ||
| 67 | if confirm != "y": | ||
| 68 | die("operation cancelled") | ||
| 69 | with sqlite3.connect(db) as conn: | ||
| 70 | data = [alias,user,host,service] | ||
| 71 | for var in data: | ||
| 72 | if var == None: | ||
| 73 | data[data.index(var)] = '%' | ||
| 74 | c = conn.cursor() | ||
| 75 | c.execute("""DELETE FROM applications WHERE alias LIKE ? AND | ||
| 76 | username LIKE ? AND hostname LIKE ? AND service LIKE ? """,\ | ||
| 77 | (data[0],data[1],data[2],data[3])) | ||
| 78 | print("applications deleted") | ||
| 79 | |||
| 80 | |||
| 81 | def get_app(db,alias,user,host,service,methods): | ||
| 82 | # If variable is not defined set to % to get all results | ||
| 83 | if methods != None: | ||
| 84 | methods = " ".join(methods) | ||
| 85 | data = [alias,user,host,service,methods] | ||
| 86 | for var in data: | ||
| 87 | if var == None: | ||
| 88 | data[data.index(var)] = '%' | ||
| 89 | with sqlite3.connect(db) as conn: | ||
| 90 | c = conn.cursor() | ||
| 91 | c.execute("""SELECT * FROM applications WHERE alias LIKE ? AND | ||
| 92 | username LIKE ? AND hostname LIKE ? AND service LIKE ? and | ||
| 93 | mfa_methods LIKE ?""",\ | ||
| 94 | (data[0],data[1],data[2],data[3],data[4])) | ||
| 95 | apps = c.fetchall() | ||
| 96 | for app in apps: | ||
| 97 | print("username: " + app[0]) | ||
| 98 | print("hostname: " + app[1]) | ||
| 99 | print("service: " + app[2]) | ||
| 100 | print("alias: " + app[3]) | ||
| 101 | print("mfa methods: " + app[4]) | ||
| 102 | print("") | ||
| 103 | |||
| 104 | |||
| 105 | |||
| 106 | def get_clients(db,show=False): | ||
| 107 | with sqlite3.connect(db) as conn: | ||
| 108 | c = conn.cursor() | ||
| 109 | c.execute("SELECT * FROM clients") | ||
| 110 | clients = c.fetchall() | ||
| 111 | for client in clients: | ||
| 112 | print("alias: " + client[0]) | ||
| 113 | print("key: " + client[1]) | ||
| 114 | if show: | ||
| 115 | print("totp secret: " + client[2]) | ||
| 116 | print("") | ||
| 117 | |||
| 118 | |||
| 119 | |||
| 120 | def get_client_info(db,alias,info): | ||
| 121 | if not alias_exists(db, alias): | ||
| 55 | die("Error: alias does not exist") | 122 | die("Error: alias does not exist") |
| 56 | else: | 123 | else: |
| 57 | client = None | 124 | client = None |
| 58 | with sqlite3.connect(DB_NAME) as conn: | 125 | with sqlite3.connect(db) as conn: |
| 59 | c = conn.cursor() | 126 | c = conn.cursor() |
| 60 | c.execute("SELECT * FROM clients WHERE alias=?",(alias,)) | 127 | c.execute("SELECT * FROM clients WHERE alias=?",(alias,)) |
| 61 | client = c.fetchone() | 128 | client = c.fetchone() |
| @@ -65,10 +132,10 @@ def get_client_info(alias,info): | |||
| 65 | return str(client[CLIENT_TOTP_INDEX]) | 132 | return str(client[CLIENT_TOTP_INDEX]) |
| 66 | 133 | ||
| 67 | 134 | ||
| 68 | def update_totp(alias): | 135 | def update_totp(db,alias): |
| 69 | if alias_exists(alias): | 136 | if alias_exists(db, alias): |
| 70 | totp_secret = pyotp.random_base32(TOTP_KEY_LENGTH) | 137 | totp_secret = pyotp.random_base32(TOTP_KEY_LENGTH) |
| 71 | with sqlite3.connect(DB_NAME) as conn: | 138 | with sqlite3.connect(db) as conn: |
| 72 | c = conn.cursor() | 139 | c = conn.cursor() |
| 73 | c.execute("UPDATE clients SET totp_secret=? WHERE alias=?",\ | 140 | c.execute("UPDATE clients SET totp_secret=? WHERE alias=?",\ |
| 74 | (totp_secret,alias)) | 141 | (totp_secret,alias)) |
| @@ -77,42 +144,76 @@ def update_totp(alias): | |||
| 77 | die("error: alias does not exist") | 144 | die("error: alias does not exist") |
| 78 | 145 | ||
| 79 | 146 | ||
| 80 | def add_client(alias): | 147 | def add_client(db,aliases): |
| 81 | if not alias_exists(alias): | 148 | for alias in aliases: |
| 82 | client_key = pyotp.random_base32(CLIENT_KEY_LENGTH) | 149 | if not alias_exists(db, alias): |
| 83 | totp_secret = pyotp.random_base32(TOTP_KEY_LENGTH) | 150 | client_key = pyotp.random_base32(CLIENT_KEY_LENGTH) |
| 84 | with sqlite3.connect(DB_NAME) as conn: | 151 | totp_secret = pyotp.random_base32(TOTP_KEY_LENGTH) |
| 85 | c = conn.cursor() | 152 | with sqlite3.connect(db) as conn: |
| 86 | c.execute("INSERT INTO clients VALUES (?,?,?)",\ | 153 | c = conn.cursor() |
| 87 | (alias,client_key,totp_secret)) | 154 | c.execute("INSERT INTO clients VALUES (?,?,?)",\ |
| 155 | (alias,client_key,totp_secret)) | ||
| 88 | 156 | ||
| 89 | print("client key: " + client_key) | 157 | print("alias: " + alias) |
| 90 | print("totp secret: " + totp_secret) | 158 | print("client key: " + client_key) |
| 91 | print("uri: " + pyotp.TOTP(totp_secret).provisioning_uri(alias+"@mfad")) | 159 | print("totp secret: " + totp_secret) |
| 92 | else: | 160 | print("uri: " + pyotp.TOTP(totp_secret).provisioning_uri(alias+"@mfad")) |
| 93 | die("Error: alias already used") | 161 | print("") |
| 162 | else: | ||
| 163 | print("error: " + alias + " already used") | ||
| 94 | 164 | ||
| 95 | def add_app(username, hostname, service, alias, mfa_methods): | 165 | |
| 96 | if not alias_exists(alias): | 166 | def delete_client(db,aliases): |
| 167 | for alias in aliases: | ||
| 168 | if alias_exists(db,alias): | ||
| 169 | with sqlite3.connect(db) as conn: | ||
| 170 | c = conn.cursor() | ||
| 171 | c.execute("DELETE FROM clients WHERE alias=?",(alias,)) | ||
| 172 | print(alias + " deleted") | ||
| 173 | else: | ||
| 174 | die("error: alias does not exist") | ||
| 175 | |||
| 176 | |||
| 177 | def add_app(db,username, hostname, service, alias, mfa_methods): | ||
| 178 | if not alias_exists(db, alias): | ||
| 97 | die("Error: alias does not exist") | 179 | die("Error: alias does not exist") |
| 98 | else: | 180 | else: |
| 99 | client_key = get_client_info(alias, "key") | 181 | client_key = get_client_info(db, alias, "key") |
| 100 | with sqlite3.connect(DB_NAME) as conn: | 182 | with sqlite3.connect(db) as conn: |
| 101 | c = conn.cursor() | 183 | c = conn.cursor() |
| 102 | c.execute("INSERT INTO applications VALUES (?,?,?,?,?)", | 184 | c.execute("INSERT INTO applications VALUES (?,?,?,?,?)", |
| 103 | (username,hostname,service,alias,mfa_methods)) | 185 | (username,hostname,service,alias,mfa_methods)) |
| 104 | 186 | ||
| 105 | 187 | ||
| 188 | def read_config(config): | ||
| 189 | parser = configparser.ConfigParser(inline_comment_prefixes="#") | ||
| 190 | parser.read(config) | ||
| 191 | return parser | ||
| 192 | |||
| 193 | |||
| 194 | def get_vars(args,confparser): | ||
| 195 | if not os.path.exists(args.config): | ||
| 196 | die("Unable to open config file") | ||
| 197 | |||
| 198 | database = None | ||
| 199 | # Set values from config file first | ||
| 200 | if confparser.has_section("mfad"): | ||
| 201 | database = confparser.get("mfad","database",fallback=None) | ||
| 202 | # Let command line args overwrite any values | ||
| 203 | if args.database: | ||
| 204 | database = args.database | ||
| 205 | # Exit if any value is null | ||
| 206 | if database == None: | ||
| 207 | die("error: no database file given") | ||
| 208 | |||
| 209 | return database | ||
| 210 | |||
| 106 | def main(): | 211 | def main(): |
| 107 | args = parse_arguments() | 212 | args = parse_arguments() |
| 213 | confparser = read_config(args.config) | ||
| 214 | db = get_vars(args,confparser) | ||
| 215 | |||
| 108 | # Sanity checks | 216 | # Sanity checks |
| 109 | if (args.add_client and args.add_app) or (args.add_client and args.get_client) \ | ||
| 110 | or (args.get_client and args.add_app): | ||
| 111 | die("Error: cannot specify multiple actions") | ||
| 112 | if args.add_client and args.alias == None: | ||
| 113 | die("Error: must specify alias to provision a client") | ||
| 114 | if args.get_client and args.alias == None: | ||
| 115 | die("Error: no alias specified") | ||
| 116 | if args.update_totp and args.alias == None: | 217 | if args.update_totp and args.alias == None: |
| 117 | die("Error: no alias specified") | 218 | die("Error: no alias specified") |
| 118 | if args.get_totp and args.alias == None: | 219 | if args.get_totp and args.alias == None: |
| @@ -122,20 +223,29 @@ def main(): | |||
| 122 | or args.methods == None): | 223 | or args.methods == None): |
| 123 | die("Error: --add-app requires all of --user,--host,--service,--alias,--methods") | 224 | die("Error: --add-app requires all of --user,--host,--service,--alias,--methods") |
| 124 | 225 | ||
| 125 | if args.add_client: | 226 | |
| 126 | add_client(args.alias) | 227 | if args.add_client != None: |
| 228 | add_client(db,args.add_client) | ||
| 229 | elif args.get_client and args.alias == None: | ||
| 230 | key = get_clients(db,args.show_secret) | ||
| 127 | elif args.get_client: | 231 | elif args.get_client: |
| 128 | key = get_client_info(args.alias,"key") | 232 | key = get_client_info(db,args.alias,"key") |
| 129 | print(key) | 233 | print(key) |
| 130 | elif args.add_app: | 234 | elif args.add_app: |
| 131 | methods = " ".join(args.methods) | 235 | methods = " ".join(args.methods) |
| 132 | add_app(args.user,args.host,args.service,args.alias,methods) | 236 | add_app(db,args.user,args.host,args.service,args.alias,methods) |
| 133 | elif args.update_totp: | 237 | elif args.update_totp: |
| 134 | update_totp(args.alias) | 238 | update_totp(db,args.alias) |
| 135 | elif args.get_totp: | 239 | elif args.get_totp: |
| 136 | secret = get_client_info(args.alias,"totp") | 240 | secret = get_client_info(db,args.alias,"totp") |
| 137 | print(secret) | 241 | print(secret) |
| 138 | print(pyotp.TOTP(secret).provisioning_uri(args.alias+"@mfad")) | 242 | print(pyotp.TOTP(secret).provisioning_uri(args.alias+"@mfad")) |
| 243 | elif args.get_app: | ||
| 244 | get_app(db,args.alias,args.user,args.host,args.service,args.methods) | ||
| 245 | elif args.delete_client: | ||
| 246 | delete_client(db,args.delete_client) | ||
| 247 | elif args.delete_app: | ||
| 248 | delete_app(db,args.alias,args.user,args.host,args.service) | ||
| 139 | 249 | ||
| 140 | 250 | ||
| 141 | 251 | ||
