Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI and testing enhancements. #366

Open
wants to merge 59 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
4116067
Add support for updating an authenticator.
Sep 14, 2022
f91c866
Allow registering an alias for authenticators.
Sep 14, 2022
6cad7eb
Aliases for webauthn authenticators.
Sep 14, 2022
0de7e23
Print a message after registering webauthn touch.
Sep 14, 2022
e8a2aa6
Add an optional use_keyring config option.
Sep 14, 2022
e6fae51
Add newline at end of file.
Sep 14, 2022
e77ad27
Add use_keyring to README.md
Sep 14, 2022
da12a0f
Replace nosetests with pytest.
Sep 14, 2022
9b04e92
Fix tests to handle alias in registered auth.
Sep 14, 2022
a72a364
Add alias tests for registered authenticators
Sep 14, 2022
5d152a3
Upgrade to okta 2.0.x.
Sep 21, 2022
d6a40c0
Upgrade to python 3.10.
Sep 21, 2022
dafd794
Use multiple RUN commands.
Sep 21, 2022
e9ba4e8
Use pip install instead of setup.py.
Sep 21, 2022
fc6d081
Run tests in docker image.
Sep 21, 2022
3975410
Switch from getpass to pwinput.
Oct 5, 2022
1d78813
Don't import the unused ui, linter fix.
Oct 5, 2022
181ebea
Use assertion.credential['id'] for credentialID.
Oct 5, 2022
77a423e
Replace assert with explicit check.
Oct 5, 2022
17171eb
Support for passing a list of credential IDs.
Oct 5, 2022
e98a740
Rework except logic a little bit.
Oct 5, 2022
a9b63e9
Whitespace cleanup.
Oct 5, 2022
48780ad
Rework the DUO factor checks.
Oct 5, 2022
a9f6d23
Rework preferred factors slightly.
Oct 5, 2022
86c28d7
Consider N Webauthn factors as a single option.
Oct 5, 2022
b07a61d
fido2 1.0.0 support.
Oct 5, 2022
f0e0dc0
Use defusedxml to silence security warnings.
Oct 5, 2022
6787e7a
Use allowed_methods for Retry.
Oct 5, 2022
e6f8eb4
Remove some whitespace on empty lines.
Oct 5, 2022
4722d9e
Allow remember_device to work with webauthn.
Oct 5, 2022
4a6a99a
Cleanup whitespace for linters.
Oct 5, 2022
1a0d785
Attempt to keep and use the same Okta session.
Oct 5, 2022
7c839b9
Add an option to disable session persistence.
Oct 5, 2022
bdfdf99
Fix some whitespace related linter warnings.
Oct 7, 2022
fcb13b6
Specify ctap-keyring-device differently.
Oct 7, 2022
937d224
Catch KeyError instead of AttributeError.
Oct 26, 2022
6fc7b67
Correct the check for disable_session.
Oct 28, 2022
435920e
Fix handling of the name for multiple keys.
Oct 28, 2022
eb82374
Add a new internal error.
Oct 28, 2022
d22fe3b
Rework error handling around calls to webauthn.
Oct 28, 2022
d012bf5
Tweak the session username check.
Oct 28, 2022
15c6d34
Drop the now unused FakeAssertion.
Oct 28, 2022
b07e1e6
Give different 'please touch' text per operation.
Oct 28, 2022
2a6b617
Remove the dead on_keepalive.
Oct 28, 2022
6ad690d
Rework keyring devices a little.
Oct 28, 2022
394728d
Rework _run_in_thread error handling.
Oct 28, 2022
0e9af18
Rewrite make_credential.
Oct 28, 2022
d6c4a57
Move _verify to the new exception handling.
Oct 28, 2022
9e10e53
Remove an unneeded error print.
Oct 28, 2022
0cd0fbc
Merge remote-tracking branch 'origin/master' into enhancements
Nov 28, 2022
e5ff581
Remove spaces to maybe fix nix.
Nov 28, 2022
062c506
Try installing git before running nix build.
Nov 28, 2022
b670eae
Drop python 3.6, add 3.x.
Nov 28, 2022
1c98e83
Try to run apt-get as root.
Nov 28, 2022
f89b7f6
Install git here too.
Nov 28, 2022
7860269
Install git in the Dockerfile.
Nov 28, 2022
2abaac6
Add humps to requirements.txt
Nov 28, 2022
83f940d
Handle the case where we have no 'sid' cookie.
Nov 29, 2022
f417d56
Add an explicit requirement on urllib3 1.26+.
Dec 15, 2022
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
2 changes: 2 additions & 0 deletions .github/workflows/build_nix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Install git
run: sudo apt-get -y install git
- uses: actions/checkout@v3
- uses: cachix/install-nix-action@v17
with:
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,16 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ 3.6, 3.7, 3.8, 3.9 ]
python-version: [ 3.7, 3.8, 3.9, 3.x ]

steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install git
run: sudo apt-get -y install git
- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand Down
14 changes: 8 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
FROM python:3.8-alpine
FROM python:3.10-alpine

WORKDIR /opt/gimme-aws-creds

COPY . .

RUN apk --update add libgcc

ENV PACKAGES="gcc musl-dev python3-dev libffi-dev openssl-dev cargo"
ENV PACKAGES="gcc musl-dev python3-dev libffi-dev openssl-dev cargo git"

RUN apk --update add $PACKAGES \
&& pip install --upgrade pip setuptools-rust \
&& python setup.py install \
&& apk del --purge $PACKAGES
RUN apk --update add $PACKAGES
RUN pip install --upgrade pip setuptools-rust
RUN pip install .
RUN pip install -r requirements_dev.txt
RUN pytest tests
RUN apk del --purge $PACKAGES

ENTRYPOINT ["/usr/local/bin/gimme-aws-creds"]
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ docker-build:
docker build -t gimme-aws-creds .

test: docker-build
nosetests -vv tests

local_test:
pytest -v tests
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ A configuration wizard will prompt you to enter the necessary configuration para
- aws_default_duration = This is optional. Lifetime for temporary credentials, in seconds. Defaults to 1 hour (3600)
- app_url - If using 'appurl' setting for gimme_creds_server, this sets the url to the aws application configured in Okta. It is typically something like <https://something.okta[preview].com/home/amazon_aws/app_instance_id/something>
- okta_username - use this username to authenticate
- preferred_mfa_type - automatically select a particular device when prompted for MFA:
- preferred_mfa_type - automatically select a particular device when prompted for MFA:
- push - Okta Verify App push or DUO push (depends on okta supplied provider type)
- token:software:totp - OTP using the Okta Verify App
- token:hardware - OTP using hardware like Yubikey
Expand All @@ -182,6 +182,8 @@ A configuration wizard will prompt you to enter the necessary configuration para
- include_path - (optional) Includes full role path to the role name in AWS credential profile name. (default: n). If `y`: `<acct>-/some/path/administrator`. If `n`: `<acct>-administrator`
- remember_device - y or n. If yes, the MFA device will be remembered by Okta service for a limited time. This option can also be set interactively in the command line using `-m` or `--remember-device`
- output_format - `json` , `export` or `windows`, determines default credential output format, can be also specified by `--output-format FORMAT` and `-o FORMAT`.
- use_keyring - y or n. Defaults to y. If n, use of the system keyring for password storage is disabled.
- disable_session - y or n. Defaults to n. If y, disables using the session token between gimmie-aws-creds invocations.

## Configuration File

