Skip to content

Commit e438dba

Browse files
authored
Merge pull request #15 from britive/dynamic-aws-credentials-file
dynamic aws credentials file, force renew, sanitize configure tenant input
2 parents be01d8f + e3ea184 commit e438dba

File tree

9 files changed

+125
-56
lines changed

9 files changed

+125
-56
lines changed

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
https://github.com/britive/python-sdk/releases/download/v2.7.0/britive-2.7.0.tar.gz
1+
https://github.com/britive/python-sdk/releases/download/v2.7.1/britive-2.7.1.tar.gz
22
certifi==2022.6.15
33
charset-normalizer==2.1.0
44
click==8.1.3

setup.cfg

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[metadata]
22
name = pybritive
3-
version = 0.1.6
3+
version = 0.2.0
44
author = Britive Inc.
55
author_email = support@britive.com
66
description = A pure Python CLI for Britive
@@ -25,7 +25,7 @@ install_requires =
2525
tabulate
2626
toml
2727
cryptography
28-
britive @ https://github.com/britive/python-sdk/releases/download/v2.7.0/britive-2.7.0.tar.gz#egg=britive-2.7.0
28+
britive @ https://github.com/britive/python-sdk/releases/download/v2.7.1/britive-2.7.1.tar.gz#egg=britive-2.7.1
2929

3030
[options.packages.find]
3131
where = src

src/pybritive/britive_cli.py

Lines changed: 79 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import io
2+
import time
3+
24
from britive.britive import Britive
35
from .helpers.config import ConfigManager
46
from .helpers.credentials import FileCredentialManager, EncryptedFileCredentialManager
@@ -240,15 +242,17 @@ def _get_app_type(self, application_id):
240242
return profile['app_type']
241243
raise click.ClickException(f'Application {application_id} not found')
242244

243-
def __get_cloud_credential_printer(self, app_type, console, mode, profile, silent, credentials):
245+
def __get_cloud_credential_printer(self, app_type, console, mode, profile, silent, credentials,
246+
aws_credentials_file):
244247
if app_type in ['AWS', 'AWS Standalone']:
245248
return printer.AwsCloudCredentialPrinter(
246249
console=console,
247250
mode=mode,
248251
profile=profile,
249252
credentials=credentials,
250253
silent=silent,
251-
cli=self
254+
cli=self,
255+
aws_credentials_file=aws_credentials_file
252256
)
253257
if app_type in ['Azure']:
254258
return printer.AzureCloudCredentialPrinter(
@@ -285,12 +289,34 @@ def checkin(self, profile):
285289
application_name=app_name
286290
)
287291

288-
def checkout(self, alias, blocktime, console, justification, mode, maxpolltime, profile, passphrase):
289-
# first check if this is a profile alias
290-
profile_or_alias = alias or profile
292+
def _checkout(self, profile_name, env_name, app_name, programmatic, blocktime, maxpolltime, justification):
293+
try:
294+
self.login()
295+
return self.b.my_access.checkout_by_name(
296+
profile_name=profile_name,
297+
environment_name=env_name,
298+
application_name=app_name,
299+
programmatic=programmatic,
300+
include_credentials=True,
301+
wait_time=blocktime,
302+
max_wait_time=maxpolltime,
303+
justification=justification
304+
)
305+
except exceptions.ApprovalRequiredButNoJustificationProvided:
306+
raise click.ClickException('approval required and no justification provided.')
307+
except ValueError as e:
308+
raise click.BadParameter(str(e))
309+
310+
@staticmethod
311+
def _should_check_force_renew(app, force_renew, console):
312+
return app in ['AWS', 'AWS Standalone'] and force_renew and not console
291313

314+
def checkout(self, alias, blocktime, console, justification, mode, maxpolltime, profile, passphrase,
315+
force_renew, aws_credentials_file):
292316
credentials = None
293317
app_type = None
318+
credential_process_creds_found = False
319+
response = None
294320

