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 /server | |
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.
Diffstat (limited to 'server')
-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 | ||