Skip to content

Commit 3f8293d

Browse files
authored
Merge pull request #134 from britive/1.7.0rc1
v1.7.0rc1 develop
2 parents aded3bc + 1a32e12 commit 3f8293d

File tree

11 files changed

+191
-19
lines changed

11 files changed

+191
-19
lines changed

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@
22

33
* As of v1.4.0 release candidates will be published in an effort to get new features out faster while still allowing time for full QA testing before moving the release candidate to a full release.
44

5+
## v1.7.0rc1 [2024-01-19]
6+
#### What's New
7+
* Display system announcement/banner if one is present for the tenant
8+
9+
#### Enhancements
10+
* New checkout mode of `gcloudauthexec` which will invoke, via sub-shell, the `gcloud auth activate-service-account` command to switch credentials for `gcloud`. Additionally, a `checkin` will reset this configuration.
11+
12+
#### Bug Fixes
13+
* Fix issue related to the `cache` and `clear` commands when no global default tenant is set
14+
15+
#### Dependencies
16+
* `britive>=2.24.0rc1`
17+
18+
#### Other
19+
* None
20+
521
## v1.6.1 [2023-12-18]
622
#### What's New
723
* None

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
boto3
2-
britive>=2.23.0
2+
britive>=2.24.0rc1
33
certifi>=2022.12.7
44
charset-normalizer==2.1.0
55
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 = 1.6.1
3+
version = 1.7.0rc1
44
author = Britive Inc.
55
author_email = support@britive.com
66
description = A pure Python CLI for Britive
@@ -27,7 +27,7 @@ install_requires =
2727
toml
2828
cryptography>=41.0.0
2929
python-dateutil
30-
britive>=2.23.0
30+
britive>=2.24.0rc1
3131
jmespath
3232
pyjwt
3333

src/pybritive/britive_cli.py

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import csv
2+
import hashlib
23
from datetime import datetime
34
from datetime import timezone
45
from datetime import timedelta
@@ -29,7 +30,7 @@
2930

3031

3132
class BritiveCli:
32-
def __init__(self, tenant_name: str = None, token: str = None, silent: bool = False,passphrase: str = None,
33+
def __init__(self, tenant_name: str = None, token: str = None, silent: bool = False, passphrase: str = None,
3334
federation_provider: str = None, from_helper_console_script: bool = False):
3435
self.silent = silent
3536
self.from_helper_console_script = from_helper_console_script
@@ -168,6 +169,22 @@ def login(self, explicit: bool = False, browser: str = None):
168169
if explicit and should_get_profiles:
169170
self._set_available_profiles() # will handle calling cache_profiles() and construct_kube_config()
170171

172+
# handle printing the banner
173+
self._display_banner()
174+
175+
def _display_banner(self):
176+
if self.silent:
177+
return
178+
179+
if not Cache().banner_expired(tenant=self.tenant_name): # if banner is not expired yet then nothing to do
180+
return
181+
182+
# if we get here then we need to at least grab the banner and see if it has changed
183+
banner = self.b.banner()
184+
banner_changed = Cache().save_banner(tenant=self.tenant_name, banner=banner)
185+
if banner and banner_changed:
186+
self.print(f'*** {banner.get("messageType", "UNKNOWN")}: {banner.get("message", "<no message>")} ***')
187+
171188
def _update_sdk_user_agent(self):
172189
# update the user agent to include the pybritive cli version
173190
user_agent = self.b.session.headers.get('User-Agent')
@@ -593,8 +610,10 @@ def checkin(self, profile, console):
593610
transaction_id=transaction_id
594611
)
595612

596-
if application_type in ('aws', 'aws standalone'):
613+
if application_type in ['aws', 'aws standalone']:
597614
self.clear_cached_aws_credentials(profile)
615+
if application_type in ['gcp']:
616+
self.clear_gcloud_auth_key_files(profile=profile)
598617

