Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.

Conversation

@smithsz
Copy link
Contributor

@smithsz smithsz commented Jun 28, 2017

What

Add IAM authentication support.

How

Added a class method and context manager to simplify IAM client construction:

from cloudant.client import Cloudant
client = Cloudant.iam('account_name', 'api_key_here', connect=True)
print client.all_dbs()

# or using context manager...
from cloudant import cloudant_iam
with cloudant_iam('account_name', 'api_key_here') as client:
    print client.all_dbs()

Under the hood there's a new use_iam=True toggle. Once enabled, the password parameter is used as the IAM API key.

IAM Authentication Workflow:

  1. Exchange IAM API key for access token via IAM_TOKEN_URL.
  2. Generate a cookie from the access token using the POST /_iam_session Cloudant endpoint.
  3. Use the IAMSession cookie in all library requests.
  4. Attempt to renew if the IAMSession cookie has expired or on any 401 response.

Testing

Includes additional mock tests.

Issues

Fixes #303.

Provides a context manager to create a Cloudant session and provide access
to databases, docs etc.
:param api_key: IAM authentication API key.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add an example similar to what we have below in cloudant_bluemix?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, will do. I'll improve the getting started stuff too once I find the proper IAM blumix docs link.

@ricellis ricellis self-requested a review July 5, 2017 14:09
Copy link
Member

@ricellis ricellis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few thoughts.


@mock.patch('cloudant._common_util.IAMSession.login')
@mock.patch('cloudant._common_util.ClientSession.request')
def test_iam_renew_cookie_on_403(self, m_req, m_login):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is invalid. A 403 should not trigger a renew.

return resp

is_expired = any((
resp.status_code == 403 and
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_iam_session shouldn't have this same 403 quirk as Cloudant's _session.


class InfiniteSession(Session):

class ClientSession(Session):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The majority of the work that CookieSession and IAMSession have to do is the same, other than the extra piece to get an IAM token (and the content-type of the POST to *_session which should be application/json for IAM).
It would be nice (but by no means essential if it is harder than it initially looks) if we could use the same login/logout/expired checks in this base class and just customize them slightly (e.g. with the url/data/response conditions).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a little tricky as ClientSession is used directly when admin party is enabled.
I still think it's worth doing. I've raise issue #310 to track the work.

client = Cloudant(USERNAME, API_KEY, account=ACCOUNT_NAME,
auto_renew=True,
connect=True,
use_iam=True)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan of exposing yet another parameter on here, I think it is particularly confusing that the USERNAME is effectively ignored in this scenario. I wonder whether we can have another way of initializing that effectively hides this complexity.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added new class method in 9f181e7.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we safely omit this property from the docs then?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, forgot to update docs. Fixed in cd1c664.

@mock.patch('cloudant._common_util.ClientSession.request')
def test_iam_get_access_token(self, m_req):
m_response = mock.MagicMock()
m_response.json.return_value = {'access_token': 'bar'}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mock response should probably have more of the expected token content in it.

@smithsz smithsz force-pushed the 303-support-iam-auth branch from dc88595 to f0cc64e Compare July 23, 2017 09:44
client = Cloudant(USERNAME, API_KEY, account=ACCOUNT_NAME,
auto_renew=True,
connect=True,
use_iam=True)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we safely omit this property from the docs then?

`Requests library timeout argument
<http://docs.python-requests.org/en/master/user/quickstart/#timeouts>`_.
but will apply to every request made using this client.
:param bool use_iam: Keyword argument, if set to True performs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should add a line here expressing that the preferred approach is to use the iam function.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 9d0a5f3.


self.assertEqual(info, m_info)
self.assertTrue(m_response.raise_for_status.called)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like there is a golden path test missing here, checking that a request is as expected without any expiry/renewal conditions.

I also think it would be valuable to have a test at the client level verifying that using the class method creates the IAM session and appropriate golden path is followed making a normal request (token, iam_session, normal request).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 85932a9 & fc8d64a.

@smithsz smithsz force-pushed the 303-support-iam-auth branch from 964d62e to 92be4f4 Compare July 27, 2017 07:55
self._auto_renew = kwargs.get('auto_renew', False)
self._session_url = url_join(server_url, '_iam_session')
self._token_url = os.environ.get(
'IAM_TOKEN_URL', 'https://iam.bluemix.net/oidc/token')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be made a kwarg rather than an env var? Why is it an env var?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our view was that it should basically never be necessary to change the token URL, but we thought there was value in providing an "override" primarily for the event that for some reason the expected URL was unavailable (although it also has value in testing).

I think an env var is more suitable for this purpose in that it doesn't require a code change if, for example, one needed to switch token server to a backup to temporarily workaround some kind of outage. That was the thinking anyway.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay

.. code-block:: python
# Authenticate using an IAM API key
client = Cloudant.iam(ACCOUNT_NAME, API_KEY, connect=True)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to mention here the environment variable that allows for different IAM servers? (Note I query below whether it should be an env var rather than a kwarg).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 3b4edd9.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

method, url, timeout=self._timeout, **kwargs)
self.cookies.clear_expired_cookies()
if self._auto_renew and 'IAMSession' not in self.cookies.keys():
self.login()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather this be more explicitly about the expiry of the IAMSession cookie, it took me a bit to understand that's what was actually happening. Like:

if self._auto_renew and 'IAMSession' not in self.cookies.keys() and not self.cookies['IAMSession'].is_expired():
    self.login()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't get a cookie object from the jar without iterating over the entire jar. self.cookies['IAMSession'] just gives your the cookie value as a string.
We'd need something horrible like:

self.cookies._cookies['mikerhodes-iam-testing.cloudant.com']['/']['IAMSession'].is_expired()

I think sticking to self.cookies.clear_expired_cookies() is the cleanest solution.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess add a comment to the effect of this being the simplest way to check whether we have an expired cookie so the next person doesn't have to figure it out by guesswork. A decent "why I'm doing it" comment rather than a "describe what the code does" comment.

Copy link
Member

@mikerhodes mikerhodes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved, though I'd like to see the comment around the mess of an API the cookie jar has.

@ricellis ricellis modified the milestone: 2.next Aug 8, 2017
@smithsz smithsz force-pushed the 303-support-iam-auth branch from 2ca785b to fcbe630 Compare August 29, 2017 11:48
@smithsz smithsz force-pushed the 303-support-iam-auth branch from 1d25ed5 to 2dd2b52 Compare August 29, 2017 16:14
@smithsz smithsz force-pushed the 303-support-iam-auth branch from 2dd2b52 to 1a905fb Compare August 29, 2017 16:40
@smithsz smithsz merged commit 73cb8b1 into master Sep 5, 2017
@smithsz smithsz deleted the 303-support-iam-auth branch September 5, 2017 10:14
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants