Skip to content

Commit 5b8697e

Browse files
authored
Merge pull request #35 from nathan-v/Feature_region_arg
Add region parameter, screen only output option, command wrapper, bug fixes, v0.7.5
2 parents 7eb8b95 + df4174f commit 5b8697e

File tree

11 files changed

+241
-36
lines changed

11 files changed

+241
-36
lines changed

aws_okta_keyman/aws.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,9 @@ def __init__(self,
127127
dir=cred_dir))
128128
os.makedirs(cred_dir)
129129

130-
self.sts = boto3.client('sts')
131-
132130
self.profile = profile
133131
self.region = region
134-
132+
self.sts = boto3.client('sts', region_name=self.region)
135133
self.assertion = SamlAssertion(assertion)
136134
self.writer = Credentials(cred_file)
137135

@@ -197,7 +195,7 @@ def available_roles(self):
197195
key=lambda k: (k['account'], k['role_name']))
198196
return self.roles
199197

200-
def assume_role(self):
198+
def assume_role(self, print_only=False):
201199
"""Use the SAML Assertion to actually get the credentials.
202200
203201
Uses the supplied (one time use!) SAML Assertion to go out to Amazon
@@ -216,7 +214,11 @@ def assume_role(self):
216214
PrincipalArn=self.roles[self.role]['principle'],
217215
SAMLAssertion=self.assertion.encode())
218216
self.creds = session['Credentials']
219-
self._write()
217+
218+
if print_only:
219+
self._print_creds()
220+
else:
221+
self._write()
220222

221223
def _write(self):
222224
"""Write out our secrets to the Credentials object."""
@@ -229,6 +231,30 @@ def _write(self):
229231
LOG.info('Session expires at {time} ⏳'.format(
230232
time=self.creds['Expiration']))
231233

234+
def _print_creds(self):
235+
""" Print out the retrieved credentials to the screen
236+
"""
237+
cred_str = "AWS_ACCESS_KEY_ID = {}\n".format(self.creds['AccessKeyId'])
238+
cred_str = "{}AWS_SECRET_ACCESS_KEY = {}\n".format(
239+
cred_str, self.creds['SecretAccessKey'])
240+
cred_str = "{}AWS_SESSION_TOKEN = {}".format(
241+
cred_str, self.creds['SessionToken'])
242+
LOG.info("AWS Credentials: \n\n\n{}\n\n".format(cred_str))
243+
244+
def export_creds_to_var_string(self):
245+
""" Export the current credentials as environment vaiables
246+
"""
247+
var_string = (
248+
"export AWS_ACCESS_KEY_ID={}; "
249+
"export AWS_SECRET_ACCESS_KEY={}; "
250+
"export AWS_SESSION_TOKEN={};"
251+
).format(
252+
self.creds['AccessKeyId'],
253+
self.creds['SecretAccessKey'],
254+
self.creds['SessionToken']
255+
)
256+
return var_string
257+
232258
def account_ids_to_names(self, roles):
233259
"""Turn account IDs into user-friendly names
234260

aws_okta_keyman/config.py

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ def __init__(self, argv):
4848
self.oktapreview = None
4949
self.password_cache = None
5050
self.password_reset = None
51+
self.command = None
52+
self.screen = None
53+
self.region = None
5154

5255
if len(argv) > 1:
5356
if argv[1] == 'config':
@@ -65,6 +68,9 @@ def validate(self):
6568
"or as an argument")
6669
raise ValueError(err)
6770