599618
def _checkout(self, profile_name, env_name, app_name, programmatic, blocktime, maxpolltime, justification):
600619
try:
@@ -649,7 +668,7 @@ def _split_profile_into_parts(self, profile):
649668
def _extend_checkout(self, profile, console):
650669
self.login()
651670
parts = self._split_profile_into_parts(profile)
652-
response = self.b.my_access.extend_checkout_by_name(
671+
self.b.my_access.extend_checkout_by_name(
653672
profile_name=parts['profile'],
654673
environment_name=parts['env'],
655674
application_name=parts['app'],
@@ -927,8 +946,49 @@ def request_withdraw(self, profile):
927946
environment_id=ids['environment_id']
928947
)
929948

930-
def clear_gcloud_auth_key_files(self):
931-
self.config.clear_gcloud_auth_key_files()
949+
@staticmethod
950+
def build_gcloud_key_file_for_gcloudauthexec(profile: str):
951+
profile_hash = hashlib.sha256(string=profile.encode('utf-8')).hexdigest()
952+
return f'gcloudauthexec-{profile_hash}.json'
953+
954+
def clear_gcloud_auth_key_files(self, profile: str = None):
955+
if profile: # we want to attempt a gcloud cli command
956+
import subprocess # lazy load as this will not always be needed
957+
958+
# build the path to the key file in question
959+
key_file = self.build_gcloud_key_file_for_gcloudauthexec(profile=profile)
960+
path = Path(self.config.gcloud_key_file_path) / key_file
961+
962+
if path.exists(): # we have a valid gcloudauthexec key file, so we know there was a checkout with this mode
963+
try:
964+
with open(str(path), 'r') as f:
965+
credentials = json.loads(f.read())
966+
commands = [
967+
'gcloud',
968+
'auth',
969+
'revoke',
970+
credentials['client_email'],
971+
'--verbosity=error'
972+
]
973+
self.debug(' '.join(commands))
974+
subprocess.run(commands, check=True)
975+
976+
gcloud_default_account = self.config.gcloud_default_account()
977+
978+
if gcloud_default_account:
979+
commands = [
980+
'gcloud',
981+
'config',
982+
'set',
983+
'account',
984+
gcloud_default_account, # no need for "" here as subprocess will properly escape
985+
'--verbosity=error'
986+
]
987+
self.debug(' '.join(commands))
988+
subprocess.run(commands, check=True)
989+
except Exception as e:
990+
self.print(f'could not reset gcloud CLI active account due to issue: {str(e)}')
991+
self.config.clear_gcloud_auth_key_files(profile=profile)
932992

933993
def api(self, method, parameters: dict, query=None):
934994
self.login()

src/pybritive/choices/mode.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
'browser-chrome',
2626
'browser-chromium',
2727
'kube-exec', # bake into kubeconfig with oidc exec output and additional caching to make kubectl more performant
28+
'gcloudauthexec', # will effectively execute results of gcloudauth in a sub-shell
2829
],
2930
case_sensitive=False
3031
)

src/pybritive/commands/clear.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from ..helpers.build_britive import build_britive
33
from ..helpers.profile_argument_decorator import click_smart_profile_argument
44

5+
56
@click.group()
67
def clear():
78
"""Clear various local settings and configurations."""

src/pybritive/helpers/build_britive.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,26 @@
44
import click
55
from merge_args import merge_args
66
from ..britive_cli import BritiveCli
7+
from click import Context
78

89

910
@dataclass
1011
class Common:
1112
britive: BritiveCli
1213

1314

15+
def should_set_output_format(ctx: Context) -> bool:
16+
parent_command = ctx.parent.command.name
17+
command = ctx.command.name
18+
if parent_command in ['configure', 'clear']:
19+
return False
20+
21+
if parent_command == 'cache' and command in ['clear']:
22+
return False
23+
24+
return True
25+
26+
1427
# this wrapper exists to centralize all "common" CLI options (options that exist for all commands)
1528
def build_britive(f):
1629
@merge_args(f)
@@ -27,8 +40,7 @@ def wrapper(
2740
passphrase=kwargs.get('passphrase'),
2841
federation_provider=kwargs.get('federation_provider')
2942
))
30-
parent_command = ctx.parent.command.name
31-
if parent_command != 'configure':
43+
if should_set_output_format(ctx=ctx):
3244
ctx.obj.britive.set_output_format(kwargs.get('output_format'))
3345
return f(ctx=ctx, **kwargs)
3446
return wrapper

src/pybritive/helpers/cache.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import hashlib
12
import json
23
import os
34
from pathlib import Path
45
from .encryption import StringEncryption, InvalidPassphraseException
6+
import time
57

68

79
class Cache:
@@ -15,7 +17,8 @@ def __init__(self, passphrase: str = None):
1517
self.default_key_values = {
1618
'profiles': [],
1719
'awscredentialprocess': {},
18-
'kube-exec': {}
20+
'kube-exec': {},
21+
'banners': {}
1922
}
2023
self.load()
2124

@@ -76,3 +79,31 @@ def save_credentials(self, profile_name: str, credentials: dict, mode: str = 'aw
7679
def clear_credentials(self, profile_name: str, mode: str = 'awscredentialprocess'):
7780
self.cache[mode].pop(profile_name.lower(), None)
7881
self.write()
82+
83+
@staticmethod
84+
def hash_banner(banner: dict) -> str:
85+
return hashlib.sha512(string=json.dumps(banner, default=str)).hexdigest()
86+
87+
def banner_expired(self, tenant: str) -> bool:
88+
cached_banner_data = self.cache.get('banners', {}).get(tenant)
89+
expires = 0
90+
if cached_banner_data is not None:
91+
expires = cached_banner_data.get('expires', 0)
92+
return expires < int(time.time())
93+
94+
def save_banner(self, tenant: str, banner: dict) -> bool:
95+
# if someone called this then we simply save the banner
96+
# regardless of whether the cached record has expired yet
97+
# as we assume the caller knows what they are doing
98+
99+
cached_banner_data = self.cache.get('banners', {}).get(tenant, {})
100+
cached_hash = cached_banner_data.get('hash', '')
101+
new_hash = hashlib.sha512(string=json.dumps(banner, default=str, sort_keys=True).encode('utf-8')).hexdigest()
102+
self.cache['banners'][tenant] = {
103+
'hash': new_hash,
104+
'expires': int(time.time()) + (5 * 60)
105+
}
106+
self.write()
107+
108+
# return True if the hashes have changed, False is they are equal
109+
return cached_hash != new_hash

src/pybritive/helpers/cloud_credential_printer.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import configparser
22
import json
33
import os
4+
import subprocess
45
from pathlib import Path
56
import platform
67
import uuid
78
import webbrowser
89
import click
10+
import hashlib
911

1012

1113
# trailing spaces matter as some options do not have the trailing space
@@ -64,6 +66,8 @@ def print(self):
6466
self.print_gcloudauth()
6567
if self.mode == 'kube':
6668
self.print_kube()
69+
if self.mode == 'gcloudauthexec':
70+
self.exec_gcloudauthexec()
6771

6872
def print_console(self):
6973
url = self.credentials.get('url', self.credentials)
@@ -97,6 +101,9 @@ def print_azps(self):
97101
def print_gcloudauth(self):
98102
self._not_implemented()
99103

104+
def exec_gcloudautoauth(self):
105+
self._not_implemented()
106+
100107
def print_kube(self):
101108
self._not_implemented()
102109

@@ -246,7 +253,7 @@ def print_json(self):
246253
def print_gcloudauth(self):
247254
# get path to gcloud key file
248255
if not self.gcloud_key_file: # if --gcloud-key-file not provided
249-
path = Path(self.cli.config.path).parent / 'pybritive-gcloud-key-files' / f'{uuid.uuid4().hex}.json'
256+
path = Path(self.cli.config.gcloud_key_file_path) / f'{uuid.uuid4().hex}.json'
250257
else: # we need to parse out/sanitize what was provided
251258
path = Path(self.gcloud_key_file).expanduser().absolute()
252259

@@ -259,6 +266,29 @@ def print_gcloudauth(self):
259266
ignore_silent=True
260267
)
261268

269+
def exec_gcloudauthexec(self):
270+
key_file = self.cli.build_gcloud_key_file_for_gcloudauthexec(profile=self.profile)
271+
path = Path(self.cli.config.gcloud_key_file_path) / key_file
272+
273+
# key file does not yet exist so write to it
274+
path.parent.mkdir(exist_ok=True, parents=True)
275+
path.write_text(json.dumps(self.credentials, indent=2), encoding='utf-8')
276+
277+
try:
278+
commands = [
279+
'gcloud',
280+
'auth',
281+
'activate-service-account',
282+
self.credentials['client_email'],
283+
'--key-file',
284+
str(path),
285+
'--verbosity=error'
286+
]
287+
288+
subprocess.run(commands, check=True)
289+
except Exception as e:
290+
self.cli.print(f'error running `gcloud auth activate-service-account ...`: {str(e)}')
291+
262292

263293
class KubernetesCredentialPrinter(CloudCredentialPrinter):
264294
def __init__(self, console, mode, profile, silent, credentials, cli, k8s_processor):

0 commit comments

Comments
 (0)