Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement pluggable auth interactive mode #1131

Merged
merged 45 commits into from
Sep 28, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
a65c1e4
feat: implementation of pluggable auth interactive mode
BigTailWolf Aug 30, 2022
6fcce53
adding test for interactive mode
BigTailWolf Aug 31, 2022
3ea5799
addressing comments
BigTailWolf Sep 2, 2022
530442b
Merge branch 'main' into b237591436
BigTailWolf Sep 2, 2022
1076877
Merge branch 'main' into b237591436
BigTailWolf Sep 6, 2022
9eec181
move interactive source of truth to kwargs in constructor and make ch…
BigTailWolf Sep 6, 2022
97fb34a
implement revoke method and refactor some constants
BigTailWolf Sep 6, 2022
95b6ce6
adding 2.7 check in revoke()
BigTailWolf Sep 7, 2022
36b7709
fix lint
BigTailWolf Sep 7, 2022
1650492
chore: update token
BigTailWolf Sep 7, 2022
2795076
include error return code in exception
BigTailWolf Sep 7, 2022
f361340
unify the expiration_time check behavior.
BigTailWolf Sep 8, 2022
2a7d288
fix lint
BigTailWolf Sep 8, 2022
b15b9d5
addressing comments
BigTailWolf Sep 8, 2022
1460d6f
chore: update token
BigTailWolf Sep 8, 2022
9b9ca69
adding environment injection for revoke
BigTailWolf Sep 9, 2022
1dce93a
Revert "unify the expiration_time check behavior."
BigTailWolf Sep 9, 2022
e0d2099
adding stdin/out to revoke subprocess in case some prompts may needed
BigTailWolf Sep 9, 2022
e48cc4a
unifying the behavior of reading response for non-interactive and int…
BigTailWolf Sep 12, 2022
27648e7
adding the logic of getting external account id
BigTailWolf Sep 12, 2022
2515fc7
update token
BigTailWolf Sep 12, 2022
7d3e7bf
explicity inject subprocess environment variables
BigTailWolf Sep 13, 2022
a703e57
using a dummy value instead of None as a temporary solution
BigTailWolf Sep 13, 2022
38267f1
fix environment variable injection
BigTailWolf Sep 13, 2022
5b242db
chore: update token
BigTailWolf Sep 13, 2022
5082d5a
addressing comments
BigTailWolf Sep 15, 2022
be691f9
fix lint
BigTailWolf Sep 15, 2022
1a39155
chore: update token
BigTailWolf Sep 16, 2022
f18009a
refactor the common code between retrieve_subject_token and revoke
BigTailWolf Sep 19, 2022
65869a2
chore: update token
BigTailWolf Sep 20, 2022
5fcad73
Merge branch 'main' into b237591436
BigTailWolf Sep 20, 2022
7189863
Merge branch 'main' into b237591436
lsirac Sep 21, 2022
7b9451f
refactoring tests
BigTailWolf Sep 21, 2022
7c39fd8
chore: update token
BigTailWolf Sep 21, 2022
d8bd24a
Merge branch 'main' into b237591436
BigTailWolf Sep 22, 2022
7de8944
refactor tests
BigTailWolf Sep 22, 2022
06fff40
feat: Retry behavior (#1113)
clundin25 Sep 22, 2022
b4a1ae6
Revert "feat: Retry behavior (#1113)"
BigTailWolf Sep 22, 2022
4950562
Merge branch 'main' into b237591436
BigTailWolf Sep 22, 2022
2db7eaf
chore: update token
BigTailWolf Sep 23, 2022
ef4b8fb
Merge branch 'main' into b237591436
BigTailWolf Sep 26, 2022
a093a26
chore: update token
BigTailWolf Sep 26, 2022
e2f84c4
Merge branch 'main' into b237591436
BigTailWolf Sep 26, 2022
ba4e66b
Merge branch 'main' into b237591436
BigTailWolf Sep 27, 2022
8af219f
chore: update token
BigTailWolf Sep 27, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
107 changes: 83 additions & 24 deletions google/auth/pluggable.py
Expand Up @@ -38,6 +38,7 @@
import json
import os
import subprocess
import sys
import time

from google.auth import _helpers
Expand Down Expand Up @@ -105,6 +106,7 @@ def __init__(
raise ValueError(
"Missing credential_source. The credential_source is not a dict."
)
self._interactive = os.environ.get("GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE") == "1"
BigTailWolf marked this conversation as resolved.
Show resolved Hide resolved
self._credential_source_executable = credential_source.get("executable")
if not self._credential_source_executable:
raise ValueError(
Expand All @@ -116,6 +118,9 @@ def __init__(
self._credential_source_executable_timeout_millis = self._credential_source_executable.get(
"timeout_millis"
)
self._credential_source_executable_interactive_timeout_millis = self._credential_source_executable.get(
"interactive_timeout_millis"
BigTailWolf marked this conversation as resolved.
Show resolved Hide resolved
)
self._credential_source_executable_output_file = self._credential_source_executable.get(
"output_file"
)
Expand All @@ -132,6 +137,20 @@ def __init__(
):
raise ValueError("Timeout must be between 5 and 120 seconds.")

if not self._credential_source_executable_interactive_timeout_millis:
BigTailWolf marked this conversation as resolved.
Show resolved Hide resolved
self._credential_source_executable_interactive_timeout_millis = (
5 * 60 * 1000
BigTailWolf marked this conversation as resolved.
Show resolved Hide resolved
)
elif (
self._credential_source_executable_interactive_timeout_millis
< 5 * 60 * 1000
or self._credential_source_executable_interactive_timeout_millis
> 30 * 60 * 1000
):
raise ValueError("Interactive timeout must be between 5 and 30 minutes.")
if self._interactive and not self._credential_source_executable_output_file:
BigTailWolf marked this conversation as resolved.
Show resolved Hide resolved
raise ValueError("Output file must be specified in interactive mode")
BigTailWolf marked this conversation as resolved.
Show resolved Hide resolved

@_helpers.copy_docstring(external_account.Credentials)
def retrieve_subject_token(self, request):
env_allow_executables = os.environ.get(
Expand All @@ -155,7 +174,15 @@ def retrieve_subject_token(self, request):
try:
# If the cached response is expired, _parse_subject_token will raise an error which will be ignored and we will call the executable again.
subject_token = self._parse_subject_token(response)
if (
self._interactive and "expiration_time" not in response
BigTailWolf marked this conversation as resolved.
Show resolved Hide resolved
): # Always treat missing expiration_time as expired and proceed to executable run
BigTailWolf marked this conversation as resolved.
Show resolved Hide resolved
raise exceptions.RefreshError
except ValueError:
if (
self._interactive
BigTailWolf marked this conversation as resolved.
Show resolved Hide resolved
): # For any interactive mode errors in the latest run, we automatically ignore it and proceed to executable run
pass
raise
except exceptions.RefreshError:
pass
Expand All @@ -171,9 +198,10 @@ def retrieve_subject_token(self, request):
env = os.environ.copy()
env["GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE"] = self._audience
env["GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE"] = self._subject_token_type
env[
"GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"
] = "0" # Always set to 0 until interactive mode is implemented.
env["GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"] = (
BigTailWolf marked this conversation as resolved.
Show resolved Hide resolved
"1" if self._interactive else "0"
) # if variable not set, backfill to "0"
BigTailWolf marked this conversation as resolved.
Show resolved Hide resolved

BigTailWolf marked this conversation as resolved.
Show resolved Hide resolved
BigTailWolf marked this conversation as resolved.
Show resolved Hide resolved
if self._service_account_impersonation_url is not None:
env[
"GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL"
Expand All @@ -183,31 +211,61 @@ def retrieve_subject_token(self, request):
"GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE"
] = self._credential_source_executable_output_file

try:
result = subprocess.run(
self._credential_source_executable_command.split(),
timeout=self._credential_source_executable_timeout_millis / 1000,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
env=env,
)
if result.returncode != 0:
raise exceptions.RefreshError(
"Executable exited with non-zero return code {}. Error: {}".format(
result.returncode, result.stdout
)
if self._interactive:
BigTailWolf marked this conversation as resolved.
Show resolved Hide resolved
try:
result = subprocess.run(
self._credential_source_executable_command.split(),
timeout=self._credential_source_executable_interactive_timeout_millis
/ 1000,
stdin=sys.stdin,
stdout=sys.stdout,
stderr=sys.stdout,
env=env,
)
except Exception:
raise
if result.returncode != 0:
raise exceptions.RefreshError(
"Executable exited with non-zero return code {}.".format(
result.returncode
)
)
except Exception:
BigTailWolf marked this conversation as resolved.
Show resolved Hide resolved
raise
else:
try:
with open(
self._credential_source_executable_output_file, encoding="utf-8"
) as data:
response = json.load(data)
subject_token = self._parse_subject_token(response)
except Exception:
raise
return subject_token

else:
try:
data = result.stdout.decode("utf-8")
response = json.loads(data)
subject_token = self._parse_subject_token(response)
result = subprocess.run(
self._credential_source_executable_command.split(),
timeout=self._credential_source_executable_timeout_millis / 1000,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
env=env,
)
if result.returncode != 0:
raise exceptions.RefreshError(
"Executable exited with non-zero return code {}. Error: {}".format(
result.returncode, result.stdout
)
)
except Exception:
raise

return subject_token
else:
try:
data = result.stdout.decode("utf-8")
response = json.loads(data)
subject_token = self._parse_subject_token(response)
except Exception:
raise
return subject_token

@classmethod
def from_info(cls, info, **kwargs):
Expand Down Expand Up @@ -264,10 +322,11 @@ def _parse_subject_token(self, response):
)
if (
"expiration_time" not in response
and not self._interactive
and self._credential_source_executable_output_file
):
raise ValueError(
"The executable response must contain an expiration_time for successful responses when an output_file has been specified in the configuration."
"The executable response must contain an expiration_time for successful responses when an output_file has been specified in the configuration in non-interactive mode."
)
if "expiration_time" in response and response["expiration_time"] < time.time():
raise exceptions.RefreshError(
Expand Down