Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 6 additions & 11 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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...")
Expand All @@ -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
------------------------------
Expand Down
Empty file added docs/_static/.gitkeep
Empty file.
10 changes: 10 additions & 0 deletions docs/api/ovh/client.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 33 additions & 0 deletions docs/api/ovh/consumer_key.rst
Original file line number Diff line number Diff line change
@@ -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

2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
6 changes: 6 additions & 0 deletions ovh/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
21 changes: 21 additions & 0 deletions ovh/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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):
"""
Expand Down
113 changes: 113 additions & 0 deletions ovh/consumer_key.py
Original file line number Diff line number Diff line change
@@ -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+'/*')

6 changes: 6 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
89 changes: 89 additions & 0 deletions tests/test_consumer_key.py
Original file line number Diff line number Diff line change
@@ -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)