Skip to content

Commit 43a130c

Browse files
authored
Merge pull request #186 from britive/v2.1.0-rc.4
v2.1.0-rc.4
2 parents 446d7f8 + bf4a8ae commit 43a130c

7 files changed

Lines changed: 168 additions & 58 deletions

File tree

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,28 @@
33
> As of v1.4.0, release candidates will be published in an effort to get new features out faster while still allowing
44
> time for full QA testing before moving the release candidate to a full release.
55
6+
## v2.1.0-rc.4 [2025-03-06]
7+
8+
__What's New:__
9+
10+
* Added "Global Settings" section to docs site.
11+
12+
__Enhancements:__
13+
14+
* Additional `global` config settings: `my_[access|resources]_retrieval_limit` to limit size of retrieved items.
15+
16+
__Bug Fixes:__
17+
18+
* Fixed missing `exceptions.StepUpAuthRequiredButNotProvided` catch during `checkout`.
19+
20+
__Dependencies:__
21+
22+
* None
23+
24+
__Other:__
25+
26+
* Allow `_` uniformity for `auto_refresh_[kube_config|profile_cache]` in `global` config.
27+
628
## v2.1.0-rc.3 [2025-02-28]
729

830
__What's New:__

docs/index.md

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,60 @@ __windows (cmd):__
113113
set REQUESTS_CA_BUNDLE="C:\Users\User\AppData\Local\corp-proxy\cacert.pem"
114114
```
115115

116+
### Global Settings
117+
118+
#### `credential_backend`
119+
120+
The backend used to store temporary access tokens to authenticate against the Britive tenant.
121+
122+
_Allowed value:_ `encrypted-file` or `file`
123+
124+
#### `default_tenant`
125+
126+
The name of the tenant used by default: [tenant].britive-app.com.
127+
128+
_Allowed value:_ the name of a configured tenant alias, e.g. `[tenant-sigma]` would be `sigma`.
129+
130+
#### `output_format`
131+
132+
Display output format.
133+
134+
If `table` is used, an optional table format can be specified as `table-format`, formats can be found here: [table_format](https://github.com/astanin/python-tabulate#table_format).
135+
136+
_Allowed value:_ `json`, `yaml`, `csv`, or `table[-format]`
137+
138+
> _NOTE:_ the following global config settings are NOT available directly via `pybritive configure global`
139+
140+
#### `auto_refresh_kube_config`
141+
142+
Auto refresh the cached Britive managed kube config.
143+
144+
_Allowed value:_ `true` or `false`
145+
146+
#### `auto_refresh_profile_cache`
147+
148+
Auto refresh the cached Britive profiles.
149+
150+
_Allowed value:_ `true` or `false`
151+
152+
#### `ca_bundle`
153+
154+
The custom TLS certificate to use when making HTTP requests.
155+
156+
_Allowed value:_ the path to a custom TLS certificate, e.g. `/location/of/the/CA_BUNDLE_FILE.pem`
157+
158+
#### `my_access_retrieval_limit`
159+
160+
Limit the number of "My Access" profiles to be retrieved.
161+
162+
_Allowed value:_ an integer greater than `0`
163+
164+
#### `my_resources_retrieval_limit`
165+
166+
Limit the number of "My Resources" items to be retrieved.
167+
168+
_Allowed value:_ an integer greater than `0`
169+
116170
## Tenant Configuration
117171

118172
Before `pybritive` can connect to a Britive tenant, it needs to know some details about that tenant.
@@ -758,13 +812,13 @@ The cache will not be updated over time. In order to update the cache more regul
758812
Note that this config flag is NOT available directly via `pybritive configure global ...`.
759813
760814
```sh
761-
pybritive configure update global auto-refresh-profile-cache true
815+
pybritive configure update global auto_refresh_profile_cache true
762816
```
763817
764818
To turn the feature off run
765819
766820
```sh
767-
pybritive configure update global auto-refresh-profile-cache false
821+
pybritive configure update global auto_refresh_profile_cache false
768822
pybritive cache clear
769823
```
770824

src/pybritive/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '2.1.0-rc.3'
1+
__version__ = '2.1.0-rc.4'

src/pybritive/britive_cli.py

Lines changed: 64 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,6 @@ def login(self, explicit: bool = False, browser: str = default_browser):
162162
should_get_profiles = any([self.config.auto_refresh_profile_cache(), self.config.auto_refresh_kube_config()])
163163
if explicit and should_get_profiles:
164164
self._set_available_profiles() # will handle calling cache_profiles() and construct_kube_config()
165-
166165
self._display_banner()
167166

168167
def _display_banner(self):
@@ -343,7 +342,11 @@ def list_resources(self):
343342
self.login()
344343
found_resource_names = []
345344
resources = []
346-
for item in self.b.my_resources.list_profiles():
345+
if resource_profile_limit := int(self.config.my_resources_retrieval_limit):
346+
profiles = self.b.my_resources.list(size=resource_profile_limit)['data']
347+
else:
348+
profiles = self.b.my_resources.list_profiles()
349+
for item in profiles:
347350
name = item['resourceName']
348351
if name not in found_resource_names:
349352
resources.append(
@@ -389,7 +392,6 @@ def list_profiles(self, checked_out: bool = False, profile_type: Optional[str] =
389392
if profile_is_checked_out:
390393
row['Expiration'] = checked_out_profiles[key]['expiration']
391394
total_seconds = checked_out_profiles[key]['expires_in_seconds']
392-
393395
hours, remainder = divmod(total_seconds, 3600)
394396
minutes, seconds = divmod(remainder, 60)
395397
time_format = f'{hours:02d}:{minutes:02d}:{seconds:02d}'
@@ -406,7 +408,7 @@ def list_profiles(self, checked_out: bool = False, profile_type: Optional[str] =
406408
if profile['2_part_profile_format_allowed']:
407409
row.pop('Environment', None)
408410
elif self.output_format == 'json':
409-
row['Name'] = f"{row['Application']}/{row['Environment']}/{row['Profile']}"
411+
row['Name'] = f'{row["Application"]}/{row["Environment"]}/{row["Profile"]}'
410412

411413
data.append(row)
412414

@@ -462,29 +464,43 @@ def _set_available_profiles(self, from_cache_command=False, profile_type: Option
462464
if not self.available_profiles:
463465
data = []
464466
if not profile_type or profile_type == 'my-access':
465-
for app in self.b.my_access.list_profiles():
466-
for profile in app.get('profiles', []):
467-
for env in profile.get('environments', []):
468-
row = {
469-
'app_name': app['appName'],
470-
'app_id': app['appContainerId'],
471-
'app_type': app['catalogAppName'],
472-
'app_description': app['appDescription'],
473-
'env_name': env['environmentName'],
474-
'env_id': env['environmentId'],
475-
'env_short_name': env['alternateEnvironmentName'],
476-
'env_description': env['environmentDescription'],
477-
'profile_name': profile['profileName'],
478-
'profile_id': profile['profileId'],
479-
'profile_allows_console': profile['consoleAccess'],
480-
'profile_allows_programmatic': profile['programmaticAccess'],
481-
'profile_description': profile['profileDescription'],
482-
'2_part_profile_format_allowed': app['requiresHierarchicalModel'],
483-
'env_properties': env.get('profileEnvironmentProperties', {}),
484-
}
485-
data.append(row)
467+
access_profile_limit = int(self.config.my_access_retrieval_limit)
468+
access_data = self.b.my_access.list(size=access_profile_limit)
469+
apps = {app['appContainerId']: app for app in access_data['apps']}
470+
envs = {env['environmentId']: env for env in access_data['environments']}
471+
accs = {
472+
p: {'env_id': v, 'types': [t['accessType'] for t in access_data['accesses'] if t['papId'] == p]}
473+
for p, v in {(a['papId'], a['environmentId']) for a in access_data['accesses']}
474+
}
475+
for profile in access_data['profiles']:
476+
acc = accs.get(profile['papId'], {})
477+
env = envs.get(acc.get('env_id'), {}) # Get environment info or default to empty dict
478+
app = apps.get(profile['appContainerId'], {})
479+
row = {
480+
'app_name': profile['catalogAppDisplayName'],
481+
'app_id': profile['appContainerId'],
482+
'app_type': profile['catalogAppName'],
483+
'app_description': app.get('appDescription'),
484+
'env_name': env.get('environmentName', ''), # Pull from `environments`
485+
'env_id': env.get('environmentId', ''), # Pull from `environments`
486+
'env_short_name': env.get('alternateEnvironmentName', ''), # Pull from `environments`
487+
'env_description': env.get('environmentDescription', ''), # Pull from `environments`
488+
'profile_name': profile['papName'],
489+
'profile_id': profile['papId'],
490+
'profile_allows_console': 'CONSOLE' in acc.get('types'),
491+
'profile_allows_programmatic': 'PROGRAMMATIC' in acc.get('types'),
492+
'profile_description': profile['papDescription'],
493+
'2_part_profile_format_allowed': app['requiresHierarchicalModel'],
494+
'env_properties': env.get('profileEnvironmentProperties', {}), # Pull from `environments`
495+
}
496+
data.append(row)
486497
if self.b.feature_flags.get('server-access') and (not profile_type or profile_type == 'my-resources'):
487-
for item in self.b.my_resources.list_profiles():
498+
if not (resource_profile_limit := int(self.config.my_resources_retrieval_limit)):
499+
profiles = self.b.my_resources.list_profiles()
500+
else:
501+
profiles = self.b.my_resources.list(size=resource_profile_limit)
502+
profiles = profiles['data']
503+
for item in profiles:
488504
row = {
489505
'app_name': None,
490506
'app_id': None,
@@ -697,6 +713,8 @@ def _checkout(
697713
if mode == 'awscredentialprocess':
698714
raise e
699715
raise click.ClickException('approval required and no justification provided.') from e
716+
except exceptions.StepUpAuthRequiredButNotProvided as e:
717+
raise click.ClickException('Step Up Authentication required and no OTP provided.') from e
700718
except ValueError as e:
701719
raise click.BadParameter(str(e)) from e
702720
except Exception as e:
@@ -761,21 +779,25 @@ def _profile_is_for_resource(self, profile, profile_type):
761779
return real_profile_name.startswith(f'{self.resource_profile_prefix}')
762780

763781
def _resource_checkout(self, blocktime, justification, maxpolltime, profile, ticket_id, ticket_type):
764-
self.login()
765-
resource_name, profile_name = self._split_resource_profile_into_parts(profile=profile)
766-
response = self.b.my_resources.checkout_by_name(
767-
include_credentials=True,
768-
justification=justification,
769-
max_wait_time=maxpolltime,
770-
profile_name=profile_name[0],
771-
progress_func=self.checkout_callback_printer, # callback will handle silent, isatty, etc.
772-
resource_name=resource_name,
773-
response_template=profile_name[1] if len(profile_name) > 1 else None,
774-
ticket_id=ticket_id,
775-
ticket_type=ticket_type,
776-
wait_time=blocktime,
777-
)
778-
return response['credentials']
782+
try:
783+
self.login()
784+
resource_name, profile_name = self._split_resource_profile_into_parts(profile=profile)
785+
return self.b.my_resources.checkout_by_name(
786+
include_credentials=True,
787+
justification=justification,
788+
max_wait_time=maxpolltime,
789+
profile_name=profile_name[0],
790+
progress_func=self.checkout_callback_printer, # callback will handle silent, isatty, etc.
791+
resource_name=resource_name,
792+
response_template=profile_name[1] if len(profile_name) > 1 else None,
793+
ticket_id=ticket_id,
794+
ticket_type=ticket_type,
795+
wait_time=blocktime,
796+
)['credentials']
797+
except exceptions.ApprovalRequiredButNoJustificationProvided as e:
798+
raise click.ClickException('approval required and no justification provided.') from e
799+
except exceptions.StepUpAuthRequiredButNotProvided as e:
800+
raise click.ClickException('Step Up Authentication required and no OTP provided.') from e
779801

780802
def _access_checkout(
781803
self,
@@ -1455,7 +1477,7 @@ def clear_cached_aws_credentials(self, profile):
14551477
# profile name as well - it will not hurt anything to try to clear
14561478
# both versions
14571479
parts = self._split_profile_into_parts(profile)
1458-
Cache().clear_credentials(profile_name=f"{parts['app']}/{parts['env']}/{parts['profile']}")
1480+
Cache().clear_credentials(profile_name=f'{parts["app"]}/{parts["env"]}/{parts["profile"]}')
14591481

14601482
def ssh_gcp_identity_aware_proxy(self, username, hostname, push_public_key, port_number, key_source):
14611483
self.silent = True

src/pybritive/helpers/aws_credential_process.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,8 @@ def get_args():
160160

161161
def usage():
162162
print(
163-
f'Usage : {argv[0]} --profile <profile> [-t/--tenant, -T/--token, -p/--passphrase, -f/--force-renew, '
164-
f'-F/--federation-provider]'
163+
f'Usage : {argv[0]} -P/--profile <profile> [-t/--tenant, -T/--token, -p/--passphrase, -f/--force-renew,'
164+
' -F/--federation-provider]'
165165
)
166166
raise SystemExit
167167

src/pybritive/helpers/config.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,14 @@ def coalesce(*arg):
4040
non_tenant_sections = ['global', 'profile-aliases', 'aws', 'gcp']
4141

4242
global_fields = [
43+
'auto_refresh_kube_config',
44+
'auto_refresh_profile_cache',
45+
'ca_bundle',
46+
'credential_backend',
4347
'default_tenant',
4448
'output_format',
45-
'credential_backend',
46-
'auto-refresh-profile-cache',
47-
'auto-refresh-kube-config',
48-
'ca_bundle',
49+
'my_access_retrieval_limit',
50+
'my_resources_retrieval_limit',
4951
]
5052

5153
tenant_fields = ['name', 'output_format', 'sso_idp']
@@ -73,6 +75,8 @@ def __init__(self, cli: object, tenant_name: Optional[str] = None):
7375
self.validation_error_messages = []
7476
self.gcloud_key_file_path: str = str(Path(self.path).parent / 'pybritive-gcloud-key-files')
7577
self.global_ca_bundle = None
78+
self.my_access_retrieval_limit = None
79+
self.my_resources_retrieval_limit = None
7680

7781
def clear_gcloud_auth_key_files(self, profile=None):
7882
path = Path(self.gcloud_key_file_path)
@@ -121,7 +125,9 @@ def load(self, force=False):
121125
self.tenants_by_name[name] = item
122126
self.aliases_and_names = {**self.tenants, **self.tenants_by_name}
123127
self.profile_aliases = self.config.get('profile-aliases', {})
124-
self.global_ca_bundle = self.config.get('ca_bundle', {})
128+
self.global_ca_bundle = self.config.get('global', {}).get('ca_bundle')
129+
self.my_access_retrieval_limit = self.config.get('global', {}).get('my_access_retrieval_limit', '0')
130+
self.my_resources_retrieval_limit = self.config.get('global', {}).get('my_resources_retrieval_limit', '0')
125131
self.loaded = True
126132

127133
def get_tenant(self):
@@ -260,10 +266,10 @@ def validate_global(self, section, fields):
260266
if field == 'credential_backend' and value not in backend_choices.choices:
261267
error = f'Invalid {section} field {field} value {value} provided. Invalid value choice.'
262268
self.validation_error_messages.append(error)
263-
if field == 'auto-refresh-profile-cache' and value not in ['true', 'false']:
269+
if field.replace('-', '_') == 'auto_refresh_profile_cache' and value not in ['true', 'false']:
264270
error = f'Invalid {section} field {field} value {value} provided. Invalid value choice.'
265271
self.validation_error_messages.append(error)
266-
if field == 'auto-refresh-kube-config' and value not in ['true', 'false']:
272+
if field.replace('-', '_') == 'auto_refresh_kube_config' and value not in ['true', 'false']:
267273
error = f'Invalid {section} field {field} value {value} provided. Invalid value choice.'
268274
self.validation_error_messages.append(error)
269275
if field == 'default_tenant':
@@ -276,6 +282,9 @@ def validate_global(self, section, fields):
276282
if not Path.is_file(ca_bundle_file_path):
277283
error = f'Invalid {field} file {ca_bundle_file_path}. File does not exist.'
278284
self.validation_error_messages.append(error)
285+
if field in ['my_access_retrieval_limit', 'my_resources_retrieval_limit'] and not value.isnumeric():
286+
error = f'Invalid {section} field {field} value {value} provided. Must be an integer.'
287+
self.validation_error_messages.append(error)
279288

280289
def validate_profile_aliases(self, section, fields):
281290
for field, value in fields.items():
@@ -314,10 +323,14 @@ def validate_tenant(self, section, fields):
314323

315324
def auto_refresh_profile_cache(self):
316325
self.load()
317-
value = self.config.get('global', {}).get('auto-refresh-profile-cache', 'false')
326+
value = self.config.get('global', {}).get(
327+
'auto_refresh_profile_cache', self.config.get('global', {}).get('auto-refresh-profile-cache', 'false')
328+
)
318329
return value == 'true'
319330

320331
def auto_refresh_kube_config(self):
321332
self.load()
322-
value = self.config.get('global', {}).get('auto-refresh-kube-config', 'false')
333+
value = self.config.get('global', {}).get(
334+
'auto_refresh_kube_config', self.config.get('global', {}).get('auto-refresh-kube-config', 'false')
335+
)
323336
return value == 'true'

src/pybritive/helpers/credentials.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,7 @@ def _setup_requests_session(self):
8383
self.session = requests.Session()
8484
retries = Retry(total=5, backoff_factor=1, status_forcelist=[429, 500, 502, 503, 504])
8585
self.session.mount('https://', HTTPAdapter(max_retries=retries))
86-
global_ca_bundle = self.cli.config.get_tenant().get('ca_bundle')
87-
if global_ca_bundle:
86+
if global_ca_bundle := self.cli.config.global_ca_bundle:
8887
os.environ['PYBRITIVE_CA_BUNDLE'] = global_ca_bundle
8988
self.session.verify = global_ca_bundle
9089
# allow the disabling of TLS/SSL verification for testing in development (mostly local development)

0 commit comments

Comments
 (0)