summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSam Chudnick <sam@chudnick.com>2022-07-02 15:45:09 -0400
committerSam Chudnick <sam@chudnick.com>2022-07-02 15:45:09 -0400
commita9b5d5eb0fe72931757d3d989ec0a74986f36315 (patch)
treec9fa29ec9ba9e4329fdc7be411c0c00f30efe585
parent8472b394ee44cd46cc36fd4fe0a4882364cab602 (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-xserver/mfac.py192
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
4import sqlite3 4import sqlite3
5import pyotp 5import pyotp
6import sys 6import sys
7import os
8import configparser
7 9
8DB_NAME = "mfa.db"
9CLIENT_KEY_LENGTH = 64 10CLIENT_KEY_LENGTH = 64
10TOTP_KEY_LENGTH = 24 11TOTP_KEY_LENGTH = 24
11 12
@@ -19,28 +20,38 @@ def die(msg):
19 20
20def parse_arguments(): 21def 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
41def alias_exists(alias): 52def 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
53def get_client_info(alias,info): 64def 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
81def 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
106def 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
120def 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
68def update_totp(alias): 135def 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
80def add_client(alias): 147def 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
95def add_app(username, hostname, service, alias, mfa_methods): 165
96 if not alias_exists(alias): 166def 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
177def 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
188def read_config(config):
189 parser = configparser.ConfigParser(inline_comment_prefixes="#")
190 parser.read(config)
191 return parser
192
193
194def 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
106def main(): 211def 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