Skip to content

Commit 162648a

Browse files
authored
Merge pull request #64 from britive/develop
v1.1.0
2 parents d23870a + d7b787c commit 162648a

9 files changed

Lines changed: 174 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,23 @@
22

33
All changes to the package starting with v0.3.1 will be logged here.
44

5+
## v1.1.0 [2023-02-16]
6+
#### What's New
7+
* Allowing 2 part `PROFILE` parameters (see documentation for details)
8+
* Build support for multiple environment name formats (name, id, alternate environment name) for the `PROFILE` parameter
9+
10+
#### Enhancements
11+
* None
12+
13+
#### Bug Fixes
14+
* add a default checkout mode for AWS - bug fix as the effort is to match parity with legacy CLI tool
15+
16+
#### Dependencies
17+
* None
18+
19+
#### Other
20+
* None
21+
522
## v1.0.0 [2023-02-09]
623
#### What's New
724
* Moving out of beta and into general availability. No other changes except for documentation updates reflecting the move out of beta.
@@ -11,7 +28,7 @@ All changes to the package starting with v0.3.1 will be logged here.
1128

1229
#### Bug Fixes
1330
* None
14-
*
31+
1532
#### Dependencies
1633
* None
1734

docs/index.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,44 @@ Britive tenant. The below list is the order of operations for determining the to
7171
3. Value retrieved from environment variable `BRITIVE_API_TOKEN`
7272
4. If none of the above are available an interactive login will be performed and temporary credentials will be stored locally for future use with the CLI
7373

74+
## `PROFILE` Parameter Construction (`checkout` and `checkin`)
75+
76+
The general construction of a `PROFILE` parameter for `checkout` and `checkin` (in addition to profile aliases)
77+
is in the format `Application Name/Environment Name/Profile Name`.
78+
79+
Behind the scenes `pybritive` will always use this format. However, there are specific application types where
80+
`Application Name == Environment Name`. For these application types, it is acceptable to provide a 2 part `PROFILE`
81+
parameter in the format `Application Name/Profile Name`. `pybritive` will convert this to the required 3 part
82+
format before interacting with backend services.
83+
84+
Additionally, `ls profiles -f list` and `cache profiles` will return the 2 part format where applicable. It is still acceptable
85+
to provide the 3 part format in all cases so any existing profile aliases or other configurations will not be impacted.
86+
87+
Below is the list of application types in which a 2 part format is acceptable.
88+
89+
* GCP
90+
* Azure
91+
* Oracle
92+
* Google Workspace
93+
94+
The list can be generated (assuming the caller has the required permissions) on demand with the following command.
95+
96+
~~~bash
97+
pybritive api applications.catalog \
98+
--query '[*].{"application type": name,"2 part format allowed":requiresHierarchicalModel}' \
99+
--format table
100+
~~~
101+
102+
Additionally, the `Environment Name` can be any one of three values. AWS example values are provided.
103+
104+
* `environmentId` - 123456789012
105+
* `environmentName` - 123456789012 (Sigma Labs)
106+
* `alternateEnvironmentName` - Sigma Labs
107+
108+
Any of the above values in the `Environment Name` position will be accepted.
109+
110+
When running `ls profiles -f list` and `cache profiles`, the `environmentName` field will be shown.
111+
74112

75113
## Workload Federation Providers
76114

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[metadata]
22
name = pybritive
3-
version = 1.0.0
3+
version = 1.1.0
44
author = Britive Inc.
55
author_email = support@britive.com
66
description = A pure Python CLI for Britive

src/pybritive/britive_cli.py

Lines changed: 77 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,8 @@ def list_profiles(self, checked_out: bool = False):
211211
self.list_separator = '/'
212212
row.pop('Description')
213213
row.pop('Type')
214+
if profile['2_part_profile_format_allowed']:
215+
row.pop('Environment')
214216
data.append(row)
215217

216218
# set special list output if needed
@@ -223,7 +225,6 @@ def list_profiles(self, checked_out: bool = False):
223225
if self.output_format == 'list-profiles':
224226
self.output_format = 'list'
225227

