-
Notifications
You must be signed in to change notification settings - Fork 55
303 support iam auth #306
303 support iam auth #306
Conversation
src/cloudant/__init__.py
Outdated
| Provides a context manager to create a Cloudant session and provide access | ||
| to databases, docs etc. | ||
| :param api_key: IAM authentication API key. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few thoughts.
tests/unit/iam_auth_tests.py
Outdated
|
|
||
| @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): |
There was a problem hiding this comment.
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.
src/cloudant/_common_util.py
Outdated
| return resp | ||
|
|
||
| is_expired = any(( | ||
| resp.status_code == 403 and |
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
docs/getting_started.rst
Outdated
| client = Cloudant(USERNAME, API_KEY, account=ACCOUNT_NAME, | ||
| auto_renew=True, | ||
| connect=True, | ||
| use_iam=True) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
tests/unit/iam_auth_tests.py
Outdated
| @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'} |
There was a problem hiding this comment.
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.
dc88595 to
f0cc64e
Compare
docs/getting_started.rst
Outdated
| client = Cloudant(USERNAME, API_KEY, account=ACCOUNT_NAME, | ||
| auto_renew=True, | ||
| connect=True, | ||
| use_iam=True) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) | ||
|
|
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
964d62e to
92be4f4
Compare
| 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') |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added in 3b4edd9.
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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()There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
mikerhodes
left a comment
There was a problem hiding this 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.
2ca785b to
fcbe630
Compare
These checks are redundant. Session endpoint requests are always made using the base request method.
1d25ed5 to
2dd2b52
Compare
2dd2b52 to
1a905fb
Compare
What
Add IAM authentication support.
How
Added a class method and context manager to simplify IAM client construction:
Under the hood there's a new
use_iam=Truetoggle. Once enabled, thepasswordparameter is used as the IAM API key.IAM Authentication Workflow:
IAM_TOKEN_URL.POST /_iam_sessionCloudant endpoint.401response.Testing
Includes additional mock tests.
Issues
Fixes #303.