Expand Down
14 changes: 7 additions & 7 deletions gimme_aws_creds/aws.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
See the License for the specific language governing permissions and* limitations under the License.*
"""
import base64
import xml.etree.ElementTree as ET
import defusedxml.ElementTree as ET

import requests
from bs4 import BeautifulSoup
Expand Down Expand Up @@ -39,7 +39,7 @@ def __init__(self, verify_ssl_certs=True):
# Allow up to 5 retries on requests to AWS in case we have network issues
self._http_client = requests.Session()
retries = Retry(total=5, backoff_factor=1,
method_whitelist=['POST'])
allowed_methods=['POST'])
self._http_client.mount('https://', HTTPAdapter(max_retries=retries))

def get_signinpage(self, saml_token, saml_target_url):
Expand All @@ -48,7 +48,7 @@ def get_signinpage(self, saml_token, saml_target_url):
'SAMLResponse': saml_token,
'RelayState': ''
}

response = self._http_client.post(
saml_target_url,
data=payload,
Expand All @@ -58,7 +58,7 @@ def get_signinpage(self, saml_token, saml_target_url):

def _enumerate_saml_roles(self, assertion, saml_target_url):
signin_page = self.get_signinpage(assertion, saml_target_url)

""" using the assertion to fetch aws sign-in page, parse it and return aws sts creds """
role_pairs = []
root = ET.fromstring(base64.b64decode(assertion))
Expand All @@ -80,10 +80,10 @@ def _enumerate_saml_roles(self, assertion, saml_target_url):
raise errors.GimmeAWSCredsError('Parsing error on {}'.format(role_pair))
else:
table[role] = idp

# init parser
soup = BeautifulSoup(signin_page, 'html.parser')

# find all roles
roles = soup.find_all("div", attrs={"class": "saml-role"})
# Normalize pieces of string;
Expand Down Expand Up @@ -120,7 +120,7 @@ def _display_role(roles):
if not current_account == last_account:
role_strs.append(current_account)
last_account = current_account

role_strs.append(' [ {} ]: {}'.format(i, role.friendly_role_name))

return role_strs
35 changes: 33 additions & 2 deletions gimme_aws_creds/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ def get_args(self):
self.roles = [role.strip() for role in args.roles.split(',') if role.strip()]
self.conf_profile = args.profile or 'DEFAULT'

def _handle_config(self, config, profile_config, include_inherits = True):
def _handle_config(self, config, profile_config, include_inherits=True):
if "inherits" in profile_config.keys() and include_inherits:
self.ui.message("Using inherited config: " + profile_config["inherits"])
if profile_config["inherits"] not in config:
Expand All @@ -189,7 +189,7 @@ def _handle_config(self, config, profile_config, include_inherits = True):
else:
return profile_config

def get_config_dict(self, include_inherits = True):
def get_config_dict(self, include_inherits=True):
"""returns the conf dict from the okta config file"""
# Check to see if config file exists, if not complain and exit
# If config file does exist return config dict from file
Expand Down Expand Up @@ -225,6 +225,8 @@ def update_config_file(self):
aws_default_duration = Default AWS session duration (3600)
preferred_mfa_type = Select this MFA device type automatically
include_path - (optional) includes that full role path to the role name for profile
use_keyring - Enables or disables use of the system keyring
disable_session - Disables persistent sessions.

"""
config = configparser.ConfigParser()
Expand All @@ -249,6 +251,8 @@ def update_config_file(self):
'aws_default_duration': '3600',
'device_token': '',
'output_format': 'export',
'use_keyring': 'y',
'disable_session': 'n',
}

# See if a config file already exists.
Expand Down Expand Up @@ -293,6 +297,9 @@ def update_config_file(self):
else:
config_dict['cred_profile'] = defaults['cred_profile']

config_dict['use_keyring'] = self.get_use_keyring(defaults['use_keyring'])
config_dict['disable_session'] = self.get_disable_session(defaults['disable_session'])

self.write_config_file(config_dict)

def write_config_file(self, config_dict):
Expand Down Expand Up @@ -538,6 +545,30 @@ def _get_remember_device(self, default_entry):
except ValueError:
ui.default.warning("Remember the MFA device must be either y or n.")

def _get_use_keyring(self, default_entry):
"""Option to use the system keyring"""
ui.default.message(
"Do you want to use the system keyring?\n"
"Please answer y or n.")
while True:
try:
return self._get_user_input_yes_no(
"Use system keyring", default_entry)
except ValueError:
ui.default.warning("Remember the value must be either y or n.")

def _get_disable_session(self, default_entry):
"""Option to disable storing and using the session token between invocations."""
ui.default.message(
"Do you want to disable storing and using the session token between invocations?\n"
"Please answer y or n.")
while True:
try:
return self._get_user_input_yes_no(
"Disable session token storage", default_entry)
except ValueError:
ui.default.warning("Remember the value must be either y or n.")

