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

Commit c828b85

Browse files
committed
Add IAM authentication tests
1 parent cdba134 commit c828b85

File tree

2 files changed

+336
-0
lines changed

2 files changed

+336
-0
lines changed

src/cloudant/_2to3.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
from urlparse import urlparse as url_parse
4242
from urlparse import urljoin as url_join
4343
from ConfigParser import RawConfigParser
44+
from cookielib import Cookie
4445

4546
def iteritems_(adict):
4647
"""
@@ -65,6 +66,7 @@ def next_(itr):
6566
from urllib.parse import quote as url_quote # pylint: disable=wrong-import-position,no-name-in-module,import-error,ungrouped-imports
6667
from urllib.parse import quote_plus as url_quote_plus # pylint: disable=wrong-import-position,no-name-in-module,import-error,ungrouped-imports
6768
from configparser import RawConfigParser # pylint: disable=wrong-import-position,no-name-in-module,import-error
69+
from http.cookiejar import Cookie # pylint: disable=wrong-import-position,no-name-in-module,import-error
6870

6971
def iteritems_(adict):
7072
"""

tests/unit/iam_auth_tests.py

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
#!/usr/bin/env python
2+
# Copyright (c) 2017 IBM. All rights reserved.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
""" Unit tests for IAM authentication. """
16+
import time
17+
import unittest
18+
import json
19+
import mock
20+
21+
from cloudant._2to3 import Cookie
22+
from cloudant._common_util import IAMSession
23+
from cloudant.client import Cloudant
24+
25+
MOCK_API_KEY = 'CqbrIYzdO3btWV-5t4teJLY_etfT_dkccq-vO-5vCXSo'
26+
27+
MOCK_ACCESS_TOKEN = ('eyJraWQiOiIyMDE3MDQwMi0wMDowMDowMCIsImFsZyI6IlJTMjU2In0.e'
28+
'yJpYW1faWQiOiJJQk1pZC0yNzAwMDdHRjBEIiwiaWQiOiJJQk1pZC0yNz'
29+
'AwMDdHRjBEIiwicmVhbG1pZCI6IklCTWlkIiwiaWRlbnRpZmllciI6IjI'
30+
'3MDAwN0dGMEQiLCJnaXZlbl9uYW1lIjoiVG9tIiwiZmFtaWx5X25hbWUi'
31+
'OiJCbGVuY2giLCJuYW1lIjoiVG9tIEJsZW5jaCIsImVtYWlsIjoidGJsZ'
32+
'W5jaEB1ay5pYm0uY29tIiwic3ViIjoidGJsZW5jaEB1ay5pYm0uY29tIi'
33+
'wiYWNjb3VudCI6eyJic3MiOiI1ZTM1ZTZhMjlmYjJlZWNhNDAwYWU0YzN'
34+
'lMWZhY2Y2MSJ9LCJpYXQiOjE1MDA0NjcxMDIsImV4cCI6MTUwMDQ3MDcw'
35+
'MiwiaXNzIjoiaHR0cHM6Ly9pYW0ubmcuYmx1ZW1peC5uZXQvb2lkYy90b'
36+
'2tlbiIsImdyYW50X3R5cGUiOiJ1cm46aWJtOnBhcmFtczpvYXV0aDpncm'
37+
'FudC10eXBlOmFwaWtleSIsInNjb3BlIjoib3BlbmlkIiwiY2xpZW50X2l'
38+
'kIjoiZGVmYXVsdCJ9.XAPdb5K4n2nYih-JWTWBGoKkxTXM31c1BB1g-Ci'
39+
'auc2LxuoNXVTyz_mNqf1zQL07FUde1Cb_dwrbotjickNcxVPost6byQzt'
40+
'fc0mRF1x2S6VR8tn7SGiRmXBjLofkTh1JQq-jutp2MS315XbTG6K6m16u'
41+
'YzL9qfMnRvQHxsZWErzfPiJx-Trg_j7OX-qNFjdNUGnRpU7FmULy0r7Rx'
42+
'Ld8mhG-M1yxVzRBAZzvM63s0XXfMnk1oLi-BuUUTqVOdrM0KyYMWfD0Q7'
43+
'2PTo4Exa17V-R_73Nq8VPCwpOvZcwKRA2sPTVgTMzU34max8b5kpTzVGJ'
44+
'6SXSItTVOUdAygZBng')
45+
46+
MOCK_OIDC_TOKEN_RESPONSE = {
47+
'access_token': MOCK_ACCESS_TOKEN,
48+
'refresh_token': ('MO61FKNvVRWkSa4vmBZqYv_Jt1kkGMUc-XzTcNnR-GnIhVKXHUWxJVV3'
49+
'RddE8Kqh3X_TZRmyK8UySIWKxoJ2t6obUSUalPm90SBpTdoXtaljpNyo'
50+
'rmqCCYPROnk6JBym72ikSJqKHHEZVQkT0B5ggZCwPMnKagFj0ufs-VIh'
51+
'CF97xhDxDKcIPMWG02xxPuESaSTJJug7e_dUDoak_ZXm9xxBmOTRKwOx'
52+
'n5sTKthNyvVpEYPE7jIHeiRdVDOWhN5LomgCn3TqFCLpMErnqwgNYbyC'
53+
'Bd9rNm-alYKDb6Jle4njuIBpXxQPb4euDwLd1osApaSME3nEarFWqRBz'
54+
'hjoqCe1Kv564s_rY7qzD1nHGvKOdpSa0ZkMcfJ0LbXSQPs7gBTSVrBFZ'
55+
'qwlg-2F-U3Cto62-9qRR_cEu_K9ZyVwL4jWgOlngKmxV6Ku4L5mHp4Kg'
56+
'EJSnY_78_V2nm64E--i2ZA1FhiKwIVHDOivVNhggE9oabxg54vd63glp'
57+
'4GfpNnmZsMOUYG9blJJpH4fDX4Ifjbw-iNBD7S2LRpP8b8vG9pb4WioG'
58+
'zN43lE5CysveKYWrQEZpThznxXlw1snDu_A48JiL3Lrvo1LobLhF3zFV'
59+
'-kQ='),
60+
'token_type': 'Bearer',
61+
'expires_in': 3600, # 60mins
62+
'expiration': 1500470702 # Wed Jul 19 14:25:02 2017
63+
}
64+
65+
66+
class IAMAuthTests(unittest.TestCase):
67+
""" Unit tests for IAM authentication. """
68+
69+
@staticmethod
70+
def _mock_cookie(expires_secs=300):
71+
return Cookie(
72+
version=0,
73+
name='IAMSession',
74+
value=('SQJCaUQxMqEfMEAyRKU6UopLVXceS0c9RPuQgDArCEYoN3l_TEY4gdf-DJ7'
75+
'4sHfjcNEUVjfdOvA'),
76+
port=None,
77+
port_specified=False,
78+
domain='localhost',
79+
domain_specified=False,
80+
domain_initial_dot=False,
81+
path="/",
82+
path_specified=True,
83+
secure=True,
84+
expires=int(time.time() + expires_secs),
85+
discard=False,
86+
comment=None,
87+
comment_url=None,
88+
rest={'HttpOnly': None},
89+
rfc2109=True)
90+
91+
@mock.patch('cloudant._common_util.ClientSession.request')
92+
def test_iam_get_access_token(self, m_req):
93+
m_response = mock.MagicMock()
94+
m_response.json.return_value = MOCK_OIDC_TOKEN_RESPONSE
95+
m_req.return_value = m_response
96+
97+
iam = IAMSession(MOCK_API_KEY, 'http://127.0.0.1:5984')
98+
access_token = iam._get_access_token()
99+
100+
m_req.assert_called_once_with(
101+
'POST',
102+
iam._token_url,
103+
auth=('bx', 'bx'),
104+
headers={'Accepts': 'application/json'},
105+
data={
106+
'grant_type': 'urn:ibm:params:oauth:grant-type:apikey',
107+
'response_type': 'cloud_iam',
108+
'apikey': MOCK_API_KEY
109+
}
110+
)
111+
112+
self.assertEqual(access_token, MOCK_ACCESS_TOKEN)
113+
self.assertTrue(m_response.raise_for_status.called)
114+
self.assertTrue(m_response.json.called)
115+
116+
@mock.patch('cloudant._common_util.ClientSession.request')
117+
@mock.patch('cloudant._common_util.IAMSession._get_access_token')
118+
def test_iam_login(self, m_token, m_req):
119+
m_token.return_value = MOCK_ACCESS_TOKEN
120+
m_response = mock.MagicMock()
121+
m_req.return_value = m_response
122+
123+
iam = IAMSession(MOCK_API_KEY, 'http://127.0.0.1:5984')
124+
iam.login()
125+
126+
m_req.assert_called_once_with(
127+
'POST',
128+
iam._session_url,
129+
headers={'Content-Type': 'application/json'},
130+
data=json.dumps({'access_token': MOCK_ACCESS_TOKEN})
131+
)
132+
133+
self.assertEqual(m_token.call_count, 1)
134+
self.assertTrue(m_response.raise_for_status.called)
135+
136+
def test_iam_logout(self):
137+
iam = IAMSession(MOCK_API_KEY, 'http://127.0.0.1:5984')
138+
# add a valid cookie to jar
139+
iam.cookies.set_cookie(self._mock_cookie())
140+
self.assertEqual(len(iam.cookies.keys()), 1)
141+
iam.logout()
142+
self.assertEqual(len(iam.cookies.keys()), 0)
143+
144+
@mock.patch('cloudant._common_util.ClientSession.get')
145+
def test_iam_get_session_info(self, m_get):
146+
m_info = {'ok': True, 'info': {'authentication_db': '_users'}}
147+
148+
m_response = mock.MagicMock()
149+
m_response.json.return_value = m_info
150+
m_get.return_value = m_response
151+
152+
iam = IAMSession(MOCK_API_KEY, 'http://127.0.0.1:5984')
153+
info = iam.info()
154+
155+
m_get.assert_called_once_with(iam._session_url)
156+
157+
self.assertEqual(info, m_info)
158+
self.assertTrue(m_response.raise_for_status.called)
159+
160+
@mock.patch('cloudant._common_util.IAMSession.login')
161+
@mock.patch('cloudant._common_util.ClientSession.request')
162+
def test_iam_first_request(self, m_req, m_login):
163+
# mock 200
164+
m_response_ok = mock.MagicMock()
165+
type(m_response_ok).status_code = mock.PropertyMock(return_value=200)
166+
m_response_ok.json.return_value = {'ok': True}
167+
168+
m_req.return_value = m_response_ok
169+
170+
iam = IAMSession(MOCK_API_KEY, 'http://127.0.0.1:5984', auto_renew=True)
171+
iam.login()
172+
173+
self.assertEqual(m_login.call_count, 1)
174+
self.assertEqual(m_req.call_count, 0)
175+
176+
# add a valid cookie to jar
177+
iam.cookies.set_cookie(self._mock_cookie())
178+
179+
resp = iam.request('GET', 'http://127.0.0.1:5984/mydb1')
180+
181+
self.assertEqual(m_login.call_count, 1)
182+
self.assertEqual(m_req.call_count, 1)
183+
self.assertEqual(resp.status_code, 200)
184+
185+
@mock.patch('cloudant._common_util.IAMSession.login')
186+
@mock.patch('cloudant._common_util.ClientSession.request')
187+
def test_iam_renew_cookie_on_expiry(self, m_req, m_login):
188+
# mock 200
189+
m_response_ok = mock.MagicMock()
190+
type(m_response_ok).status_code = mock.PropertyMock(return_value=200)
191+
m_response_ok.json.return_value = {'ok': True}
192+
193+
m_req.return_value = m_response_ok
194+
195+
iam = IAMSession(MOCK_API_KEY, 'http://127.0.0.1:5984', auto_renew=True)
196+
iam.login()
197+
198+
# add an expired cookie to jar
199+
iam.cookies.set_cookie(self._mock_cookie(expires_secs=-300))
200+
201+
resp = iam.request('GET', 'http://127.0.0.1:5984/mydb1')
202+
203+
self.assertEqual(m_login.call_count, 2)
204+
self.assertEqual(m_req.call_count, 1)
205+
self.assertEqual(resp.status_code, 200)
206+
207+
@mock.patch('cloudant._common_util.IAMSession.login')
208+
@mock.patch('cloudant._common_util.ClientSession.request')
209+
def test_iam_renew_cookie_on_401_success(self, m_req, m_login):
210+
# mock 200
211+
m_response_ok = mock.MagicMock()
212+
type(m_response_ok).status_code = mock.PropertyMock(return_value=200)
213+
m_response_ok.json.return_value = {'ok': True}
214+
# mock 401
215+
m_response_bad = mock.MagicMock()
216+
type(m_response_bad).status_code = mock.PropertyMock(return_value=401)
217+
218+
m_req.side_effect = [m_response_bad, m_response_ok, m_response_ok]
219+
220+
iam = IAMSession(MOCK_API_KEY, 'http://127.0.0.1:5984', auto_renew=True)
221+
iam.login()
222+
self.assertEqual(m_login.call_count, 1)
223+
224+
# add a valid cookie to jar
225+
iam.cookies.set_cookie(self._mock_cookie())
226+
227+
resp = iam.request('GET', 'http://127.0.0.1:5984/mydb1')
228+
self.assertEqual(resp.status_code, 200)
229+
self.assertEqual(m_login.call_count, 2)
230+
self.assertEqual(m_req.call_count, 2)
231+
232+
resp = iam.request('GET', 'http://127.0.0.1:5984/mydb1')
233+
self.assertEqual(resp.status_code, 200)
234+
self.assertEqual(m_login.call_count, 2)
235+
self.assertEqual(m_req.call_count, 3)
236+
237+
238+
@mock.patch('cloudant._common_util.IAMSession.login')
239+
@mock.patch('cloudant._common_util.ClientSession.request')
240+
def test_iam_renew_cookie_on_403(self, m_req, m_login):
241+
# mock 200
242+
m_response_ok = mock.MagicMock()
243+
type(m_response_ok).status_code = mock.PropertyMock(return_value=200)
244+
m_response_ok.json.return_value = {'ok': True}
245+
# mock 403
246+
m_response_bad = mock.MagicMock()
247+
type(m_response_bad).status_code = mock.PropertyMock(return_value=403)
248+
m_response_bad.json.return_value = {'error': 'credentials_expired'}
249+
250+
m_req.side_effect = [m_response_bad, m_response_ok]
251+
252+
iam = IAMSession('foo', 'http://127.0.0.1:5984', auto_renew=True)
253+
iam.login()
254+
255+
resp = iam.request('GET', 'http://127.0.0.1:5984/mydb1')
256+
257+
self.assertEqual(m_login.call_count, 2)
258+
self.assertTrue(resp.json()['ok'])
259+
260+
@mock.patch('cloudant._common_util.IAMSession.login')
261+
@mock.patch('cloudant._common_util.ClientSession.request')
262+
def test_iam_renew_cookie_on_401_failure(self, m_req, m_login):
263+
# mock 401
264+
m_response_bad = mock.MagicMock()
265+
type(m_response_bad).status_code = mock.PropertyMock(return_value=401)
266+
267+
m_req.return_value = m_response_bad
268+
269+
iam = IAMSession(MOCK_API_KEY, 'http://127.0.0.1:5984', auto_renew=True)
270+
iam.login()
271+
self.assertEqual(m_login.call_count, 1)
272+
273+
# add a valid cookie to jar
274+
iam.cookies.set_cookie(self._mock_cookie())
275+
276+
resp = iam.request('GET', 'http://127.0.0.1:5984/mydb1')
277+
self.assertEqual(resp.status_code, 401)
278+
self.assertEqual(m_login.call_count, 2)
279+
self.assertEqual(m_req.call_count, 2)
280+
281+
resp = iam.request('GET', 'http://127.0.0.1:5984/mydb1')
282+
self.assertEqual(resp.status_code, 401)
283+
self.assertEqual(m_login.call_count, 3)
284+
self.assertEqual(m_req.call_count, 4)
285+
286+
@mock.patch('cloudant._common_util.IAMSession.login')
287+
@mock.patch('cloudant._common_util.ClientSession.request')
288+
def test_iam_renew_cookie_disabled(self, m_req, m_login):
289+
# mock 401
290+
m_response_bad = mock.MagicMock()
291+
type(m_response_bad).status_code = mock.PropertyMock(return_value=401)
292+
293+
m_req.return_value = m_response_bad
294+
295+
iam = IAMSession(MOCK_API_KEY, 'http://127.0.0.1:5984', auto_renew=False)
296+
iam.login()
297+
self.assertEqual(m_login.call_count, 1)
298+
299+
resp = iam.request('GET', 'http://127.0.0.1:5984/mydb1')
300+
self.assertEqual(resp.status_code, 401)
301+
self.assertEqual(m_login.call_count, 1) # no attempt to renew
302+
self.assertEqual(m_req.call_count, 1)
303+
304+
resp = iam.request('GET', 'http://127.0.0.1:5984/mydb1')
305+
self.assertEqual(resp.status_code, 401)
306+
self.assertEqual(m_login.call_count, 1) # no attempt to renew
307+
self.assertEqual(m_req.call_count, 2)
308+
309+
@mock.patch('cloudant._common_util.IAMSession.login')
310+
@mock.patch('cloudant._common_util.ClientSession.request')
311+
def test_iam_client_create(self, m_req, m_login):
312+
# mock 200
313+
m_response_ok = mock.MagicMock()
314+
type(m_response_ok).status_code = mock.PropertyMock(return_value=200)
315+
m_response_ok.json.return_value = ['animaldb']
316+
317+
m_req.return_value = m_response_ok
318+
319+
# create IAM client
320+
client = Cloudant.iam('foo', MOCK_API_KEY)
321+
client.connect()
322+
323+
# add a valid cookie to jar
324+
client.r_session.cookies.set_cookie(self._mock_cookie())
325+
326+
dbs = client.all_dbs()
327+
328+
self.assertEqual(m_login.call_count, 1)
329+
self.assertEqual(m_req.call_count, 1)
330+
self.assertEqual(dbs, ['animaldb'])
331+
332+
333+
if __name__ == '__main__':
334+
unittest.main()

0 commit comments

Comments
 (0)