295321
if mode == 'awscredentialprocess':
296322
self.silent = True # the aws credential process CANNOT output anything other than the expected JSON
@@ -299,59 +325,68 @@ def checkout(self, alias, blocktime, console, justification, mode, maxpolltime,
299325
# if not simply return those credentials
300326
# if they are expired
301327
app_type = 'AWS' # just hardcode as we know for sure this is for AWS
302-
credentials = Cache(passphrase=passphrase).get_awscredentialprocess(profile_name=profile_or_alias)
328+
credentials = Cache(passphrase=passphrase).get_awscredentialprocess(profile_name=alias or profile)
303329
if credentials:
304330
expiration_timestamp_str = credentials['expirationTime'].replace('Z', '')
305331
expires = datetime.fromisoformat(expiration_timestamp_str)
306332
now = datetime.utcnow()
307333
if now >= expires: # check to ensure the credentials are still valid, if not, set to None and get new
308334
credentials = None
335+
else:
336+
credential_process_creds_found = True
309337

310-
if not credentials: # nothing found via aws credential process or not aws credential process mode
311-
self.login()
312-
profile = self.config.profile_aliases.get(profile, profile)
313-
parts = profile.split('/')
314-
if len(parts) != 3:
315-
raise click.ClickException('Provided profile string does not have the required 3 parts.')
316-
app_name = parts[0]
317-
env_name = parts[1]
318-
profile_name = parts[2]
338+
profile_real = self.config.profile_aliases.get(profile, profile)
339+
parts = profile_real.split('/')
340+
if len(parts) != 3:
341+
raise click.ClickException('Provided profile string does not have the required 3 parts.')
319342

320-
try:
321-
response = self.b.my_access.checkout_by_name(
322-
profile_name=profile_name,
323-
environment_name=env_name,
324-
application_name=app_name,
325-
programmatic=False if console else True,
326-
include_credentials=True,
327-
wait_time=blocktime,
328-
max_wait_time=maxpolltime,
329-
justification=justification
330-
)
343+
# create this params once so we can use it multiple places
344+
params = {
345+
'profile_name': parts[2],
346+
'env_name': parts[1],
347+
'app_name': parts[0],
348+
'programmatic': False if console else True,
349+
'blocktime': blocktime,
350+
'maxpolltime': maxpolltime,
351+
'justification': justification
352+
}
353+
354+
if not credential_process_creds_found: # nothing found via aws cred process or not aws cred process mode
355+
response = self._checkout(**params)
356+
app_type = self._get_app_type(response['appContainerId'])
357+
credentials = response['credentials']
358+
359+
# this handles the --force-renew flag
360+
# lets check to see if the we should checkin this profile first and check it out again
361+
if self._should_check_force_renew(app_type, force_renew, console):
362+
expiration = datetime.fromisoformat(credentials['expirationTime'].replace('Z', ''))
363+
now = datetime.utcnow()
364+
diff = (expiration - now).total_seconds() / 60.0
365+
if diff < force_renew: # time to checkin the profile so we can refresh creds
366+
self.print('checking in the profile to get renewed credentials....standby')
367+
self.checkin(profile=profile_real)
368+
response = self._checkout(**params)
369+
credential_process_creds_found = False # need to write new creds to cache
331370
credentials = response['credentials']
332-
app_type = self._get_app_type(response['appContainerId'])
333-
except exceptions.ApprovalRequiredButNoJustificationProvided:
334-
raise click.ClickException('approval required and no justification provided.')
335-
except ValueError as e:
336-
raise click.BadParameter(str(e))
337-
338-
if alias: # do this down here so we know that the profile is valid and a checkout was successful
339-
self.config.save_profile_alias(alias=alias, profile=profile)
340-
if mode == 'awscredentialprocess':
341-
Cache(passphrase=passphrase).save_awscredentialprocess(
342-
profile_name=profile_or_alias,
343-
credentials=credentials
344-
)
345371

346-
cc_printer = self.__get_cloud_credential_printer(
372+
if alias: # do this down here so we know that the profile is valid and a checkout was successful
373+
self.config.save_profile_alias(alias=alias, profile=profile)
374+
375+
if mode == 'awscredentialprocess' and not credential_process_creds_found:
376+
Cache(passphrase=passphrase).save_awscredentialprocess(
377+
profile_name=alias or profile,
378+
credentials=credentials
379+
)
380+
381+
self.__get_cloud_credential_printer(
347382
app_type,
348383
console,
349384
mode,
350-
profile_or_alias,
385+
alias or profile,
351386
self.silent,
352-
credentials
353-
)
354-
cc_printer.print()
387+
credentials,
388+
aws_credentials_file
389+
).print()
355390

356391
def import_existing_npm_config(self):
357392
profile_aliases = self.config.import_global_npm_config()

src/pybritive/commands/checkout.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66

77
@click.command()
88
@build_britive
9-
@britive_options(names='alias,blocktime,console,justification,mode,maxpolltime,silent,tenant,token,passphrase')
9+
@britive_options(names='alias,blocktime,console,justification,mode,maxpolltime,silent,force_renew,aws_credentials_file,'
10+
'tenant,token,passphrase')
1011
@click.argument('profile', shell_complete=profile_completer)
11-
def checkout(ctx, alias, blocktime, console, justification, mode, maxpolltime, silent, tenant, token,
12-
passphrase, profile):
12+
def checkout(ctx, alias, blocktime, console, justification, mode, maxpolltime, silent, force_renew,
13+
aws_credentials_file, tenant, token, passphrase, profile):
1314
"""Checkout a profile.
1415
1516
This command takes 1 required argument `PROFILE`. This should be a string representation of the profile
@@ -25,5 +26,7 @@ def checkout(ctx, alias, blocktime, console, justification, mode, maxpolltime, s
2526
mode=mode,
2627
maxpolltime=maxpolltime,
2728
profile=profile,
28-
passphrase=passphrase
29+
passphrase=passphrase,
30+
force_renew=force_renew,
31+
aws_credentials_file=aws_credentials_file
2932
)

src/pybritive/helpers/cloud_credential_printer.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,9 @@ def _not_implemented(self):
8383

8484

8585
class AwsCloudCredentialPrinter(CloudCredentialPrinter):
86-
def __init__(self, console, mode, profile, silent, credentials, cli):
86+
def __init__(self, console, mode, profile, silent, credentials, cli, aws_credentials_file):
8787
super().__init__('AWS', console, mode, profile, silent, credentials, cli)
88+
self.aws_credentials_file = aws_credentials_file
8889

8990
def print_text(self):
9091
self.cli.print('AWS_ACCESS_KEY_ID', ignore_silent=True)
@@ -123,7 +124,7 @@ def print_env(self):
123124

124125
def print_integrate(self):
125126
# get path to aws credentials file
126-
env_path = os.getenv('AWS_SHARED_CREDENTIALS_FILE')
127+
env_path = self.aws_credentials_file
127128
if not env_path:
128129
path = Path.home() / '.aws' / 'credentials' # handle os specific separators properly
129130
else:

src/pybritive/helpers/config.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,10 @@ def save(self):
141141

142142
def save_tenant(self, tenant: str, alias: str = None, output_format: str = None):
143143
self.load()
144-
if not alias:
144+
tenant = tenant.replace('.britive-app.com', '')
145+
if alias:
146+
alias = alias.replace('.britive-app.com', '')
147+
else:
145148
alias = tenant
146149
if f'tenant-{alias}' not in self.config.keys():
147150
self.config[f'tenant-{alias}'] = {}
@@ -157,7 +160,7 @@ def save_global(self, default_tenant_name: str = None, output_format: str = None
157160
if 'global' not in self.config.keys():
158161
self.config['global'] = {}
159162
if default_tenant_name:
160-
self.config['global']['default_tenant'] = default_tenant_name
163+
self.config['global']['default_tenant'] = default_tenant_name.replace('.britive-app.com', '')
161164
if output_format:
162165
self.config['global']['output_format'] = output_format
163166
if backend:
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import click
2+
3+
4+
option = click.option(
5+
'--aws-credentials-file',
6+
default=None,
7+
help='AWS Programmatic Only - When mode is `integrate` specify a non-default location for the AWS '
8+
'credentials file.',
9+
envvar='AWS_SHARED_CREDENTIALS_FILE',
10+
show_envvar=True,
11+
show_default=True
12+
)

src/pybritive/options/britive_options.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
from ..options.file import option as file
1818
from ..options.configure_backend import option as configure_backend
1919
from ..options.passphrase import option as passphrase
20+
from ..options.force_renew import option as force_renew
21+
from ..options.aws_credentials_file import option as aws_credentials_file
2022

2123
options_map = {
2224
'tenant': tenant,
@@ -37,7 +39,9 @@
3739
'checked_out': checked_out,
3840
'file': file,
3941
'configure_backend': configure_backend,
40-
'passphrase': passphrase
42+
'passphrase': passphrase,
43+
'force_renew': force_renew,
44+
'aws_credentials_file': aws_credentials_file
4145
}
4246

4347

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import click
2+
3+
option = click.option(
4+
'--force-renew',
5+
default=None,
6+
show_default=True,
7+
type=int,
8+
help='AWS Programmatic Only - If the credentials are to expire within the specified number of minutes, check in '
9+
'the profile first and check it out again to get a new set of credentials.'
10+
)
11+

0 commit comments

Comments
 (0)