44__author__ = 'Simon Robinson'
55__copyright__ = 'Copyright (c) 2022 Simon Robinson'
66__license__ = 'Apache 2.0'
7- __version__ = '2022-10-16 ' # ISO 8601 (YYYY-MM-DD)
7+ __version__ = '2022-10-24 ' # ISO 8601 (YYYY-MM-DD)
88
99import argparse
1010import base64
@@ -304,6 +304,11 @@ def accounts():
304304 AppConfig .get () # make sure config is loaded
305305 return AppConfig ._ACCOUNTS
306306
307+ @staticmethod
308+ def add_account (username ):
309+ AppConfig ._PARSER .add_section (username )
310+ AppConfig ._ACCOUNTS = [s for s in AppConfig ._PARSER .sections () if '@' in s ]
311+
307312 @staticmethod
308313 def save ():
309314 if AppConfig ._LOADED :
@@ -317,23 +322,35 @@ def get_oauth2_credentials(username, password, recurse_retries=True):
317322 """Using the given username (i.e., email address) and password, reads account details from AppConfig and
318323 handles OAuth 2.0 token request and renewal, saving the updated details back to AppConfig (or removing them
319324 if invalid). Returns either (True, '[OAuth2 string for authentication]') or (False, '[Error message]')"""
320- if username not in AppConfig .accounts ():
325+
326+ # we support broader catch-all account names (e.g., `@domain.com` / `@`) if enabled
327+ valid_accounts = [username in AppConfig .accounts ()]
328+ if AppConfig .globals ().getboolean ('allow_catch_all_accounts' , fallback = False ):
329+ user_domain = '@%s' % username .split ('@' )[- 1 ]
330+ valid_accounts .extend ([account in AppConfig .accounts () for account in [user_domain , '@' ]])
331+
332+ if not any (valid_accounts ):
321333 Log .error ('Proxy config file entry missing for account' , username , '- aborting login' )
322334 return (False , '%s: No config file entry found for account %s - please add a new section with values '
323335 'for permission_url, token_url, oauth2_scope, redirect_uri, client_id and '
324336 'client_secret' % (APP_NAME , username ))
325337
326338 config = AppConfig .get ()
327- current_time = int (time .time ())
328339
329- permission_url = config .get (username , 'permission_url' , fallback = None )
330- token_url = config .get (username , 'token_url' , fallback = None )
331- oauth2_scope = config .get (username , 'oauth2_scope' , fallback = None )
332- redirect_uri = config .get (username , 'redirect_uri' , fallback = None )
333- redirect_listen_address = config .get (username , 'redirect_listen_address' , fallback = None )
334- client_id = config .get (username , 'client_id' , fallback = None )
335- client_secret = config .get (username , 'client_secret' , fallback = None )
336- client_secret_encrypted = config .get (username , 'client_secret_encrypted' , fallback = None )
340+ def get_account_with_catch_all_fallback (option ):
341+ fallback = None
342+ if AppConfig .globals ().getboolean ('allow_catch_all_accounts' , fallback = False ):
343+ fallback = config .get (user_domain , option , fallback = config .get ('@' , option , fallback = None ))
344+ return config .get (username , option , fallback = fallback )
345+
346+ permission_url = get_account_with_catch_all_fallback ('permission_url' )
347+ token_url = get_account_with_catch_all_fallback ('token_url' )
348+ oauth2_scope = get_account_with_catch_all_fallback ('oauth2_scope' )
349+ redirect_uri = get_account_with_catch_all_fallback ('redirect_uri' )
350+ redirect_listen_address = get_account_with_catch_all_fallback ('redirect_listen_address' )
351+ client_id = get_account_with_catch_all_fallback ('client_id' )
352+ client_secret = get_account_with_catch_all_fallback ('client_secret' )
353+ client_secret_encrypted = get_account_with_catch_all_fallback ('client_secret_encrypted' )
337354
338355 # note that we don't require permission_url here because it is not needed for the client credentials grant flow,
339356 # and likewise for client_secret here because it can be optional for Office 365 configurations
@@ -355,6 +372,7 @@ def get_oauth2_credentials(username, password, recurse_retries=True):
355372 'are using an Office 365 setup that does not need a secret, please delete this line entirely;' ,
356373 'otherwise, if authentication fails, please double-check this value is correct' )
357374
375+ current_time = int (time .time ())
358376 token_salt = config .get (username , 'token_salt' , fallback = None )
359377 access_token = config .get (username , 'access_token' , fallback = None )
360378 access_token_expiry = config .getint (username , 'access_token_expiry' , fallback = current_time )
@@ -413,6 +431,8 @@ def get_oauth2_credentials(username, password, recurse_retries=True):
413431 client_secret , auth_code , oauth2_scope )
414432
415433 access_token = response ['access_token' ]
434+ if not config .has_section (username ):
435+ AppConfig .add_account (username ) # in wildcard mode the section may not yet exist
416436 config .set (username , 'token_salt' , token_salt )
417437 config .set (username , 'access_token' , OAuth2Helper .encrypt (fernet , access_token ))
418438 config .set (username , 'access_token_expiry' , str (current_time + response ['expires_in' ]))
@@ -425,6 +445,8 @@ def get_oauth2_credentials(username, password, recurse_retries=True):
425445
426446 if AppConfig .globals ().getboolean ('encrypt_client_secret_on_first_use' , fallback = False ):
427447 if client_secret :
448+ # note: save to the `username` entry even if `user_domain` exists, avoiding conflicts when using
449+ # incompatible `encrypt_client_secret_on_first_use` and `allow_catch_all_accounts` options
428450 config .set (username , 'client_secret_encrypted' , OAuth2Helper .encrypt (fernet , client_secret ))
429451 config .remove_option (username , 'client_secret' )
430452
@@ -1945,8 +1967,17 @@ def create_config_menu(self):
19451967 if len (config_accounts ) <= 0 :
19461968 items .append (pystray .MenuItem (' No accounts configured' , None , enabled = False ))
19471969 else :
1970+ catch_all_enabled = AppConfig .globals ().getboolean ('allow_catch_all_accounts' , fallback = False )
1971+ catch_all_accounts = []
19481972 for account in config_accounts :
1949- items .append (pystray .MenuItem (App .get_last_activity (account ), None , enabled = False ))
1973+ if account .startswith ('@' ) and catch_all_enabled :
1974+ catch_all_accounts .append (account )
1975+ else :
1976+ items .append (pystray .MenuItem (App .get_last_activity (account ), None , enabled = False ))
1977+ if len (catch_all_accounts ) > 0 :
1978+ items .append (pystray .MenuItem ('Catch-all accounts:' , None , enabled = False ))
1979+ for account in catch_all_accounts :
1980+ items .append (pystray .MenuItem (' %s' % account , None , enabled = False ))
19501981 if not sys .platform == 'darwin' :
19511982 items .append (pystray .MenuItem (' Refresh activity data' , self .icon .update_menu ))
19521983 items .append (pystray .Menu .SEPARATOR )
0 commit comments