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

Feature/update to alpine 3.16 #269

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
4 changes: 2 additions & 2 deletions .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.6, 3.7, 3.8]
python-version: [3.6, 3.7, 3.8, 3.9, "3.10"]

steps:
- uses: actions/checkout@v2
Expand Down Expand Up @@ -44,4 +44,4 @@ jobs:
pip install pytest
coverage run --source=aws_google_auth/ --omit=aws_google_auth/tests/* setup.py test
coverage report
coveralls
coveralls
15 changes: 10 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
FROM alpine:3.5
FROM python:3.10-alpine3.16

# RUN apk add --update-cache py3-pip ca-certificates py3-certifi py3-lxml\
# python3-dev cython cython-dev libusb-dev build-base \
# eudev-dev linux-headers libffi-dev openssl-dev \
# jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev \
# tiff-dev tk-dev tcl-dev \
# py3-cryptography py3-wheel py3-pillow

RUN apk add --update-cache py3-pip ca-certificates py3-certifi py3-lxml\
python3-dev cython cython-dev libusb-dev build-base \
eudev-dev linux-headers libffi-dev openssl-dev \
jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev \
tiff-dev tk-dev tcl-dev
py3-cryptography py3-wheel py3-pillow py3-hidapi

COPY setup.py README.rst requirements.txt /build/
RUN pip3 install -r /build/requirements.txt
Expand All @@ -13,4 +17,5 @@ COPY aws_google_auth /build/aws_google_auth
RUN pip3 install -e /build/[u2f]

ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
ENV TZ=UTC
ENTRYPOINT ["aws-google-auth"]
15 changes: 11 additions & 4 deletions aws_google_auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,12 @@ def parse_args(args):
parser.add_argument('-S', '--sp-id', help='Google SSO SP identifier ($GOOGLE_SP_ID)')
parser.add_argument('-R', '--region', help='AWS region endpoint ($AWS_DEFAULT_REGION)')
duration_group = parser.add_mutually_exclusive_group()
duration_group.add_argument('-d', '--duration', type=int, help='Credential duration in seconds (defaults to value of $DURATION, then falls back to 43200)')
duration_group.add_argument('--auto-duration', action='store_true', help='Tries to use the longest allowed duration ($AUTO_DURATION)')
duration_group.add_argument('-d', '--duration', type=int,
help='Credential duration in seconds '
'(defaults to value of $DURATION, then falls back to 43200)')
duration_group.add_argument('--auto-duration', action='store_true',
help='Tries to use the longest allowed duration '
'($AUTO_DURATION)')
parser.add_argument('-p', '--profile', help='AWS profile (defaults to value of $AWS_PROFILE, then falls back to \'sts\')')
parser.add_argument('-A', '--account', help='Filter for specific AWS account.')
parser.add_argument('-D', '--disable-u2f', action='store_true', help='Disable U2F functionality.')
Expand All @@ -40,8 +44,11 @@ def parse_args(args):
parser.add_argument('--no-cache', dest="saml_cache", action='store_false', help='Do not cache the SAML Assertion.')
parser.add_argument('--print-creds', action='store_true', help='Print Credentials.')
parser.add_argument('--resolve-aliases', action='store_true', help='Resolve AWS account aliases.')
parser.add_argument('--save-failure-html', action='store_true', help='Write HTML failure responses to file for troubleshooting.')
parser.add_argument('--save-saml-flow', action='store_true', help='Write all GET and PUT requests and HTML responses to/from Google to files for troubleshooting.')
parser.add_argument('--save-failure-html', action='store_true',
help='Write HTML failure responses to file for troubleshooting.')
parser.add_argument('--save-saml-flow', action='store_true',
help='Write all GET and PUT requests and HTML responses to/from '
'Google to files for troubleshooting.')

role_group = parser.add_mutually_exclusive_group()
role_group.add_argument('-a', '--ask-role', action='store_true', help='Set true to always pick the role')
Expand Down
13 changes: 9 additions & 4 deletions aws_google_auth/amazon.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ def expiration(self):
return self.token['Credentials']['Expiration']

def print_export_line(self):
export_template = "export AWS_ACCESS_KEY_ID='{}' AWS_SECRET_ACCESS_KEY='{}' AWS_SESSION_TOKEN='{}' AWS_SESSION_EXPIRATION='{}'"
export_template = "export AWS_ACCESS_KEY_ID='{}' "
"AWS_SECRET_ACCESS_KEY='{}' "
"AWS_SESSION_TOKEN='{}' "
"AWS_SESSION_EXPIRATION='{}'"

formatted = export_template.format(
self.access_key_id,
Expand Down Expand Up @@ -98,10 +101,12 @@ def assume_role(self, role, principal, saml_assertion, duration=None, auto_durat
try:
res = self.sts_client.assume_role_with_saml(**sts_call_vars)
except ClientError as err:
if (err.response.get('Error', []).get('Code') == 'ValidationError' and err.response.get('Error', []).get('Message')):
error_code = err.response.get('Error', []).get('Code')
error_message = err.response.get('Error', []).get('Message')
if (error_code == 'ValidationError' and error_message):
m = re.search(
'Member must have value less than or equal to ([0-9]{3,5})',
err.response['Error']['Message']
error_message
)
if m is not None and m.group(1):
new_duration = int(m.group(1))
Expand Down Expand Up @@ -135,7 +140,7 @@ def resolve_aws_alias(role, principal, aws_dict):
response = iam.list_account_aliases()
account_alias = response['AccountAliases'][0]
aws_dict[role.split(':')[4]] = account_alias
except:
except Exception:
sts = session.client('sts',
aws_access_key_id=saml['Credentials']['AccessKeyId'],
aws_secret_access_key=saml['Credentials']['SecretAccessKey'],
Expand Down
33 changes: 23 additions & 10 deletions aws_google_auth/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,16 +104,22 @@ def raise_if_invalid(self):
# duration
assert (self.duration.__class__ is int), "Expected duration to be an integer. Got {}.".format(self.duration.__class__)
assert (self.duration >= 900), "Expected duration to be greater than or equal to 900. Got {}.".format(self.duration)
assert (self.duration <= self.max_duration), "Expected duration to be less than or equal to max_duration ({}). Got {}.".format(self.max_duration, self.duration)
assert (self.duration <= self.max_duration), \
"Expected duration to be less than or equal to max_duration ({}). Got {}."\
.format(self.max_duration, self.duration)

# auto_duration
assert (self.auto_duration.__class__ is bool), "Expected auto_duration to be a boolean. Got {}.".format(self.auto_duration.__class__)
assert (self.auto_duration.__class__ is bool), \
"Expected auto_duration to be a boolean. Got {}."\
.format(self.auto_duration.__class__)

# profile
assert (self.profile.__class__ is str), "Expected profile to be a string. Got {}.".format(self.profile.__class__)
assert (self.profile.__class__ is str), \
"Expected profile to be a string. Got {}.".format(self.profile.__class__)

# region
assert (self.region.__class__ is str), "Expected region to be a string. Got {}.".format(self.region.__class__)
assert (self.region.__class__ is str), \
"Expected region to be a string. Got {}.".format(self.region.__class__)

# idp_id
assert (self.idp_id is not None), "Expected idp_id to be set to non-None value."
Expand All @@ -129,16 +135,21 @@ def raise_if_invalid(self):
assert (type(self.password) in [str, unicode]), "Expected password to be a string. Got {}.".format(
type(self.password))
except NameError:
assert (type(self.password) is str), "Expected password to be a string. Got {}.".format(
type(self.password))
assert (type(self.password) is str), \
"Expected password to be a string. Got {}.".format(type(self.password))

# role_arn (Can be blank, we'll just prompt)
if self.role_arn is not None:
assert (self.role_arn.__class__ is str), "Expected role_arn to be None or a string. Got {}.".format(self.role_arn.__class__)
assert ("arn:aws:iam::" in self.role_arn or "arn:aws-us-gov:iam::" in self.role_arn), "Expected role_arn to contain 'arn:aws:iam::'. Got '{}'.".format(self.role_arn)
assert (self.role_arn.__class__ is str), \
"Expected role_arn to be None or a string. Got {}."\
.format(self.role_arn.__class__)
assert ("arn:aws:iam::" in self.role_arn or "arn:aws-us-gov:iam::" in self.role_arn), \
"Expected role_arn to contain 'arn:aws:iam::'. Got '{}'.".format(self.role_arn)

# u2f_disabled
assert (self.u2f_disabled.__class__ is bool), "Expected u2f_disabled to be a boolean. Got {}.".format(self.u2f_disabled.__class__)
assert (self.u2f_disabled.__class__ is bool), \
"Expected u2f_disabled to be a boolean. Got {}."\
.format(self.u2f_disabled.__class__)

# quiet
assert (self.quiet.__class__ is bool), "Expected quiet to be a boolean. Got {}.".format(self.quiet.__class__)
Expand Down Expand Up @@ -191,7 +202,9 @@ def write(self, amazon_object):
credentials_parser.set(self.profile, 'aws_access_key_id', amazon_object.access_key_id)
credentials_parser.set(self.profile, 'aws_secret_access_key', amazon_object.secret_access_key)
credentials_parser.set(self.profile, 'aws_security_token', amazon_object.session_token)
credentials_parser.set(self.profile, 'aws_session_expiration', amazon_object.expiration.strftime('%Y-%m-%dT%H:%M:%S%z'))
credentials_parser.set(self.profile,
'aws_session_expiration',
amazon_object.expiration.strftime('%Y-%m-%dT%H:%M:%S%z'))
credentials_parser.set(self.profile, 'aws_session_token', amazon_object.session_token)

with open(self.credentials_file, 'w+') as f:
Expand Down
27 changes: 21 additions & 6 deletions aws_google_auth/google.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@

from aws_google_auth import _version

# TODO: BeautifulSoup is still using the deprecated collections.Callable
# which has been removed in Python 3.10 and later. This dirty hack should
# allow things to continue working for a while
if sys.version_info.minor >= 10:
import collections
collections.Callable = collections.abc.Callable

# The U2F USB Library is optional, if it's there, include it.
try:
from aws_google_auth import u2f
Expand Down Expand Up @@ -190,7 +197,7 @@ def find_key_handles(input, challengeTxt):
base64UrlEncoded = base64.urlsafe_b64encode(base64.b64decode(item))
if base64UrlEncoded != challengeTxt: # make sure its not the challengeTxt - if it not return it
keyHandles.append(base64UrlEncoded)
except:
except Exception:
pass
return keyHandles

Expand All @@ -200,7 +207,7 @@ def find_app_id(inputString):
searchResult = re.search('"appid":"[a-z://.-_] + "', inputString).group()
searchObject = json.loads('{' + searchResult + '}')
return str(searchObject['appid'])
except:
except Exception:
logging.exception('Was unable to find appid value in googles SAML page')
sys.exit(1)

Expand Down Expand Up @@ -356,15 +363,17 @@ def parse_saml(self):
parsed = BeautifulSoup(self.session_state.text, 'html.parser')
try:
saml_element = parsed.find('input', {'name': 'SAMLResponse'}).get('value')
except:
except Exception:

if self.save_failure:
logging.error("SAML lookup failed, storing failure page to "
"'saml.html' to assist with debugging.")
with open("saml.html", 'wb') as out:
out.write(self.session_state.text.encode('utf-8'))

raise ExpectedGoogleException('Something went wrong - Could not find SAML response, check your credentials or use --save-failure-html to debug.')
raise ExpectedGoogleException('Something went wrong - '
'Could not find SAML response, check your credentials '
'or use --save-failure-html to debug.')

return base64.b64decode(saml_element)

Expand Down Expand Up @@ -466,10 +475,16 @@ def handle_sk(self, sess):
appId = self.find_app_id(str(keyHandleJsonPayload))

# txt sent for signing needs to be base64 url encode
# we also have to remove any base64 padding because including including it will prevent google accepting the auth response
# we also have to remove any base64 padding because including
# it will prevent google accepting the auth response
challenges_txt_encode_pad_removed = base64.urlsafe_b64encode(base64.b64decode(challenges_txt)).strip('='.encode())

u2f_challenges = [{'version': 'U2F_V2', 'challenge': challenges_txt_encode_pad_removed.decode(), 'appId': appId, 'keyHandle': keyHandle.decode()} for keyHandle in keyHandles]
u2f_challenges = [{
'version': 'U2F_V2',
'challenge': challenges_txt_encode_pad_removed.decode(),
'appId': appId,
'keyHandle': keyHandle.decode()
} for keyHandle in keyHandles]

# Prompt the user up to attempts_remaining times to insert their U2F device.
attempts_remaining = 5
Expand Down
2 changes: 2 additions & 0 deletions aws_google_auth/tests/test_google.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from mock import Mock
from aws_google_auth import google

# flake8: noqa


class TestGoogle(unittest.TestCase):
def read_local_file(self, filename):
Expand Down
1 change: 1 addition & 0 deletions aws_google_auth/tests/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import aws_google_auth

# flake8: noqa

class TestInit(unittest.TestCase):

Expand Down
4 changes: 2 additions & 2 deletions aws_google_auth/u2f.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ def u2f_auth(challenges, facet):
for device in devices[:]:
try:
device.open()
except:
except Exception:
# Some U2F devices fail on the first attempt to open but
# succeed on subsequent attempts. So retry once.
try:
device.open()
except:
except Exception:
devices.remove(device)

try:
Expand Down
2 changes: 1 addition & 1 deletion aws_google_auth/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def get_input(prompt):
@staticmethod
def pick_a_role(roles, aliases=None, account=None):
if account:
filtered_roles = {role: principal for role, principal in roles.items() if(account in role)}
filtered_roles = {role: principal for role, principal in roles.items() if (account in role)}
else:
filtered_roles = roles

Expand Down