71+
if self.region is None:
72+
self.region = 'us-east-1'
73+
6874
if self.username is None:
6975
user = getpass.getuser()
7076
LOG.info(
@@ -166,13 +172,14 @@ def main_args(arg_group, required=False):
166172
@staticmethod
167173
def optional_args(optional_args):
168174
"""Define the always-optional arguments."""
169-
optional_args.add_argument('-u', '--username', type=str, help=(
170-
'Okta Login Name - either '
171-
'bob@foobar.com, or just bob works too,'
172-
' depending on your organization '
173-
'settings. Will use the current user if not'
174-
'specified.'
175-
)),
175+
optional_args.add_argument('-u', '--username', type=str,
176+
help=(
177+
'Okta Login Name - either '
178+
'bob@foobar.com, or just bob works too,'
179+
' depending on your organization '
180+
'settings. Will use the current user if '
181+
'not specified.'
182+
))
176183
optional_args.add_argument('-a', '--appid', type=str, help=(
177184
'The "redirect link" Application ID - '
178185
'this can be found by mousing over the '
@@ -228,6 +235,24 @@ def optional_args(optional_args):
228235
' if it has changed or is incorrect.'
229236
),
230237
default=False)
238+
optional_args.add_argument('-C', '--command', type=str,
239+
help=(
240+
'Command to run with the requested '
241+
'AWS keys provided as environment '
242+
'variables.'
243+
))
244+
optional_args.add_argument('-s', '--screen', action='store_true',
245+
help=(
246+
'Print the retrieved key '
247+
'only and do not write to the AWS '
248+
'credentials file.'
249+
),
250+
default=False)
251+
optional_args.add_argument('-re', '--region', type=str,
252+
help=(
253+
'AWS region to use for calls. '
254+
'Required for GovCloud.'
255+
))
231256

232257
@staticmethod
233258
def read_yaml(filename, raise_on_error=False):
@@ -274,14 +299,20 @@ def write_config(self):
274299

275300
LOG.debug("YAML being saved: {}".format(config_out))
276301

302+
file_folder = os.path.dirname(os.path.abspath(file_path))
303+
if not os.path.exists(file_folder):
304+
LOG.debug("Creating missin config file folder : {}".format(
305+
file_folder))
306+
os.makedirs(file_folder)
307+
277308
with open(file_path, 'w') as outfile:
278309
yaml.safe_dump(config_out, outfile, default_flow_style=False)
279310

280311
@staticmethod
281312
def clean_config_for_write(config):
282313
"""Remove args we don't want to save to a config file."""
283314
ignore = ['name', 'appid', 'argv', 'writepath', 'config', 'debug',
284-
'oktapreview', 'password_reset']
315+
'oktapreview', 'password_reset', 'command']
285316
for var in ignore:
286317
del config[var]
287318

@@ -293,7 +324,7 @@ def clean_config_for_write(config):
293324
@staticmethod
294325
def user_input(text):
295326
"""Wrap input() making testing support of py2 and py3 easier."""
296-
return input(text)
327+
return input(text).strip()
297328

298329
def interactive_config(self):
299330
""" Runs an interactive configuration to make it simpler to create

aws_okta_keyman/duo.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,3 +273,4 @@ def do_redirect(self, url, sid):
273273

274274
if 'cookie' in result['response']:
275275
return result['response']['cookie']
276+
return None

aws_okta_keyman/keyman.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import getpass
2222
import logging
23+
import os
2324
import sys
2425
import time
2526
import traceback
@@ -108,7 +109,7 @@ def setup_logging():
108109
@staticmethod
109110
def user_input(text):
110111
"""Wrap input() making testing support of py2 and py3 easier."""
111-
return input(text)
112+
return input(text).strip()
112113

113114
def user_password(self):
114115
"""Wrap getpass to simplify testing."""
@@ -351,8 +352,10 @@ def start_session(self):
351352
appid=self.config.appid)
352353

353354
try:
355+
self.log.info("Starting AWS session for {}".format(
356+
self.config.region))
354357
session = aws.Session(assertion, profile=self.config.name,
355-
role=self.role)
358+
role=self.role, region=self.config.region)
356359

357360
except xml.etree.ElementTree.ParseError:
358361
self.log.error('Could not find any Role in the SAML assertion')
@@ -377,11 +380,11 @@ def aws_auth_loop(self):
377380

378381
try:
379382
session = self.start_session()
380-
session.assume_role()
383+
session.assume_role(self.config.screen)
381384

382385
except aws.MultipleRoles:
383386
self.handle_multiple_roles(session)
384-
session.assume_role()
387+
session.assume_role(self.config.screen)
385388
except requests.exceptions.ConnectionError:
386389
self.log.warning('Connection error... will retry')
387390
time.sleep(5)
@@ -404,10 +407,23 @@ def aws_auth_loop(self):
404407
self.auth_okta(state_token=err.state_token)
405408
continue
406409

407-
# If we're not running in re-up mode, once we have the assertion
408-
# and creds, go ahead and quit.
409410
if not self.config.reup:
410-
self.log.info('All done! 👍')
411-
return None
411+
return self.wrap_up(session)
412412

413413
self.log.info('Reup enabled, sleeping... 💤')
414+
415+
def wrap_up(self, session):
416+
""" Execute any final steps when we're not in reup mode
417+
418+
Args:
419+
session: aws.session object
420+
"""
421+
if self.config.command:
422+
command_string = "{} {}".format(
423+
session.export_creds_to_var_string(),
424+
self.config.command
425+
)
426+
self.log.info("Running requested command...\n\n")
427+
os.system(command_string)
428+
else:
429+
self.log.info('All done! 👍')

aws_okta_keyman/metadata.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,5 @@
1414
# Copyright 2018 Nathan V
1515
"""Package metadata."""
1616

17-
__version__ = '0.7.4'
17+
__version__ = '0.7.5'
1818
__desc__ = 'AWS Okta Keyman'

aws_okta_keyman/okta.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -181,12 +181,13 @@ def validate_mfa(self, fid, state_token, passcode):
181181
"""
182182
if len(passcode) > 6 or len(passcode) < 5:
183183
LOG.error('Passcodes must be 5 or 6 digits')
184-
return
184+
return None
185185

