Skip to content

Commit b792fa7

Browse files
Merge pull request #77 from CartoDB/75-Auth_API_based_client
AuthAPIClient class
2 parents cebfc3a + 4ec745d commit b792fa7

File tree

6 files changed

+152
-39
lines changed

6 files changed

+152
-39
lines changed

NEWS

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
Feb-21-2018: version 1.2.1
2+
- Added AuthAPIClient class for future Auth API usage and API key validation (#69)
3+
14
Feb-21-2018: version 1.2.0
2-
- Added PROTOCOL and HOST to test configuration allowing local testing.
5+
- Added PROTOCOL and HOST to test configuration allowing local testing
36

47
Dec-27-2017: version 1.1.2
58
- Fix on examples management of environment variables (#70)

carto/auth.py

Lines changed: 88 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
88
.. moduleauthor:: Daniel Carrion <[email protected]>
99
.. moduleauthor:: Alberto Romeu <[email protected]>
10+
.. moduleauthor:: Juan Ignacio Sánchez <[email protected]>
1011
1112
1213
"""
@@ -16,7 +17,7 @@
1617
import sys
1718
import warnings
1819

19-
from pyrestcli.auth import BaseAuthClient
20+
from pyrestcli.auth import BaseAuthClient, BasicAuthClient
2021

2122
from .exceptions import CartoException
2223

@@ -26,7 +27,43 @@
2627
from urlparse import urlparse
2728

2829

29-
class APIKeyAuthClient(BaseAuthClient):
30+
class _UsernameGetter:
31+
def get_user_name(self, base_url):
32+
try:
33+
url_info = urlparse(base_url)
34+
# On-Prem:
35+
# /user/<username>
36+
m = re.search('^/user/([^/]+)/.*$', url_info.path)
37+
if m is None:
38+
# Cloud personal account (org and standalone)
39+
# <username>.carto.com
40+
netloc = url_info.netloc
41+
if netloc.startswith('www.'):
42+
netloc = netloc.split('www.')[1]
43+
m = re.search('^(.*?)\..*', netloc)
44+
return m.group(1)
45+
except Exception:
46+
raise CartoException(_("Could not find a valid user_name in the " +
47+
"base URL provided. Please check that the" +
48+
"URL is one of " +
49+
"'https://{user_name}.carto.com', " +
50+
"'https://carto.com/user/{user_name}' " +
51+
"or a similar one based on your domain"))
52+
53+
54+
class _BaseUrlChecker:
55+
def check_base_url(self, base_url):
56+
if not base_url.startswith("https"):
57+
warnings.warn("You are using unencrypted API key \
58+
authentication!!!")
59+
# Make sure there is a trailing / for urljoin
60+
if not base_url.endswith('/'):
61+
base_url += '/'
62+
63+
return base_url
64+
65+
66+
class APIKeyAuthClient(_UsernameGetter, _BaseUrlChecker, BaseAuthClient):
3067
"""
3168
This class provides you with authenticated access to CARTO's APIs using
3269
your API key.
@@ -48,17 +85,9 @@ def __init__(self, base_url, api_key, organization=None, session=None):
4885
4986
:return:
5087
"""
51-
if not base_url.startswith("https"):
52-
warnings.warn("You are using unencrypted API key \
53-
authentication!!!")
54-
5588
self.organization = organization
5689
self.api_key = api_key
57-
58-
# Make sure there is a trailing / for urljoin
59-
if not base_url.endswith('/'):
60-
base_url += '/'
61-
90+
base_url = self.check_base_url(base_url)
6291
self.username = self.get_user_name(base_url)
6392

6493
super(APIKeyAuthClient, self).__init__(base_url, session=session)
@@ -88,28 +117,6 @@ def send(self, relative_path, http_method, **requests_args):
88117
except Exception as e:
89118
raise CartoException(e)
90119

91-
def get_user_name(self, base_url):
92-
try:
93-
url_info = urlparse(base_url)
94-
# On-Prem:
95-
# /user/<username>
96-
m = re.search('^/user/([^/]+)/.*$', url_info.path)
97-
if m is None:
98-
# Cloud personal account (org and standalone)
99-
# <username>.carto.com
100-
netloc = url_info.netloc
101-
if netloc.startswith('www.'):
102-
netloc = netloc.split('www.')[1]
103-
m = re.search('^(.*?)\..*', netloc)
104-
return m.group(1)
105-
except Exception:
106-
raise CartoException(_("Could not find a valid user_name in the " +
107-
"base URL provided. Please check that the" +
108-
"URL is one of " +
109-
"'https://{user_name}.carto.com', " +
110-
"'https://carto.com/user/{user_name}' " +
111-
"or a similar one based on your domain"))
112-
113120
def prepare_send(self, http_method, **requests_args):
114121
http_method = http_method.lower()
115122
if (http_method in ['post', 'put']) and "json" in requests_args:
@@ -169,4 +176,51 @@ def send(self, relative_path, http_method, **requests_args):
169176
http_method,
170177
**requests_args)
171178
except Exception as e:
172-
raise CartoException(e)
179+
raise CartoException(e)
180+
181+
182+
class AuthAPIClient(_UsernameGetter, _BaseUrlChecker, BasicAuthClient):
183+
"""
184+
This class provides you with authenticated access to CARTO's APIs using
185+
your API key at Basic authentication header, as provided by Auth API.
186+
187+
Auth API is still under development. You might want to take a look at
188+
APIKeyAuthClient for missing features or an stable API.
189+
190+
You can find your API key by clicking on the API key section of the user
191+
dropdown menu
192+
"""
193+
194+
def __init__(self, base_url, api_key, organization=None, session=None):
195+
"""
196+
Init method
197+
198+
:param base_url: Base URL. API endpoint paths will always be relative
199+
to this URL
200+
:param api_key: API key
201+
:param organization: For enterprise users, organization user belongs to
202+
:param session: requests' session object
203+
:type api_key: str
204+
:type organization: str
205+
206+
:return:
207+
"""
208+
self.organization = organization
209+
self.api_key = api_key
210+
base_url = self.check_base_url(base_url)
211+
self.username = self.get_user_name(base_url)
212+
213+
super(AuthAPIClient, self).__init__(self.username, api_key, base_url, session=session)
214+
215+
def is_valid_api_key(self):
216+
"""
217+
Checks validity. Right now, an API key is considered valid if it
218+
can list user API keys and the result contains that API key.
219+
This might change in the future.
220+
221+
:return: True if the API key is considered valid for current user.
222+
"""
223+
res = self.send('/api/v3/api_keys', 'get')
224+
return \
225+
res.ok and \
226+
self.api_key in (ak['token'] for ak in res.json()['result'])

doc/source/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@
5555
# built documents.
5656
#
5757
# The short X.Y version.
58-
version = u'1.1.2'
58+
version = u'1.2.1'
5959
# The full version, including alpha/beta/rc tags.
60-
release = u'1.1.2'
60+
release = u'1.2.1'
6161

6262
# The language for content autogenerated by Sphinx. Refer to documentation
6363
# for a list of supported languages.

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
author="Daniel Carrión",
1818
author_email="[email protected]",
1919
description="SDK around CARTO's APIs",
20-
version="1.1.2",
20+
version="1.2.1",
2121
url="https://github.com/CartoDB/carto-python",
2222
install_requires=required,
2323
packages=["carto"])

tests/conftest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from carto.users import UserManager
88
from mocks import MockRequests, NotMockRequests
99

10+
DEFAULT_PUBLIC_API_KEY = 'default_public'
11+
1012
if "PROTOCOL" in os.environ:
1113
PROTOCOL = os.environ["PROTOCOL"]
1214
else:

tests/test_auth.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import pytest
22

33
from secret import API_KEY
4-
54
from carto.auth import APIKeyAuthClient
65
from carto.exceptions import CartoException
6+
from conftest import USR_BASE_URL, DEFAULT_PUBLIC_API_KEY
7+
from carto.auth import AuthAPIClient
78

89

910
def test_wrong_url():
@@ -33,3 +34,56 @@ def test_on_prem_url():
3334
assert user1 == 'user1'
3435
assert user2 == 'user2'
3536
assert user3 == 'user3'
37+
38+
USER1_BASE_URL = 'https://user1.carto.com/'
39+
USER1_USERNAME = 'user1'
40+
41+
42+
def test_api_key_auth_client_username():
43+
conf_username = APIKeyAuthClient(USER1_BASE_URL, API_KEY).username
44+
assert conf_username == USER1_USERNAME
45+
46+
47+
def test_api_key_auth_client_me_endpoint():
48+
client = APIKeyAuthClient(USER1_BASE_URL, API_KEY)
49+
username = client.send('/api/v3/me', 'get').json()['config']['user_name']
50+
assert username == USER1_USERNAME
51+
52+
53+
def test_auth_api_client_username():
54+
conf_username = AuthAPIClient(USER1_BASE_URL, API_KEY).username
55+
assert conf_username == USER1_USERNAME
56+
57+
58+
def test_auth_api_client_me_endpoint():
59+
client = AuthAPIClient(USER1_BASE_URL, API_KEY)
60+
username = client.send('/api/v3/me', 'get').json()['config']['user_name']
61+
assert username == USER1_USERNAME
62+
63+
64+
def test_api_key_auth_cant_read_api_keys_with_default_public():
65+
client = APIKeyAuthClient(USER1_BASE_URL, DEFAULT_PUBLIC_API_KEY)
66+
response = client.send('/api/v3/api_keys', 'get')
67+
assert response.status_code == 401
68+
69+
70+
def test_auth_api_can_read_api_keys_with_default_public():
71+
client = AuthAPIClient(USER1_BASE_URL, DEFAULT_PUBLIC_API_KEY)
72+
response = client.send('/api/v3/api_keys', 'get')
73+
assert response.status_code == 200
74+
assert response.json()['count'] == 1
75+
76+
77+
def test_auth_api_is_valid_api_key_with_wrong_key():
78+
assert AuthAPIClient(USER1_BASE_URL, 'wadus').is_valid_api_key() is False
79+
80+
81+
def test_auth_api_is_valid_api_key_with_default_public_key():
82+
assert AuthAPIClient(USER1_BASE_URL, DEFAULT_PUBLIC_API_KEY). \
83+
is_valid_api_key()
84+
85+
86+
def test_auth_api_is_valid_api_key_with_master_key():
87+
if API_KEY == 'mockmockmock':
88+
pytest.skip("Can't be tested with mock api key")
89+
assert AuthAPIClient(USR_BASE_URL, API_KEY).is_valid_api_key()

0 commit comments

Comments
 (0)