From 3980f0c6719e27681aeb53fbdeb74fa87eaf4a60 Mon Sep 17 00:00:00 2001 From: Jean-Tiare Le Bigot Date: Tue, 5 Apr 2016 19:01:06 +0200 Subject: [PATCH] add consumer key helpers Signed-off-by: Jean-Tiare Le Bigot --- README.rst | 17 ++--- docs/_static/.gitkeep | 0 docs/api/ovh/client.rst | 10 +++ docs/api/ovh/consumer_key.rst | 33 ++++++++++ docs/index.rst | 2 +- ovh/__init__.py | 6 ++ ovh/client.py | 21 +++++++ ovh/consumer_key.py | 113 ++++++++++++++++++++++++++++++++++ tests/test_client.py | 6 ++ tests/test_consumer_key.py | 89 ++++++++++++++++++++++++++ 10 files changed, 285 insertions(+), 12 deletions(-) create mode 100644 docs/_static/.gitkeep create mode 100644 docs/api/ovh/consumer_key.rst create mode 100644 ovh/consumer_key.py create mode 100644 tests/test_consumer_key.py diff --git a/README.rst b/README.rst index 69b82a5..26bbd46 100644 --- a/README.rst +++ b/README.rst @@ -130,12 +130,11 @@ customer's informations: client = ovh.Client() # Request RO, /me API access - access_rules = [ - {'method': 'GET', 'path': '/me'}, - ] + ck = client.new_consumer_key_request() + ck.add_rules(ovh.API_READ_ONLY, "/me") # Request token - validation = client.request_consumerkey(access_rules) + validation = ck.request() print "Please visit %s to authenticate" % validation['validationUrl'] raw_input("and press Enter to continue...") @@ -148,16 +147,12 @@ customer's informations: Returned ``consumerKey`` should then be kept to avoid re-authenticating your end-user on each use. -.. note:: To request full and unlimited access to the API, you may use wildcards: +.. note:: To request full and unlimited access to the API, you may use ``add_recursive_rules``: .. code:: python - access_rules = [ - {'method': 'GET', 'path': '/*'}, - {'method': 'POST', 'path': '/*'}, - {'method': 'PUT', 'path': '/*'}, - {'method': 'DELETE', 'path': '/*'} - ] + # Allow all GET, POST, PUT, DELETE on /* (full API) + ck.add_recursive_rules(ovh.API_READ_WRITE, '/') Install a new mail redirection ------------------------------ diff --git a/docs/_static/.gitkeep b/docs/_static/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/api/ovh/client.rst b/docs/api/ovh/client.rst index 28189cd..7453081 100644 --- a/docs/api/ovh/client.rst +++ b/docs/api/ovh/client.rst @@ -22,6 +22,16 @@ High level helpers request_consumerkey ------------------- +Helpers to generate a consumer key. See ``new_consumer_key_request`` +below for a full working example or :py:class:`ConsumerKeyRequest` +for dertailed implementatation. + +The basic idea of ``ConsumerKeyRequest`` is to generate appropriate +autorization requests from human readable function calls. In short: +use it! + +.. automethod:: Client.new_consumer_key_request + .. automethod:: Client.request_consumerkey get/post/put/delete diff --git a/docs/api/ovh/consumer_key.rst b/docs/api/ovh/consumer_key.rst new file mode 100644 index 0000000..31f9749 --- /dev/null +++ b/docs/api/ovh/consumer_key.rst @@ -0,0 +1,33 @@ +############# +Client Module +############# + +.. currentmodule:: ovh.consumer_key + +.. automodule:: ovh.consumer_key + +.. autoclass:: ConsumerKeyRequest + +Constructor +=========== + +__init__ +-------- + +.. automethod:: ConsumerKeyRequest.__init__ + +Helpers +======= + +Generate rules +-------------- + +.. automethod:: ConsumerKeyRequest.add_rule +.. automethod:: ConsumerKeyRequest.add_rules +.. automethod:: ConsumerKeyRequest.add_recursive_rules + +Trigger request +--------------- + +.. automethod:: ConsumerKeyRequest.request + diff --git a/docs/index.rst b/docs/index.rst index 1817026..9d9fc68 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -364,7 +364,7 @@ This lookup mechanism makes it easy to overload credentials for a specific project or user. Passing parameters -============= +================== You can call all the methods of the API with the necessary arguments. diff --git a/ovh/__init__.py b/ovh/__init__.py index d6fc7f2..26f19ce 100644 --- a/ovh/__init__.py +++ b/ovh/__init__.py @@ -32,3 +32,9 @@ ResourceNotFoundError, BadParametersError, ResourceConflictError, HTTPError, InvalidKey, InvalidCredential, NotGrantedCall, NotCredential, Forbidden, ) +from .consumer_key import ( + ConsumerKeyRequest, + API_READ_ONLY, + API_READ_WRITE, + API_READ_WRITE_SAFE, +) diff --git a/ovh/client.py b/ovh/client.py index db4d6d0..5aafab2 100644 --- a/ovh/client.py +++ b/ovh/client.py @@ -50,6 +50,7 @@ from requests.exceptions import RequestException from .config import config +from .consumer_key import ConsumerKeyRequest from .exceptions import ( APIError, NetworkError, InvalidResponse, InvalidRegion, InvalidKey, ResourceNotFoundError, BadParametersError, ResourceConflictError, HTTPError, @@ -189,6 +190,26 @@ def time_delta(self): self._time_delta = server_time - int(time.time()) return self._time_delta + def new_consumer_key_request(self): + """ + Create a new consumer key request. This is the recommended way to create + a new consumer key request. + + Full example: + + >>> import ovh + >>> client = ovh.Client("ovh-eu") + >>> ck = client.new_consumer_key_request() + >>> ck.add_rules(ovh.API_READ_ONLY, "/me") + >>> ck.add_recursive_rules(ovh.API_READ_WRITE, "/sms") + >>> ck.request() + { + 'state': 'pendingValidation', + 'consumerKey': 'TnpZAd5pYNqxk4RhlPiSRfJ4WrkmII2i', + 'validationUrl': 'https://eu.api.ovh.com/auth/?credentialToken=now2OOAVO4Wp6t7bemyN9DMWIobhGjFNZSHmixtVJM4S7mzjkN2L5VBfG96Iy1i0' + } + """ + return ConsumerKeyRequest(self) def request_consumerkey(self, access_rules, redirect_url=None): """ diff --git a/ovh/consumer_key.py b/ovh/consumer_key.py new file mode 100644 index 0000000..7a14ba2 --- /dev/null +++ b/ovh/consumer_key.py @@ -0,0 +1,113 @@ +# -*- encoding: utf-8 -*- +# +# Copyright (c) 2013-2016, OVH SAS. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of OVH SAS nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ````AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +""" +This module provides a consumer key creation helper. Consumer keys are linked +with permissions defining whicg endpoint they are allowed to call. Just like +a physical key can unlock some doors but not others. + +OVH API consumer keys authorization is pattern based. This makes it extremely +powerful and flexible as it may apply on only a very specific subset of the API +but it's also trickier to get right on simple scenarios. + +Hence this module +""" + +# Common authorization patterns +API_READ_ONLY = ["GET"] +API_READ_WRITE = ["GET", "POST", "PUT", "DELETE"] +API_READ_WRITE_SAFE = ["GET", "POST", "PUT"] + +class ConsumerKeyRequest(object): + ''' + ConsumerKey request. The generated consumer key will be linked to the + client's ``application_key``. When performing the request, the + ``consumer_key`` will automatically be registered in the client. + + It is recommended to save the generated key as soon as it validated to avoid + requesting a new one on each API access. + ''' + + def __init__(self, client): + ''' + Create a new consumer key helper on API ``client``. The keys will be + tied to the ``application_key`` defined in the client. + ''' + self._client = client + self._access_rules = [] + + def request(self, redirect_url=None): + ''' + Create the consumer key with the configures autorizations. The user will + need to validate it befor it can be used with the API + + >>> ck.request() + { + 'state': 'pendingValidation', + 'consumerKey': 'TnpZAd5pYNqxk4RhlPiSRfJ4WrkmII2i', + 'validationUrl': 'https://eu.api.ovh.com/auth/?credentialToken=now2OOAVO4Wp6t7bemyN9DMWIobhGjFNZSHmixtVJM4S7mzjkN2L5VBfG96Iy1i0' + } + ''' + return self._client.request_consumerkey(self._access_rules, redirect_url) + + def add_rule(self, method, path): + ''' + Add a new rule to the request. Will grant the ``(method, path)`` tuple. + Path can be any API route pattern like ``/sms/*`` or ``/me``. For example, + to grant RO access on personal data: + + >>> ck.add_rule("GET", "/me") + ''' + self._access_rules.append({'method': method.upper(), 'path': path}) + + def add_rules(self, methods, path): + ''' + Add rules for ``path`` pattern, for each methods in ``methods``. This is + a convenient helper over ``add_rule``. For example, this could be used + to grant all access on the API at once: + + >>> ck.add_rules(["GET", "POST", "PUT", "DELETE"], "/*") + ''' + for method in methods: + self.add_rule(method, path) + + def add_recursive_rules(self, methods, path): + ''' + Use this method to grant access on a full API tree. This is the + recommended way to grant access in the API. It will take care of granted + the root call *AND* sub-calls for you. Which is commonly forgotten... + For example, to grant a full access on ``/sms``: + + >>> ck.add_recursive_rules(["GET", "POST", "PUT", "DELETE"], "/sms") + ''' + path = path.rstrip('*/ ') + if path: + self.add_rules(methods, path) + self.add_rules(methods, path+'/*') + diff --git a/tests/test_client.py b/tests/test_client.py index f167d5d..6660d90 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -138,6 +138,12 @@ def test_request_consumerkey(self, m_call): 'accessRules': FAKE_RULES, }, False) + def test_new_consumer_key_request(self): + api = Client(ENDPOINT, APPLICATION_KEY, APPLICATION_SECRET, CONSUMER_KEY) + + ck = api.new_consumer_key_request() + self.assertEqual(ck._client, api) + ## test wrappers def test__canonicalize_kwargs(self): diff --git a/tests/test_consumer_key.py b/tests/test_consumer_key.py new file mode 100644 index 0000000..effb1c3 --- /dev/null +++ b/tests/test_consumer_key.py @@ -0,0 +1,89 @@ +# -*- encoding: utf-8 -*- +# +# Copyright (c) 2013-2016, OVH SAS. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of OVH SAS nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import unittest +import requests +import mock + +class testConsumerKeyRequest(unittest.TestCase): + def test_add_rules(self): + # Prepare + import ovh + m_client = mock.Mock() + ck = ovh.ConsumerKeyRequest(m_client) + + # Test: No-op + self.assertEqual([], ck._access_rules) + ck._access_rules = [] + + # Test: allow one + ck.add_rule("GET", '/me') + self.assertEqual([ + {'method': 'GET', 'path': '/me'}, + ], ck._access_rules) + ck._access_rules = [] + + # Test: allow safe methods on domain + ck.add_rules(ovh.API_READ_WRITE_SAFE, '/domains/test.com') + self.assertEqual([ + {'method': 'GET', 'path': '/domains/test.com'}, + {'method': 'POST', 'path': '/domains/test.com'}, + {'method': 'PUT', 'path': '/domains/test.com'}, + ], ck._access_rules) + ck._access_rules = [] + + # Test: allow all sms, strips suffix + ck.add_recursive_rules(ovh.API_READ_WRITE, '/sms/*') + self.assertEqual([ + {'method': 'GET', 'path': '/sms'}, + {'method': 'POST', 'path': '/sms'}, + {'method': 'PUT', 'path': '/sms'}, + {'method': 'DELETE', 'path': '/sms'}, + + {'method': 'GET', 'path': '/sms/*'}, + {'method': 'POST', 'path': '/sms/*'}, + {'method': 'PUT', 'path': '/sms/*'}, + {'method': 'DELETE', 'path': '/sms/*'}, + ], ck._access_rules) + ck._access_rules = [] + + # Test: allow all, does not insert the empty rule + ck.add_recursive_rules(ovh.API_READ_WRITE, '/') + self.assertEqual([ + {'method': 'GET', 'path': '/*'}, + {'method': 'POST', 'path': '/*'}, + {'method': 'PUT', 'path': '/*'}, + {'method': 'DELETE', 'path': '/*'}, + ], ck._access_rules) + ck._access_rules = [] + + # Test launch request + ck.add_recursive_rules(ovh.API_READ_WRITE, '/') + self.assertEqual(m_client.request_consumerkey.return_value, ck.request()) + m_client.request_consumerkey.assert_called_once_with(ck._access_rules, None) +