def _get_user_input(self, message, default=None):
"""formats message to include default and then prompts user for input
via keyboard with message. Returns user's input or if user doesn't
Expand Down
4 changes: 4 additions & 0 deletions gimme_aws_creds/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ class NoFIDODeviceFoundError(Exception):
pass


class NoEligibleFIDODeviceFoundError(Exception):
pass


class FIDODeviceTimeoutError(Exception):
pass

Expand Down
37 changes: 28 additions & 9 deletions gimme_aws_creds/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
# extras
import boto3
from botocore.exceptions import ClientError
from okta.framework.ApiClient import ApiClient
from okta.framework.OktaError import OktaError
from okta.api_client import APIClient
from okta.errors.error import Error as OktaError

# local imports
from . import errors, ui
Expand Down Expand Up @@ -239,8 +239,8 @@ def _get_aws_account_info(okta_org_url, okta_api_key, username):
""" Call the Okta User API and process the results to return
just the information we need for gimme_aws_creds"""
# We need access to the entire JSON response from the Okta APIs, so we need to
# use the low-level ApiClient instead of UsersClient and AppInstanceClient
users_client = ApiClient(okta_org_url, okta_api_key, pathname='/api/v1/users')
# use the low-level APIClient instead of UsersClient and AppInstanceClient
users_client = APIClient(okta_org_url, okta_api_key, pathname='/api/v1/users')

# Get User information
try:
Expand Down Expand Up @@ -505,7 +505,7 @@ def conf_dict(self):
:rtype: dict
"""
# noinspection PyUnusedLocal
config = self.config
config = self.config # noqa
return self._cache['conf_dict']

@property
Expand Down Expand Up @@ -556,6 +556,16 @@ def okta(self):

okta.set_remember_device(self.config.remember_device
or self.conf_dict.get('remember_device', False))

if self.conf_dict.get('use_keyring') in ('n', 'false', 'False'):
okta.set_use_keyring(False)
else:
okta.set_use_keyring(True)

if self.conf_dict.get('disable_session', 'False') not in ('y', 'true', 'True'):
if self.conf_dict.get('session_token') is not None and self.conf_dict.get('session_username') is not None:
okta.set_session_token(self.conf_dict.get('session_username'), self.conf_dict.get('session_token'))

return okta

def get_resolver(self):
Expand All @@ -575,6 +585,13 @@ def device_token(self):
def set_auth_session(self, auth_session):
self._cache['auth_session'] = auth_session

okta = self._cache['okta']
base_config = self.config.get_config_dict(include_inherits=False)
base_config['session_username'] = auth_session['username']
base_config['session_token'] = auth_session['session']
self.config.write_config_file(base_config)
okta.set_session_token(base_config.get('session_username'), base_config.get('session_token'))

@property
def auth_session(self):
if 'auth_session' in self._cache:
Expand Down Expand Up @@ -794,7 +811,7 @@ def _run(self):
self.handle_action_store_json_creds()
self.handle_action_list_roles()
self.handle_setup_fido_authenticator()

# for each data item, if we have an override on output, prioritize that
# if we do not, prioritize writing credentials to file if that is in our
# configuration. If we are not writing to a credentials file, use whatever
Expand Down Expand Up @@ -841,7 +858,6 @@ def write_result_action(self, action, data):
self.ui.result("export AWS_SECURITY_TOKEN=" +
data['credentials']['aws_security_token'])


def handle_action_configure(self):
# Create/Update config when configure arg set
if not self.config.action_configure:
Expand Down Expand Up @@ -879,7 +895,7 @@ def handle_action_register_device(self):
self.ui.notify('*** You may be prompted for MFA more than once for this run.\n')

auth_result = self.auth_session
base_config = self.config.get_config_dict(include_inherits = False)
base_config = self.config.get_config_dict(include_inherits=False)
base_config['device_token'] = auth_result['device_token']
self.config.write_config_file(base_config)
self.okta.device_token = base_config['device_token']
Expand All @@ -905,7 +921,10 @@ def handle_setup_fido_authenticator(self):

self.okta.set_preferred_mfa_type(None)
credential_id, user = self.okta.setup_fido_authenticator()
alias = self.ui.input('Alias for webauthn token: ')
if alias == "":
alias = None

registered_authenticators = RegisteredAuthenticators(self.ui)
registered_authenticators.add_authenticator(credential_id, user)
registered_authenticators.add_authenticator(credential_id, user, alias)
raise errors.GimmeAWSCredsExitSuccess()
Loading