186186
valid = self.send_user_response(fid, state_token, passcode, 'passCode')
187187
if valid:
188188
self.set_token(valid)
189189
return True
190+
return None
190191

191192
def validate_answer(self, fid, state_token, answer):
192193
"""Validate an Okta user with Question-based MFA.
@@ -206,12 +207,13 @@ def validate_answer(self, fid, state_token, answer):
206207
"""
207208
if not answer:
208209
LOG.error('Answer cannot be blank')
209-
return
210+
return None
210211

211212
valid = self.send_user_response(fid, state_token, answer, 'answer')
212213
if valid:
213214
self.set_token(valid)
214215
return True
216+
return None
215217

216218
def send_user_response(self, fid, state_token, user_response, resp_type):
217219
"""Call Okta with a factor response and verify it.
@@ -450,6 +452,7 @@ def handle_mfa_response(self, ret):
450452
return True
451453

452454
self.handle_response_factors(response_factors, ret['stateToken'])
455+
return None
453456

454457
def handle_push_factors(self, factors, state_token):
455458
"""Handle any push-type factors.
@@ -547,9 +550,9 @@ def get_aws_apps(self):
547550
if i['appName'] == 'amazon_aws'}
548551

549552
accounts = []
550-
for k, v in aws_list.items():
551-
appid = v.split("/", 5)[5]
552-
accounts.append({'name': k, 'appid': appid})
553+
for key, val in aws_list.items():
554+
appid = val.split("/", 5)[5]
555+
accounts.append({'name': key, 'appid': appid})
553556
return accounts
554557

555558
def mfa_callback(self, auth, verification, state_token):

aws_okta_keyman/test/aws_test.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ def setUp(self):
123123
self.mock_saml = self.patcher.start()
124124
self.fake_assertion = mock.MagicMock(name='FakeAssertion')
125125
self.mock_saml.return_value = self.fake_assertion
126+
self.botopatch = mock.patch('aws_okta_keyman.aws.boto3')
127+
self.mock_boto = self.botopatch.start()
126128

127129
@mock.patch('os.path.expanduser')
128130
@mock.patch('os.makedirs')
@@ -286,6 +288,57 @@ def test_assume_role_preset(self, mock_write):
286288
mock.call()
287289
])
288290

291+
@mock.patch('aws_okta_keyman.aws.Session._print_creds')
292+
@mock.patch('aws_okta_keyman.aws.Session._write')
293+
def test_assume_role_print(self, mock_write, mock_print):
294+
assertion = mock.Mock()
295+
assertion.roles.return_value = [{'arn': '', 'principle': ''}]
296+
session = aws.Session('BogusAssertion')
297+
session.role = 0
298+
session.roles = [{'arn': '', 'principle': ''}]
299+
session.assertion = assertion
300+
sts = {'Credentials':
301+
{'AccessKeyId': 'AKI',
302+
'SecretAccessKey': 'squirrel',
303+
'SessionToken': 'token',
304+
'Expiration': 'never'
305+
}}
306+
session.sts = mock.Mock()
307+
session.sts.assume_role_with_saml.return_value = sts
308+
309+
session.assume_role(print_only=True)
310+
311+
assert not mock_write.called
312+
assert mock_print.called
313+
314+
@mock.patch('aws_okta_keyman.aws.LOG')
315+
def test_print_creds(self, log_mock):
316+
session = aws.Session('BogusAssertion')
317+
expected = (
318+
'AWS Credentials: \n\n\n'
319+
'AWS_ACCESS_KEY_ID = None\n'
320+
'AWS_SECRET_ACCESS_KEY = None\n'
321+
'AWS_SESSION_TOKEN = None\n\n'
322+
)
323+
324+
session._print_creds()
325+
326+
log_mock.assert_has_calls([
327+
mock.call.info(expected)
328+
])
329+
330+
def test_export_creds_to_var_string(self):
331+
session = aws.Session('BogusAssertion')
332+
expected = (
333+
'export AWS_ACCESS_KEY_ID=None; '
334+
'export AWS_SECRET_ACCESS_KEY=None; '
335+
'export AWS_SESSION_TOKEN=None;'
336+
)
337+
338+
ret = session.export_creds_to_var_string()
339+
340+
self.assertEqual(ret, expected)
341+
289342
def test_available_roles(self):
290343
roles = [{'role': '::::1:role/role', 'principle': ''},
291344
{'role': '::::1:role/role', 'principle': ''}]

0 commit comments

Comments
 (0)