Skip to content

Commit d8a09e7

Browse files
committed
fix: delay prompting user in URLLoader plug-in
At the time the URLLoader plug-in is instantiated, we don't know if the URLAccountLoader will even need to make an HTTP call as it may have cached data. So, if the user has not specified a password via CLI args, env vars, or config file, we don't want to prematurely prompt them unless URLAccountLoader really needs to make an HTTP request.
1 parent db0d4e6 commit d8a09e7

File tree

1 file changed

+47
-18
lines changed

1 file changed

+47
-18
lines changed

src/awsrun/plugins/accts/__init__.py

+47-18
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
import os
4747
from pathlib import Path
4848

49-
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
49+
from requests.auth import AuthBase, HTTPBasicAuth, HTTPDigestAuth
5050
from requests_ntlm import HttpNtlmAuth
5151

5252
from awsrun.acctload import (
@@ -903,31 +903,30 @@ def instantiate(self, args):
903903
if args.loader_password:
904904
auth_options["password"] = args.loader_password
905905

906-
# If they don't exist in user config, then pick defaults
907-
if "username" not in auth_options:
908-
auth_options["username"] = _default_username()
909-
if "password" not in auth_options:
910-
auth_options["password"] = _default_password(auth_options["username"])
906+
# If a username and password has not been provided via CLI flags
907+
# or via the configuration file, we'll fallback to environment
908+
# variables if they exist, or lastly we'll prompt the user
909+
# interactively. BUT, we don't want to do that here because the
910+
# URLAccountLoader caches data, so we might not need to make an
911+
# HTTP call, and thus prompting the user would be unneeded (and
912+
# annoying). So, instead, we use DeferPrompting to wrap the
913+
# various requests HTTP*Auth classes. This will defer the
914+
# instantiation of those classes until `requests` invokes the
915+
# callable `auth` parameter to its various methods.
911916

912917
if args.loader_auth == "oauth2" and "token_url" not in auth_options:
913918
raise TypeError(
914919
"with oauth2 authentication token_url must be set in config: Accounts->options->auth_options->token_url"
915920
)
916921

917922
auth_types = {
918-
"none": _HTTPNone,
919-
"basic": HTTPBasicAuth,
920-
"digest": HTTPDigestAuth,
921-
"ntlm": HttpNtlmAuth,
922-
"oauth2": HTTPOAuth2,
923+
"none": _HTTPNone(),
924+
"basic": _DeferPrompting(HTTPBasicAuth, auth_options),
925+
"digest": _DeferPrompting(HTTPDigestAuth, auth_options),
926+
"ntlm": _DeferPrompting(HttpNtlmAuth, auth_options),
927+
"oauth2": _DeferPrompting(HTTPOAuth2, auth_options),
923928
}
924-
925-
try:
926-
auth = auth_types[args.loader_auth](**auth_options)
927-
except TypeError as e:
928-
raise TypeError(
929-
f"incompatible auth_options specified in config: Accounts->options->auth_options: {e}"
930-
)
929+
auth = auth_types[args.loader_auth]
931930

932931
parsers = {
933932
"json": JSONParser,
@@ -970,6 +969,36 @@ def _default_password(user):
970969
)
971970

972971

972+
# Helper class to wrap one of `requests` auth classes to defer instantiation
973+
# of those classes until `requests` actually needs to use the auth data. This
974+
# is used to avoid interactively prompting a user for their password if one
975+
# had not been specified via CLI args or their config file. The default will
976+
# come from the env variable if set, otherwise the user is prompted. But we
977+
# don't want to prompt when we instantiated the `requests` auth classes
978+
# because at that time, we do not know if the account loader data has been
979+
# cached and thus not require making an HTTP call. So, why bother prompting
980+
# the user in that case?
981+
class _DeferPrompting(AuthBase):
982+
def __init__(self, auth_class, auth_options):
983+
self.auth_class = auth_class
984+
self.auth_options = auth_options
985+
986+
def __call__(self, req):
987+
if "username" not in self.auth_options:
988+
self.auth_options["username"] = _default_username()
989+
if "password" not in self.auth_options:
990+
self.auth_options["password"] = _default_password(
991+
self.auth_options["username"]
992+
)
993+
try:
994+
auth = self.auth_class(**self.auth_options)
995+
except TypeError as e:
996+
raise TypeError(
997+
f"incompatible auth_options specified in config: Accounts->options->auth_options: {e}"
998+
)
999+
return auth(req)
1000+
1001+
9731002
class _HTTPNone:
9741003
"""HTTPNone is a no-op auth type for requests library."""
9751004

0 commit comments

Comments
 (0)