Skip to content
This repository was archived by the owner on Jul 1, 2024. It is now read-only.
Open
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
3 changes: 2 additions & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
include CONTRIBUTING.rst
include CHANGELOG.rst
include LICENSE
include README.rst
include PLUGINS.rst
include README.rst
50 changes: 50 additions & 0 deletions PLUGINS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
========================================
AWS Process Credential Providers Plug-in
========================================

.. image:: https://travis-ci.org/awslabs/awsprocesscreds.svg?branch=master
:target: https://travis-ci.org/awslabs/awsprocesscreds


This document covers what is is to be a SAML provider plug-in.

Generally, a plug-in refers to any class registered to the entry point group
'saml_form_authenticators' and which also conforms to the SAMLAuthenticator
interface. See Requirements for more constraints.

Example:

entry_points={
'saml_form_authenticators': [
'example = plugin.example:ExampleFormsBasedAuthenticator',
}

For reference, the file setup.py in this project registers both default
providers as plug-ins.


General Plug-in Overview
------------------------

At runtime, all registered plug-in names retrieved using pkg_resources will be
matched against the user supplied value for -p (--provider). An exact match
will instatiate that class, no match will throw an unspupported error.

Inheritance from SAMLAuthenticator is not required.


Requirements
------------

Generally this assume an installed module.

* Plug-in has an entry point registered under group 'saml_form_authenticators'
* Class implements the awsprocesscreds.saml:SAMLAuthenticator specification


Futher Information
------------------

Both of the SAML authenticators shipped with the product utilize the plug-in
loading process. If you are looking at how to implement one to support your
own business requirements then it is suggested to review both those classes.
13 changes: 11 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ arguments:

* ``-e / --endpoint`` - Your SAML idp endpoint.
* ``-u / --username`` - Your SAML username.
* ``-p / --provider`` - The name of your SAML provider. Currently okta and
adfs are supported.
* ``-p / --provider`` - The name of your SAML provider plug-in. Default
support includes 'okta' and 'adfs', both form-based auth.
* ``-a / --role-arn``- The role arn you wish to assume. Your SAML provider
must be configured to give you access to this arn.

Expand Down Expand Up @@ -73,6 +73,15 @@ Example adfs configuration::
.. _AWS CLI Config docs: http://docs.aws.amazon.com/cli/latest/topic/config-vars.html#cli-aws-help-config-vars


Plug-in Support
---------------

In cases where you have your own SAML requirements, there is a provided plug-in
architecture to supplement the the default okta and adfs implementations.

See the PLUGINS documentation in the root of this project.


Custom Providers
----------------

Expand Down
7 changes: 4 additions & 3 deletions awsprocesscreds/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ def saml(argv=None, prompter=getpass.getpass, client_creator=None,
help='Your SAML username.'
)
parser.add_argument(
'-p', '--provider', required=True, choices=['okta', 'adfs'],
'-p', '--provider', required=True,
help=(
'The name of your SAML provider. Currently okta and adfs '
'form-based auth is supported.'
'The name of your SAML provider plug-in. Default '
'support includes \'okta\' and \'adfs\', both '
'form-based auth. '
)
)
parser.add_argument(
Expand Down
18 changes: 11 additions & 7 deletions awsprocesscreds/saml.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import six
import requests
import pkg_resources
import botocore
from botocore.client import Config
from botocore.compat import urlsplit
Expand Down Expand Up @@ -306,12 +307,6 @@ def error(self, message):


class SAMLCredentialFetcher(CachedCredentialFetcher):
SAML_FORM_AUTHENTICATORS = {
'okta': OktaAuthenticator,
'adfs': ADFSFormsBasedAuthenticator

}

def __init__(self, client_creator, provider_name, saml_config,
role_selector=_role_selector,
password_prompter=getpass.getpass, cache=None,
Expand All @@ -321,7 +316,8 @@ def __init__(self, client_creator, provider_name, saml_config,
self._role_selector = role_selector
self._config = saml_config
self._provider_name = provider_name
authenticator_cls = self.SAML_FORM_AUTHENTICATORS.get(provider_name)
authenticators = self.get_form_authenticators()
authenticator_cls = authenticators.get(provider_name)
if authenticator_cls is None:
raise ValueError('Unsupported SAML provider: %s' % provider_name)
self._authenticator = authenticator_cls(password_prompter)
Expand All @@ -334,6 +330,14 @@ def __init__(self, client_creator, provider_name, saml_config,
self._stored_cache_key = None
self._expiry_window_seconds = expiry_window_seconds

def get_form_authenticators(self):
authenticators = {}
for entry_point in pkg_resources.iter_entry_points(
'saml_form_authenticators'):
authenticators[
entry_point.name] = entry_point.load()
return authenticators

@property
def _cache_key(self):
if self._stored_cache_key is None:
Expand Down
6 changes: 4 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@ def find_version(*file_paths):
keywords='aws credentials',
entry_points={
'console_scripts': [
'awsprocesscreds-saml = awsprocesscreds.cli:saml'
]
'awsprocesscreds-saml = awsprocesscreds.cli:saml'],
'saml_form_authenticators': [
'adfs = awsprocesscreds.saml:ADFSFormsBasedAuthenticator',
'okta = awsprocesscreds.saml:OktaAuthenticator']
},
classifiers=(
'Development Status :: 2 - Pre-Alpha',
Expand Down
43 changes: 34 additions & 9 deletions tests/functional/test_saml.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
import logging
import xml.dom.minidom
import base64

import pkg_resources
import requests
import pytest

from tests import create_assertion
from awsprocesscreds.cli import saml, PrettyPrinterLogHandler
from awsprocesscreds.saml import SAMLCredentialFetcher
from awsprocesscreds.saml import OktaAuthenticator, ADFSFormsBasedAuthenticator


@pytest.fixture
Expand All @@ -22,8 +23,27 @@ def argv():
]


@pytest.fixture
def mock_pkg_resources(monkeypatch):
class EntryPoint:
def __init__(self, name, load):
self.name = name
self._load = load

def load(self):
return self._load

def stub_iter(*args, **kwargs):
return [
EntryPoint('okta', OktaAuthenticator),
EntryPoint('adfs', ADFSFormsBasedAuthenticator),
]

monkeypatch.setattr(pkg_resources, "iter_entry_points", stub_iter)


def test_cli(mock_requests_session, argv, prompter, assertion, client_creator,
capsys, cache_dir):
capsys, cache_dir, mock_pkg_resources):
session_token = {'sessionToken': 'spam'}
token_response = mock.Mock(
spec=requests.Response, status_code=200, text=json.dumps(session_token)
Expand Down Expand Up @@ -54,7 +74,7 @@ def test_cli(mock_requests_session, argv, prompter, assertion, client_creator,


def test_no_cache(mock_requests_session, argv, prompter, assertion,
client_creator, capsys, cache_dir):
client_creator, capsys, cache_dir, mock_pkg_resources):
session_token = {'sessionToken': 'spam'}
token_response = mock.Mock(
spec=requests.Response, status_code=200, text=json.dumps(session_token)
Expand Down Expand Up @@ -91,7 +111,7 @@ def test_no_cache(mock_requests_session, argv, prompter, assertion,


def test_verbose(mock_requests_session, argv, prompter, assertion,
client_creator, cache_dir):
client_creator, cache_dir, mock_pkg_resources):
session_token = {'sessionToken': 'spam'}
token_response = mock.Mock(
spec=requests.Response, status_code=200, text=json.dumps(session_token)
Expand Down Expand Up @@ -122,7 +142,8 @@ def test_verbose(mock_requests_session, argv, prompter, assertion,


def test_log_handler_parses_assertion(mock_requests_session, argv, prompter,
client_creator, cache_dir, caplog):
client_creator, cache_dir, caplog,
mock_pkg_resources):
session_token = {'sessionToken': 'spam'}
token_response = mock.Mock(
spec=requests.Response, status_code=200, text=json.dumps(session_token)
Expand Down Expand Up @@ -159,7 +180,8 @@ def test_log_handler_parses_assertion(mock_requests_session, argv, prompter,


def test_log_handler_parses_dict(mock_requests_session, argv, prompter,
client_creator, cache_dir, caplog):
client_creator, cache_dir, caplog,
mock_pkg_resources):
session_token = {'sessionToken': 'spam'}
token_response = mock.Mock(
spec=requests.Response, status_code=200, text=json.dumps(session_token)
Expand Down Expand Up @@ -202,7 +224,8 @@ def test_log_handler_parses_dict(mock_requests_session, argv, prompter,
assert expected_log in caplog.record_tuples


def test_unsupported_saml_auth_type(client_creator, prompter):
def test_unsupported_saml_auth_type(client_creator, prompter,
mock_pkg_resources):
invalid_config = {
'saml_authentication_type': 'unsupported',
'saml_provider': 'okta',
Expand Down Expand Up @@ -235,8 +258,9 @@ def test_unsupported_saml_provider(client_creator, prompter):
)


def test_prompter_only_called_once(client_creator, prompter, assertion,
mock_requests_session):
def test_prompter_only_called_once(client_creator, prompter,
assertion, mock_requests_session,
mock_pkg_resources):
session_token = {'sessionToken': 'spam'}
token_response = mock.Mock(
spec=requests.Response, status_code=200, text=json.dumps(session_token)
Expand All @@ -257,6 +281,7 @@ def test_prompter_only_called_once(client_creator, prompter, assertion,
'saml_username': 'monty',
'role_arn': 'arn:aws:iam::123456789012:role/monty'
}

fetcher = SAMLCredentialFetcher(
client_creator=client_creator,
saml_config=config,
Expand Down
26 changes: 18 additions & 8 deletions tests/unit/test_saml.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from dateutil.tz import tzlocal
import pytest
import requests
import pkg_resources

from awsprocesscreds.saml import ADFSFormsBasedAuthenticator
from awsprocesscreds.saml import FormParser
Expand All @@ -23,6 +24,20 @@ def mock_requests_session():
return mock.Mock(spec=requests.Session)


@pytest.fixture
def mock_fake_pkg_resources(monkeypatch, mock_authenticator):
provider_name = 'myprovider'
authenticator_cls = mock.Mock(return_value=mock_authenticator)

def stub_iter(*args, **kwargs):
return [type('', (object,),
{'name': provider_name,
'load': lambda self: authenticator_cls})()]

monkeypatch.setattr(pkg_resources, "iter_entry_points",
stub_iter)


@pytest.fixture
def generic_auth(prompter, mock_requests_session):
return GenericFormsBasedAuthenticator(prompter, mock_requests_session)
Expand Down Expand Up @@ -89,16 +104,11 @@ def cache():

@pytest.fixture
def fetcher(generic_config, client_creator, prompter, mock_authenticator,
cache):
provider_name = 'myprovider'
authenticator_cls = mock.Mock(return_value=mock_authenticator)
cache, mock_fake_pkg_resources):

class MockSAMLFetcher(SAMLCredentialFetcher):
SAML_FORM_AUTHENTICATORS = {
provider_name: authenticator_cls
}
provider_name = 'myprovider'

saml_fetcher = MockSAMLFetcher(
saml_fetcher = SAMLCredentialFetcher(
client_creator=client_creator,
provider_name=provider_name,
saml_config=generic_config,
Expand Down