Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@

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

## v1.2.0 [2023-03-03]
#### What's New
* Support for Azure Managed Identities as federation providers.

#### Enhancements
* Fall back to reduced functionality (no shell completion) when the python environment is using `click<8.0.0`.

#### Bug Fixes
* If a justification for checkout/secrets viewing is provided, ensure it is <=255 characters.
* Fix issue with extraction of OIDC token expiration time. Moved to `jwt` library to perform the token decode.

#### Dependencies
* Switching `britive` dependency from a compatible version requirement to a `>=` requirement to capture minor updates.
* `britive>=2.16.0` from britive~=2.15.1

#### Other
* Modify the error handling and reporting process to not raise `click.ClickException` exceptions in the `safe_cli` method. Instead, raise the underlying exception so a better error message is provided.

## v1.1.1 [2023-02-16]
#### What's New
* None
Expand Down
33 changes: 33 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,21 @@ pip install $(curl -s https://api.github.com/repos/britive/python-cli/releases/l
The end user is free to install the CLI into a virtual environment or in the global scope, so it is available
everywhere.

If the `pybritive` executable is not found when attempting to invoke the program, you may need to add the location
of the `pybritive` executable to your path. This location can differ by OS and whether you are using a virtualenv or
not. The easiest way to see where executables get installed via `pip install ...` is to run the following command.

~~~
echo `python3 -m site --user-base`/bin
~~~

You will need to add this location to your path. The following command will do that but it is recommended to add
this command into your `.bashrc, .zshrc, etc.` file, so it will always get executed on new terminal windows.

~~~
export PATH=\"`python3 -m site --user-base`/bin:\$PATH\"
~~~

## Tenant Configuration

Before `pybritive` can connect to a Britive tenant, it needs to know some details about that tenant.
Expand Down Expand Up @@ -129,6 +144,12 @@ At feature launch the following types of identity providers are supported for wo
* Github Actions
* AWS
* Bitbucket
* Azure System Assigned Managed Identities
* Azure User Assigned Managed Identities

For more information on Azure Managed Identities reference the below link.

https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview

It is possible to source an identity token from a different OIDC provider and explicitly set it via the `--token\-T` flag.
However, if you are using one of the above providers, a shortcut is provided to abstract away the complexity of sourcing these tokens.
Expand All @@ -154,6 +175,18 @@ pybritive checkout "profile" --federation-provider aws_expirationseconds # use

# bitbucket (note that no additional options are available for bitbucket)
pybritive checkout "profile" --federation-provider bitbucket

# azure system assigned managed identities
pybritive checkout "profile" --federation-provider azuresmi # use system assigned managed identities with the default OIDC audience
pybritive checkout "profile" --federation-provider azuresmi-audience # use system assigned managed identities with a custom OIDC audience
pybritive checkout "profile" --federation-provider azuresmi-audience_expirationseconds # use system assigned managed identities with a custom OIDC audience and set the Britive expiration (in seconds) of the generated token
pybritive checkout "profile" --federation-provider azuresmi_expirationseconds # use system assigned managed identities with the default OIDC audience and set the Britive expiration (in seconds) of the generated token

# azure user assigned managed identities (note that a client id is a required field)
pybritive checkout "profile" --federation-provider azuresmi-clientid # use user assigned managed identities with the default OIDC audience
pybritive checkout "profile" --federation-provider azuresmi-clientid|audience # use user assigned managed identities with a custom OIDC audience
pybritive checkout "profile" --federation-provider azuresmi-clientid|audience_expirationseconds # use user assigned managed identities with a custom OIDC audience and set the Britive expiration (in seconds) of the generated token
pybritive checkout "profile" --federation-provider azuresmi-clientid_expirationseconds # use user assigned managed identities with the default OIDC audience and set the Britive expiration (in seconds) of the generated token
~~~

In general the field format for `--federation-provider` is `provider-[something provider specific]_[duration in seconds]`.
Expand Down
2 changes: 1 addition & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ nav:
theme: readthedocs
repo_url: https://github.com/britive/python-cli
edit_uri: ''
copyright: 2022 Britive, Inc.
copyright: 2023 Britive, Inc.
markdown_extensions:
- attr_list
- mkdocs-click
Expand Down
5 changes: 3 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
britive~=2.15.0
britive>=2.16.0
certifi>=2022.12.7
charset-normalizer==2.1.0
click==8.1.3
Expand All @@ -17,4 +17,5 @@ mkdocs-click==0.8.0
twine~=4.0.1
python-dateutil~=2.8.2
boto3
jmespath
jmespath
pyjwt
5 changes: 3 additions & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = pybritive
version = 1.1.1
version = 1.2.0
author = Britive Inc.
author_email = support@britive.com
description = A pure Python CLI for Britive
Expand All @@ -26,8 +26,9 @@ install_requires =
toml
cryptography~=39.0.1
python-dateutil
britive~=2.15.0
britive>=2.16.0
jmespath
pyjwt

[options.packages.find]
where = src
Expand Down
14 changes: 12 additions & 2 deletions src/pybritive/britive_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,8 @@ def checkout(self, alias, blocktime, console, justification, mode, maxpolltime,
response = None
self.verbose_checkout = verbose

self._validate_justification(justification)

if mode == 'awscredentialprocess':
self.silent = True # the aws credential process CANNOT output anything other than the expected JSON
# we need to check the credential process cache for the credentials first
Expand Down Expand Up @@ -524,7 +526,8 @@ def configure_global(self, default_tenant_name, output_format, backend):
backend=backend
)

def viewsecret(self, path, blocktime, justification,maxpolltime):
def viewsecret(self, path, blocktime, justification, maxpolltime):
self._validate_justification(justification)
self.login()

try:
Expand Down Expand Up @@ -554,7 +557,8 @@ def viewsecret(self, path, blocktime, justification,maxpolltime):
# and finally print the secret data
self.print(value)

def downloadsecret(self, path, blocktime, justification,maxpolltime, file):
def downloadsecret(self, path, blocktime, justification, maxpolltime, file):
self._validate_justification(justification)
self.login()

try:
Expand Down Expand Up @@ -617,6 +621,7 @@ def configure_update(self, section, field, value):
self.config.update(section=section, field=field, value=value)

def request_submit(self, profile, justification):
self._validate_justification(justification)
self.login()
parts = self._split_profile_into_parts(profile)

Expand Down Expand Up @@ -762,6 +767,11 @@ def _convert_names_to_ids(self, profile_name: str, environment_name: str, applic
'environment_id': possible_environments[0]
}

@staticmethod
def _validate_justification(justification: str):
if justification and len(justification) > 255:
raise ValueError('justification cannot be longer than 255 characters.')




Expand Down
4 changes: 2 additions & 2 deletions src/pybritive/cli_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ def safe_cli():
cli()
except Exception as e:
if debug:
raise click.ClickException(str(e))
raise e
else:
raise click.ClickException(str(e)) from None
raise e from None


# this is the "main" app - it really does nothing but print the overview/help section
Expand Down
4 changes: 2 additions & 2 deletions src/pybritive/commands/checkin.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import click
from ..helpers.build_britive import build_britive
from ..options.britive_options import britive_options
from ..completers.profile import profile_completer
from ..helpers.profile_argument_dectorator import click_smart_profile_argument


@click.command()
@build_britive
@britive_options(names='tenant,token,passphrase,federation_provider')
@click.argument('profile', shell_complete=profile_completer)
@click_smart_profile_argument
def checkin(ctx, tenant, token, passphrase, federation_provider, profile):
"""Checkin a profile.

Expand Down
4 changes: 2 additions & 2 deletions src/pybritive/commands/checkout.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import click
from ..helpers.build_britive import build_britive
from ..options.britive_options import britive_options
from ..completers.profile import profile_completer
from ..helpers.profile_argument_dectorator import click_smart_profile_argument


@click.command()
@build_britive
@britive_options(names='alias,blocktime,console,justification,mode,maxpolltime,silent,force_renew,aws_credentials_file,'
'gcloud_key_file,verbose,tenant,token,passphrase,federation_provider')
@click.argument('profile', shell_complete=profile_completer)
@click_smart_profile_argument
def checkout(ctx, alias, blocktime, console, justification, mode, maxpolltime, silent, force_renew,
aws_credentials_file, gcloud_key_file, verbose, tenant, token, passphrase, federation_provider, profile):
"""Checkout a profile.
Expand Down
9 changes: 5 additions & 4 deletions src/pybritive/commands/request.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import click
from ..helpers.build_britive import build_britive
from ..options.britive_options import britive_options
from ..completers.profile import profile_completer
from ..helpers.profile_argument_dectorator import click_smart_profile_argument


@click.group()
def request():
Expand All @@ -12,7 +13,7 @@ def request():
@request.command()
@build_britive
@britive_options(names='justification,tenant,token,passphrase,federation_provider')
@click.argument('profile', shell_complete=profile_completer)
@click_smart_profile_argument
def submit(ctx, justification, tenant, token, passphrase, federation_provider, profile):
"""Submit a request to checkout a profile.

Expand All @@ -33,7 +34,7 @@ def submit(ctx, justification, tenant, token, passphrase, federation_provider, p
@request.command()
@build_britive
@britive_options(names='tenant,token,passphrase,federation_provider')
@click.argument('profile', shell_complete=profile_completer)
@click_smart_profile_argument
def withdraw(ctx, tenant, token, passphrase, federation_provider, profile):
"""Withdraw a request to checkout a profile.

Expand All @@ -45,4 +46,4 @@ def withdraw(ctx, tenant, token, passphrase, federation_provider, profile):

ctx.obj.britive.request_withdraw(
profile=profile
)
)
13 changes: 11 additions & 2 deletions src/pybritive/completers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
# register the completion scripts since they are not required to be registered anywhere else
from . import powershell_completion
from .import bash_gte_42

