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

Updated tool to read/write from AWS profile configurations. #7

Merged
merged 8 commits into from
Jul 12, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 53 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,48 @@ can build with:
Usage
-----

.. code:: shell

$ aws-google-auth --help
usage: aws-google-auth [-h] [-v] [-u USERNAME] [-I IDP_ID] [-S SP_ID]
[-R REGION] [-d DURATION] [-p PROFILE]

Acquire temporary AWS credentials via Google SSO

optional arguments:
-h, --help show this help message and exit
-v, --version show program's version number and exit

-u USERNAME, --username USERNAME
Google Apps username ($GOOGLE_USERNAME)
-I IDP_ID, --idp-id IDP_ID
Google SSO IDP identifier ($GOOGLE_IDP_ID)
-S SP_ID, --sp-id SP_ID
Google SSO SP identifier ($GOOGLE_SP_ID)
-R REGION, --region REGION
AWS region endpoint ($AWS_DEFAULT_REGION)
-d DURATION, --duration DURATION
Credential duration ($DURATION)
-p PROFILE, --profile PROFILE
AWS profile ($AWS_PROFILE)


Native Python
~~~~~~~~~~~~~

1. Execute ``aws-google-auth``
2. You will be prompted to supply each parameter

*Note* You can skip prompts by either passing parameters to the command, or setting the specified Environment variables.

Via Docker
~~~~~~~~~~~~~

1. Set environment variables for ``GOOGLE_USERNAME``, ``GOOGLE_IDP_ID``,
and ``GOOGLE_SP_ID`` (see above under "Important Data" for how to
find the last two; the first one is usually your email address)
2. For Docker:
``docker run -it -e GOOGLE_USERNAME -e GOOGLE_IDP_ID -e GOOGLE_SP_ID aws-google-auth``
3. For Python: ``aws-google-auth``

You'll be prompted for your password. If you've set up an MFA token for
your Google account, you'll also be prompted for the current token
Expand All @@ -76,9 +112,19 @@ If you have more than one role available to you, you'll be prompted to
choose the role from a list; otherwise, if your credentials are correct,
you'll just see the AWS keys printed on stdout.

You should ``eval`` the ``export`` statements that come out, because
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking for advice on how to update the README to reflect the new features.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about rewriting the Usage section of the README. Something along the lines of:

  • Usage
    • Configuration via environment variables (setting GOOGLE_USERNAME etc)
    • Writing to a profile configuration (which arguments to use)
    • Using credentials in environment variables only (leaving off profile argument)
    • Running from a Docker image
    • Running from a locally-installed Python package

and then, I can merge this into the bash_wrapper branch and update the section on "environment variables only"

that'll set environment variables for you. This tools currently doesn't
write credentials to an ``~/.aws/credentials`` file

Storage of profile credentials
------------------------------

Through the use of AWS profiles, using the ``-p`` or ``--profile`` flag, the ``aws-google-auth`` utility will store the supplied username, IDP and SP details in your ``./aws/config`` files.

When re-authenticating using the same profile, the values will be remembered to speed up the re-authentication process.
This enables an approach that enables you to enter your username, IPD and SP values once and then after only need to re-enter your password (and MFA if enabled).

Creating an alias as below can be a quick and easy way to re-authenticate with a simple command shortcut.

``alias aws-development='unset AWS_PROFILE; aws-google-auth -p aws-dev; export AWS_PROFILE=aws-dev'``


Notes on Authentication
-----------------------
Expand Down Expand Up @@ -120,3 +166,6 @@ Acknowledgements
This work is inspired by `keyme <https://github.com/wheniwork/keyme>`__
-- their digging into the guts of how Google SAML auth works is what's
enabled it.

The attribute management and credential injection into AWS configuration files
was heavily borrowed from `aws-adfs <https://github.com/venth/aws-adfs>`
116 changes: 90 additions & 26 deletions aws_google_auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,18 @@
import json
from bs4 import BeautifulSoup
from lxml import etree
import configparser

VERSION = "0.0.6"
import prepare

VERSION = "0.0.7"

REGION = os.getenv("AWS_DEFAULT_REGION") or "ap-southeast-2"
IDP_ID = os.getenv("GOOGLE_IDP_ID")
SP_ID = os.getenv("GOOGLE_SP_ID")
USERNAME = os.getenv("GOOGLE_USERNAME")
DURATION = os.getenv("DURATION")
PROFILE = os.getenv("AWS_PROFILE")

class GoogleAuth:
def __init__(self, **kwargs):
Expand Down Expand Up @@ -277,32 +281,41 @@ def cli():
parser.add_argument('-S', '--sp-id', default=SP_ID, help='Google SSO SP identifier ($GOOGLE_SP_ID)')
parser.add_argument('-R', '--region', default=REGION, help='AWS region endpoint ($AWS_DEFAULT_REGION)')
parser.add_argument('-d', '--duration', default=DURATION, help='Credential duration ($DURATION)')
parser.add_argument('-p', '--profile', default=PROFILE, help='AWS profile ($AWS_PROFILE)')