226-
227228
def list_applications(self):
228229
self.login()
229230
self._set_available_profiles()
@@ -276,12 +277,14 @@ def _set_available_profiles(self):
276277
'app_description': app['appDescription'],
277278
'env_name': env['environmentName'],
278279
'env_id': env['environmentId'],
280+
'env_short_name': env['alternateEnvironmentName'],
279281
'env_description': env['environmentDescription'],
280282
'profile_name': profile['profileName'],
281283
'profile_id': profile['profileId'],
282284
'profile_allows_console': profile['consoleAccess'],
283285
'profile_allows_programmatic': profile['programmaticAccess'],
284-
'profile_description': profile['profileDescription']
286+
'profile_description': profile['profileDescription'],
287+
'2_part_profile_format_allowed': app['requiresHierarchicalModel']
285288
}
286289
data.append(row)
287290
self.available_profiles = data
@@ -300,7 +303,7 @@ def __get_cloud_credential_printer(self, app_type, console, mode, profile, silen
300303
if app_type in ['AWS', 'AWS Standalone']:
301304
return printer.AwsCloudCredentialPrinter(
302305
console=console,
303-
mode=mode,
306+
mode=mode or self.config.aws_default_checkout_mode(), # handle the aws default_checkout_mode here
304307
profile=profile,
305308
credentials=credentials,
306309
silent=silent,
@@ -350,10 +353,15 @@ def _checkout(self, profile_name, env_name, app_name, programmatic, blocktime, m
350353
try:
351354
self.login()
352355

353-
return self.b.my_access.checkout_by_name(
356+
ids = self._convert_names_to_ids(
354357
profile_name=profile_name,
355358
environment_name=env_name,
356-
application_name=app_name,
359+
application_name=app_name
360+
)
361+
362+
return self.b.my_access.checkout(
363+
profile_id=ids['profile_id'],
364+
environment_id=ids['environment_id'],
357365
programmatic=programmatic,
358366
include_credentials=True,
359367
wait_time=blocktime,
@@ -381,8 +389,10 @@ def _should_check_force_renew(app, force_renew, console):
381389
def _split_profile_into_parts(self, profile):
382390
profile_real = self.config.profile_aliases.get(profile, profile)
383391
parts = profile_split(profile_real)
392+
if len(parts) == 2: # handle shortcut for profiles where the app and environment name are the same
393+
parts = [parts[0], parts[0], parts[1]]
384394
if len(parts) != 3:
385-
raise click.ClickException('Provided profile string does not have the required 3 parts.')
395+
raise click.ClickException('Provided profile string does not have the required parts.')
386396
parts_dict = {
387397
'app': parts[0],
388398
'env': parts[1],
@@ -434,7 +444,7 @@ def checkout(self, alias, blocktime, console, justification, mode, maxpolltime,
434444
credentials = response['credentials']
435445

436446
# this handles the --force-renew flag
437-
# lets check to see if the we should checkin this profile first and check it out again
447+
# lets check to see if we should checkin this profile first and check it out again
438448
if self._should_check_force_renew(app_type, force_renew, console):
439449
expiration = datetime.fromisoformat(credentials['expirationTime'].replace('Z', ''))
440450
now = datetime.utcnow()
@@ -446,7 +456,7 @@ def checkout(self, alias, blocktime, console, justification, mode, maxpolltime,
446456
credential_process_creds_found = False # need to write new creds to cache
447457
credentials = response['credentials']
448458

449-
if alias: # do this down here so we know that the profile is valid and a checkout was successful
459+
if alias: # do this down here, so we know that the profile is valid and a checkout was successful
450460
self.config.save_profile_alias(alias=alias, profile=profile)
451461

452462
if mode == 'awscredentialprocess' and not credential_process_creds_found:
@@ -574,8 +584,11 @@ def cache_profiles(self, load=True):
574584
for p in self.available_profiles:
575585
profile = self.escape_profile_element(p['app_name'])
576586
profile += '/'
577-
profile += self.escape_profile_element(p['env_name'])
578-
profile += '/'
587+
588+
if not p['2_part_profile_format_allowed']:
589+
profile += self.escape_profile_element(p['env_name'])
590+
profile += '/'
591+
579592
profile += self.escape_profile_element(p['profile_name'])
580593
profiles.append(profile)
581594
Cache().save_profiles(profiles)
@@ -669,9 +682,63 @@ def api(self, method, parameters={}, query=None):
669682
# output the response, optionally filtering based on provided jmespath query/search
670683
self.print(jmespath.search(query, response) if query else response)
671684

685+
# yes - this method exits in b.my_access as _get_profile_and_environment_ids_given_names
686+
# but we are doing additional business logic here to enhance the cli experience so there is
687+
# a need to duplicate some of this logic (although we are doing additional work here so the logic is
688+
# not 100% duplicated)
689+
def _convert_names_to_ids(self, profile_name: str, environment_name: str, application_name: str) -> dict:
690+
# set the available profiles if this is not already done
691+
self._set_available_profiles()
692+
693+
# do some sanitization just in case
694+
profile_name = profile_name.lower().strip()
695+
environment_name = environment_name.lower().strip()
696+
application_name = application_name.lower().strip()
697+
698+
found_profiles = {}
672699

700+
# collect relevant profile/environment combinations to which the identity is entitled
701+
for profile in self.available_profiles:
702+
if profile['app_name'].lower() != application_name: # kick out all the unmatched applications
703+
continue
704+
if profile['profile_name'].lower() != profile_name: # kick out the unmatched profiles
705+
continue
706+
707+
# if we get here we know we are on a record that is the right app and right profile
708+
709+
found_profile_id = profile['profile_id']
673710

711+
if found_profile_id not in found_profiles.keys():
712+
found_profiles[found_profile_id] = []
674713

714+
# load up multiple options
715+
env_options = [
716+
profile['env_name'].lower(),
717+
profile['env_id'].lower(),
718+
profile['env_short_name'].lower()
719+
]
720+
721+
if environment_name in env_options:
722+
found_profiles[found_profile_id].append(profile['env_id'])
723+
724+
# let's first check to ensure we have only 1 profile
725+
if len(found_profiles.keys()) == 0:
726+
raise click.ClickException('no profile found with the provided application, environment, and profile names')
727+
if len(found_profiles.keys()) > 1:
728+
raise click.ClickException('multiple matching profiles found - cannot determine which profile to use')
729+
730+
# and now we can check to ensure we have only 1 environment
731+
found_profile_id = list(found_profiles.keys())[0]
732+
possible_environments = found_profiles[found_profile_id]
733+
if len(possible_environments) == 0:
734+
raise click.ClickException('no profile found with the provided application, environment, and profile names')
735+
if len(possible_environments) > 1:
736+
raise click.ClickException('multiple matching profiles found - cannot determine which profile to use')
737+
738+
return {
739+
'profile_id': found_profile_id,
740+
'environment_id': possible_environments[0]
741+
}
675742

676743

677744

src/pybritive/commands/checkout.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
'gcloud_key_file,verbose,tenant,token,passphrase,federation_provider')
1111
@click.argument('profile', shell_complete=profile_completer)
1212
def checkout(ctx, alias, blocktime, console, justification, mode, maxpolltime, silent, force_renew,
13-
aws_credentials_file, gcloud_key_file,verbose, tenant, token, passphrase, federation_provider, profile):
13+
aws_credentials_file, gcloud_key_file, verbose, tenant, token, passphrase, federation_provider, profile):
1414
"""Checkout a profile.
1515
1616
This command takes 1 required argument `PROFILE`. This should be a string representation of the profile

src/pybritive/helpers/cloud_credential_printer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def __init__(self, app_type, console, mode, profile, silent, credentials, cli):
2222
self.app_type = app_type
2323
self.profile = profile
2424
self.console = console
25+
mode = mode or 'json' # set a default if nothing is provided via flag --mode/-m
2526
helper = mode.split('-')
2627
env_prefix = helper[1] if 1 < len(helper) else None
2728
self.mode = helper[0]

src/pybritive/helpers/config.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import toml
88
from ..choices.output_format import output_format_choices
99
from ..choices.backend import backend_choices
10+
from ..choices.mode import mode_choices
1011
from britive.britive import Britive
1112
from ..helpers.split import profile_split
1213

@@ -37,7 +38,8 @@ def coalesce(*arg):
3738

3839
non_tenant_sections = [
3940
'global',
40-
'profile-aliases'
41+
'profile-aliases',
42+
'aws'
4143
]
4244

4345
global_fields = [
@@ -52,6 +54,10 @@ def coalesce(*arg):
5254
'output_format'
5355
]
5456

57+
aws_fields = [
58+
'default_checkout_mode'
59+
]
60+
5561

5662
class ConfigManager:
5763
def __init__(self, cli: object, tenant_name: str = None):
@@ -183,6 +189,7 @@ def import_global_npm_config(self):
183189
npm_config = toml.load(f)
184190
tenant = npm_config.get('tenantURL', '').replace('https://', '').replace('.britive-app.com', '').lower()
185191
output_format = npm_config.get('output_format', '').lower()
192+
aws_section = npm_config.get('AWS', None)
186193

187194
# reset the config as we are building a new one
188195
self.config = {
@@ -198,6 +205,12 @@ def import_global_npm_config(self):
198205
self.cli.print(f'Found default output format {output_format}.')
199206
self.config['global']['output_format'] = output_format
200207

208+
if aws_section:
209+
checkout_mode = aws_section.get('checkoutMode')
210+
if checkout_mode:
211+
self.cli.print(f'Found aws default checkout mode of {checkout_mode}.')
212+
self.config['aws'] = {'default_checkout_mode': checkout_mode.lower()}
213+
201214
self.save()
202215
self.load(force=True)
203216

@@ -207,6 +220,10 @@ def backend(self):
207220
self.load()
208221
return self.config.get('global', {}).get('credential_backend', 'encrypted-file')
209222

223+
def aws_default_checkout_mode(self):
224+
self.load()
225+
return self.config.get('aws', {}).get('default_checkout_mode', None)
226+
210227
def update(self, section, field, value):
211228
self.load()
212229
if section not in self.config.keys():
@@ -226,6 +243,8 @@ def validate(self):
226243
self.validate_global(section, fields)
227244
if section == 'profile-aliases':
228245
self.validate_profile_aliases(section, fields)
246+
if section == 'aws':
247+
self.validate_aws(section, fields)
229248
if section.startswith('tenant-'):
230249
self.validate_tenant(section, fields)
231250

@@ -262,6 +281,14 @@ def validate_profile_aliases(self, section, fields):
262281
'separated by a /'
263282
self.validation_error_messages.append(error)
264283

284+
def validate_aws(self, section, fields):
285+
for field, value in fields.items():
286+
if field not in aws_fields:
287+
self.validation_error_messages.append(f'Invalid {section} field {field} provided.')
288+
if field == 'default_checkout_mode' and value not in mode_choices.choices:
289+
error = f'Invalid {section} field {field} value {value} provided. Invalid value choice.'
290+
self.validation_error_messages.append(error)
291+
265292
def validate_tenant(self, section, fields):
266293
for field, value in fields.items():
267294
if field not in tenant_fields:

src/pybritive/options/mode.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,18 @@
22
from ..choices.mode import mode_choices
33

44

5+
# as of v1.1.0 not setting a default value here on purpose as the config file now has an
6+
# aws section which provides a default value if the --mode option is omitted
7+
# the default to `json` will occur now in helpers/cloud_credential_printer::CloudCredentialPrinter.__init__
58
option = click.option(
69
'--mode', '-m',
7-
default='json',
810
type=mode_choices,
911
show_choices=True,
10-
show_default=True,
1112
help='The way in which the checked out credentials are presented. `integrate` will place the credentials into '
1213
'the cloud providers local credential file (AWS only). Value `env` can optionally include terminal specific '
1314
'options for setting environment variables '
1415
'(example: env-nix for Linux/Mac, env-wincmd for Windows Command Prompt, env-winps for Windows PowerShell).'
15-
'`gcloudauth` will save the generated key file/credentials to the pybritive config directory and generate a '
16-
'gcloud auth command which can be directly evaluated.'
16+
'`gcloudauth` will save the generated key file/credentials to the pybritive config directory and generate a '
17+
'gcloud auth command which can be directly evaluated. Will default to `json` if not provided.'
1718
)
1819

tests/test_0200_configure.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,8 @@ def test_configure_update_tenant_correct_data(runner, cli):
170170
common_asserts(result, substring=['name=pybritivetest1.dev'])
171171
# set it back
172172
runner.invoke(cli, f'configure update tenant-{tenant} name {tenant}'.split(' '))
173+
174+
175+
def test_configure_update_aws_data(runner, cli):
176+
result = runner.invoke(cli, f'configure update aws default_checkout_mode integrate'.split(' '))
177+
common_asserts(result, substring=['aws', 'default_checkout_mode=integrate'])

0 commit comments

Comments
 (0)