import pkg_resources

# click < 8.0.0 does shell completion different...
# not all the classes/decorators are available, so we cannot
# create custom shell completions like we can with click > 8.0.0
click_major_version = int(pkg_resources.get_distribution('click').version.split('.')[0])
if click_major_version >= 8:
from . import powershell_completion
from . import bash_gte_42

18 changes: 13 additions & 5 deletions src/pybritive/helpers/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from .encryption import StringEncryption, InvalidPassphraseException
from britive.britive import Britive
from dateutil import parser
import jwt


interactive_login_fields_to_pop = [
Expand Down Expand Up @@ -144,18 +145,25 @@ def perform_federation_provider_authentication(self):
provider = provider.lower()
token = str(token)

expiration_time = (time.time_ns() // 1000000) + federation_provider_default_expiration_seconds
expiration_time = (int(time.time()) + federation_provider_default_expiration_seconds) * 1000

try:
if provider == 'aws':
token = base64.b64decode(token.encode('utf-8'))
token_expires = json.loads(token)['iam_request_headers']['x-britive-expires']
expiration_time = int(parser.parse(token_expires).timestamp() * 1000)
if provider == 'oidc':
ignore1, token, ignore2 = token.split('.')
token = base64.b64decode(token.encode('utf-8'))
token = json.loads(token)
expiration_time = int(token['exp'] * 1000)
expiration_time = jwt.decode(
token,
# validation of the token will occur on the Britive backend
# so not verifying everything here is okay since we are just
# trying to extract the token expiration time so we can store
# it in the ~/.britive/pybritive.credentials[.encrypted] file
options={
'verify_signature': False,
'verify_aud': False
}
)['exp'] * 1000
except Exception as e:
self.cli.print(f'Cannot obtain token expiration time for {self.federation_provider}. Defaulting to '
f'{federation_provider_default_expiration_seconds} seconds.')
Expand Down
15 changes: 15 additions & 0 deletions src/pybritive/helpers/profile_argument_dectorator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import click
import pkg_resources
from ..completers.profile import profile_completer

click_major_version = int(pkg_resources.get_distribution('click').version.split('.')[0])


def click_smart_profile_argument(func):
if click_major_version >= 8:
dec = click.argument('profile', shell_complete=profile_completer)
else:
dec = click.argument('profile')
return dec(func)


2 changes: 1 addition & 1 deletion src/pybritive/options/justification.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
'--justification', '-j',
default=None,
show_default=True,
help='Justification for the checkout approval process, if the profile checkout requires approval.'
help='Justification for the checkout approval process, if the profile checkout requires approval.',
)

4 changes: 4 additions & 0 deletions test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pip install click==7.1.2
pytest tests/ -vvv
pip install click --upgrade
pytest tests/ -vvv
23 changes: 23 additions & 0 deletions tests/test_0450_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import os
from pathlib import Path
import json


def common_asserts(result, substring=None, exit_code=0):
assert result.exit_code == exit_code
if isinstance(substring, str):
substring = [substring]
for sub in substring:
assert sub in result.output


def test_api(runner, cli):
result = runner.invoke(cli, 'api users.list'.split(' '))
common_asserts(result, ['userId', 'status', 'email', 'identityProvider'])







2 changes: 1 addition & 1 deletion tests/test_0600_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def test_cache_profiles(runner, cli):
assert result.exit_code == 0
assert 'profiles' in data.keys()
assert len(data['profiles']) > 0
assert len(data['profiles'][0].split('/')) == 3
assert len(data['profiles'][0].split('/')) in [2, 3]



Expand Down