args = parser.parse_args()

if args.username is None:
args.username = raw_input("Google username: ")

if args.idp_id is None or args.sp_id is None:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added raw_input lookups for these values (below) if not resolved from the stored config file.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

print "Must set both GOOGLE_IDP_ID and GOOGLE_SP_ID"
parser.print_help()
sys.exit(1)

if args.duration is None:
print "Setting duration to 3600 seconds"
args.duration = 3600

if args.duration > 3600:
print "Duration must be less than or equal to 3600"
duration = 3600

config = prepare.get_prepared_config(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks up the config parameters from the ~/.aws/config file, overwridden from cli parameters or ENV variables.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noice

args.profile,
args.region,
args.username,
args.idp_id,
args.sp_id,
args.duration
)

if config.google_username is None:
config.google_username = raw_input("Google username: ")
else:
print "Google username: " + config.google_username

if config.google_idp_id is None:
config.google_idp_id = raw_input("Google idp: ")

if config.google_sp_id is None:
config.google_sp_id = raw_input("Google sp: ")

passwd = getpass.getpass()

google = GoogleAuth(
username=args.username,
username=config.google_username,
password=passwd,
idp_id=args.idp_id,
sp_id=args.sp_id
idp_id=config.google_idp_id,
sp_id=config.google_sp_id
)

google.do_login()
Expand All @@ -312,18 +325,69 @@ def cli():
doc = etree.fromstring(base64.b64decode(encoded_saml))
roles = dict([x.split(',') for x in doc.xpath('//*[@Name = "https://aws.amazon.com/SAML/Attributes/Role"]//text()')])

role, provider = pick_one(roles)
if not config.role_arn in roles:
config.role_arn, config.provider = pick_one(roles)

print "Assuming " + role
print "Assuming " + config.role_arn

sts = boto3.client('sts', region_name=REGION)
sts = boto3.client('sts', region_name=config.region)
token = sts.assume_role_with_saml(
RoleArn=role,
PrincipalArn=provider,
RoleArn=config.role_arn,
PrincipalArn=config.provider,
SAMLAssertion=encoded_saml,
DurationSeconds=args.duration)
DurationSeconds=config.duration)

print "export AWS_ACCESS_KEY_ID='{}'".format(token['Credentials']['AccessKeyId'])
print "export AWS_SECRET_ACCESS_KEY='{}'".format(token['Credentials']['SecretAccessKey'])
print "export AWS_SESSION_TOKEN='{}'".format(token['Credentials']['SessionToken'])
print "export AWS_SESSION_EXPIRATION='{}'".format(token['Credentials']['Expiration'])
if conifig.profile is None:
print "export AWS_ACCESS_KEY_ID='{}'".format(token['Credentials']['AccessKeyId'])
print "export AWS_SECRET_ACCESS_KEY='{}'".format(token['Credentials']['SecretAccessKey'])
print "export AWS_SESSION_TOKEN='{}'".format(token['Credentials']['SessionToken'])
print "export AWS_SESSION_EXPIRATION='{}'".format(token['Credentials']['Expiration'])

_store(config, token)


def _store(config, aws_session_token):

def store_config(profile, config_location, storer):
config_file = configparser.RawConfigParser()
config_file.read(config_location)

if not config_file.has_section(profile):
config_file.add_section(profile)

storer(config_file, profile)

with open(config_location, 'w+') as f:
try:
config_file.write(f)
finally:
f.close()

def credentials_storer(config_file, profile):
config_file.set(profile, 'aws_access_key_id', aws_session_token['Credentials']['AccessKeyId'])
config_file.set(profile, 'aws_secret_access_key', aws_session_token['Credentials']['SecretAccessKey'])
config_file.set(profile, 'aws_session_token', aws_session_token['Credentials']['SessionToken'])
config_file.set(profile, 'aws_security_token', aws_session_token['Credentials']['SessionToken'])

def config_storer(config_file, profile):
config_file.set(profile, 'region', config.region)
config_file.set(profile, 'output', config.output_format)
config_file.set(profile, 'google_config.role_arn', config.role_arn)
config_file.set(profile, 'google_config.provider', config.provider)
config_file.set(profile, 'google_config.google_idp_id', config.google_idp_id)
config_file.set(profile, 'google_config.google_sp_id', config.google_sp_id)
config_file.set(profile, 'google_config.google_username', config.google_username)
config_file.set(profile, 'google_config.duration', config.duration)

store_config(config.profile, config.aws_credentials_location, credentials_storer)
if config.profile == 'default':
store_config(config.profile, config.aws_config_location, config_storer)
else:
store_config('profile {}'.format(config.profile), config.aws_config_location, config_storer)


if __name__ == '__main__':
try:
cli()
except KeyboardInterrupt:
pass
125 changes: 125 additions & 0 deletions aws_google_auth/prepare.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import ast
import configparser
import os
import botocore.session
from types import MethodType


def get_prepared_config(
profile,
region,
google_username,
google_idp_id,
google_sp_id,
duration
):

def default_if_none(value, default):
return value if value is not None else default

google_config.profile = default_if_none(profile, google_config.profile)

_create_base_aws_cli_config_files_if_needed(google_config)
_load_google_config_from_stored_profile(google_config, google_config.profile)

google_config.region = default_if_none(region, google_config.region)
google_config.google_username = default_if_none(google_username, google_config.google_username)
google_config.google_idp_id = default_if_none(google_idp_id, google_config.google_idp_id)
google_config.google_sp_id = default_if_none(google_sp_id, google_config.google_sp_id)
google_config.duration = default_if_none(duration, google_config.duration)

return google_config


def _create_google_default_config():
config = type('', (), {})()

# Use botocore session API to get defaults
session = botocore.session.Session()

# region: The default AWS region that this script will connect
# to for all API calls
config.region = session.get_config_variable('region') or 'eu-central-1'

# aws cli profile to store config and access keys into
config.profile = session.profile or 'default'

# output format: The AWS CLI output format that will be configured in the
# adf profile (affects subsequent CLI calls)
config.output_format = session.get_config_variable('format') or 'json'

# aws credential location: The file where this script will store the temp
# credentials under the configured profile
config.aws_credentials_location = os.path.expanduser(session.get_config_variable('credentials_file'))
config.aws_config_location = os.path.expanduser(session.get_config_variable('config_file'))

config.role_arn = None
config.provider = None

config.google_sp_id = None
config.google_idp_id = None
config.google_username = None
config.duration = 3600

return config


def _load_google_config_from_stored_profile(google_config, profile):

def get_or(self, profile, option, default_value):
if self.has_option(profile, option):
return self.get(profile, option)
return default_value

def load_from_config(config_location, profile, loader):
config = configparser.RawConfigParser()
config.read(config_location)
if config.has_section(profile):
setattr(config, get_or.__name__, MethodType(get_or, config))
loader(config, profile)

del config

def load_config(config, profile):
google_config.region = config.get_or(profile, 'region', google_config.region)
google_config.output_format = config.get_or(profile, 'output', google_config.output_format)

google_config.role_arn = config.get_or(profile, 'google_config.role_arn', google_config.role_arn)
google_config.provider = config.get_or(profile, 'google_config.provider', google_config.provider)
google_config.google_idp_id = config.get_or(profile, 'google_config.google_idp_id', google_config.google_idp_id)
google_config.google_sp_id = config.get_or(profile, 'google_config.google_sp_id', google_config.google_sp_id)
google_config.google_username = config.get_or(profile, 'google_config.google_username', google_config.google_username)

if profile == 'default':
load_from_config(google_config.aws_config_location, profile, load_config)
else:
load_from_config(google_config.aws_config_location, 'profile ' + profile, load_config)


def _create_base_aws_cli_config_files_if_needed(google_config):
def touch(fname, mode=0o600):
flags = os.O_CREAT | os.O_APPEND
with os.fdopen(os.open(fname, flags, mode)) as f:
try:
os.utime(fname, None)
finally:
f.close()

aws_config_root = os.path.dirname(google_config.aws_config_location)

if not os.path.exists(aws_config_root):
os.mkdir(aws_config_root, 0o700)

if not os.path.exists(google_config.aws_credentials_location):
touch(google_config.aws_credentials_location)

aws_credentials_root = os.path.dirname(google_config.aws_credentials_location)

if not os.path.exists(aws_credentials_root):
os.mkdir(aws_credentials_root, 0o700)

if not os.path.exists(google_config.aws_config_location):
touch(google_config.aws_config_location)


google_config = _create_google_default_config()
5 changes: 2 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
long_description = f.read()

VERSION = '0.0.6'
VERSION = '0.0.7'

setup(
name='aws-google-auth',
Expand Down Expand Up @@ -81,7 +81,7 @@
# requirements files see:
# https://packaging.python.org/en/latest/requirements.html
# install_requires=['peppercorn'],
install_requires=['boto3', 'lxml', 'requests', 'beautifulsoup4'],
install_requires=['boto3', 'lxml', 'requests', 'beautifulsoup4', 'configparser'],

# List additional groups of dependencies here (e.g. development
# dependencies). You can install these using the following syntax,
Expand Down Expand Up @@ -114,4 +114,3 @@
],
},
)