Skip to content

Commit

Permalink
Merge branch 'release-0.12.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
akira-dev committed Mar 27, 2017
2 parents 0679641 + 67a91be commit 723786b
Show file tree
Hide file tree
Showing 6 changed files with 301 additions and 36 deletions.
4 changes: 4 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ Flask-REST-JSONAPI has lot of features:
* Sparse fieldsets
* Pagination
* Sorting
* OAuth
* Permission management


User's Guide
Expand All @@ -52,6 +54,8 @@ Flask-REST-JSONAPI with Flask.
pagination
sorting
errors
permission
oauth

API Reference
-------------
Expand Down
93 changes: 93 additions & 0 deletions docs/oauth.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
.. _oauth:

OAuth
=====

.. currentmodule:: flask_rest_jsonapi

Flask-REST-JSONAPI support OAuth via `Flask-OAuthlib <https://github.com/lepture/flask-oauthlib>`_

Example:

.. code-block:: python
from flask import Flask
from flask_rest_jsonapi import Api
from flask_oauthlib.provider import OAuth2Provider
app = Flask(__name__)
oauth2 = OAuth2Provider()
api = Api()
api.init_app(app)
api.oauth_manager(oauth2)
In this example Flask-REST-JSONAPI will protect all your resource methods with this decorator ::

oauth2.require_oauth(<scope>)

The pattern of the scope is like that ::

<action>_<resource_type>

Where action is:

* list: for the get method of a ResourceList
* create: for the post method of a ResourceList
* get: for the get method of a ResourceDetail
* update: for the patch method of a ResourceDetail
* delete: for the delete method of a ResourceDetail

Example ::

list_person

If you want to customize the scope you can provide a function that computes your custom scope. The function have to looks like that:

.. code-block:: python
def get_scope(resource, method):
"""Compute the name of the scope for oauth

:param Resource resource: the resource manager
:param str method: an http method
:return str: the name of the scope
"""
return 'custom_scope'

Usage example:

.. code-block:: python
from flask import Flask
from flask_rest_jsonapi import Api
from flask_oauthlib.provider import OAuth2Provider
app = Flask(__name__)
oauth2 = OAuth2Provider()
api = Api()
api.init_app(app)
api.oauth_manager(oauth2)
api.scope_setter(get_scope)
.. note::

You can name the custom scope computation method as you want but you have to set the 2 required parameters: resource and method like in this previous example.

If you want to disable OAuth or make custom methods protection for a resource you can add this option to the resource manager.

Example:

.. code-block:: python
from flask_rest_jsonapi import ResourceList
from your_project.extensions import oauth2

class PersonList(ResourceList):
disable_oauth = True

@oauth2.require_oauth('custom_scope')
def get(*args, **kwargs):
return 'Hello world !'
86 changes: 86 additions & 0 deletions docs/permission.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
.. _permission:

Permission
==========

.. currentmodule:: flask_rest_jsonapi

Flask-REST-JSONAPI provides a very agnostic permission system.

Example:

.. code-block:: python
from flask import Flask
from flask_rest_jsonapi import Api
from your_project.permission import permission_manager
app = Flask(__name__)
api = Api()
api.init_app(app)
api.permission_manager(permission_manager)
In this previous example, the API will check permission before each method call with the permission_manager function.

The permission manager must be a function that looks like this:

.. code-block:: python
def permission_manager(view, view_args, view_kwargs, *args, **kwargs):
"""The function use to check permissions
:param callable view: the view
:param list view_args: view args
:param dict view_kwargs: view kwargs
:param list args: decorator args
:param dict kwargs: decorator kwargs
"""
.. note::

Flask-REST-JSONAPI use a decorator to check permission for each method named has_permission. You can provide args and kwargs to this decorators so you can retrieve this args and kwargs in the permission_manager. The default usage of the permission system does not provides any args or kwargs to the decorator.

If permission is denied I recommand to raise exception like that:

.. code-block:: python
raise JsonApiException(<error_source>,
<error_details>,
title='Permission denied',
status='403')

You can disable the permission system or make custom permission checking management of a resource like that:

.. code-block:: python
from flask_rest_jsonapi import ResourceList
from your_project.extensions import api

class PersonList(ResourceList):
disable_permission = True

@api.has_permission('custom_arg', custom_kwargs='custom_kwargs')
def get(*args, **kwargs):
return 'Hello world !'
.. warning::

If you want to use both permission system and oauth support to retrieve information like user from oauth (request.oauth.user) in the permission system you have to initialize permission system before to initialize oauth support because of decorators cascading.

Example:

.. code-block:: python
from flask import Flask
from flask_rest_jsonapi import Api
from flask_oauthlib.provider import OAuth2Provider
from your_project.permission import permission_manager
app = Flask(__name__)
oauth2 = OAuth2Provider()
api = Api()
api.init_app(app)
api.permission_manager(permission_manager) # initialize permission system first
api.oauth_manager(oauth2) # initialize oauth support second
121 changes: 101 additions & 20 deletions flask_rest_jsonapi/api.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,38 @@
# -*- coding: utf-8 -*-

from flask import Blueprint
import inspect
from functools import wraps

from flask_rest_jsonapi.resource import ResourceList

class Api(object):

def __init__(self, app=None):
self.app = None
self.blueprint = None

if app is not None:
if isinstance(app, Blueprint):
self.blueprint = app
else:
self.app = app
class Api(object):

def __init__(self, app=None, blueprint=None):
self.app = app
self.blueprint = blueprint
self.resources = []
self.resource_registry = []

def init_app(self, app=None):
def init_app(self, app=None, blueprint=None):
"""Update flask application with our api
:param Application app: a flask application
"""
if self.app is None:
if app is not None:
self.app = app

if blueprint is not None:
self.blueprint = blueprint

for resource in self.resources:
self.route(resource['resource'],
resource['view'],
*resource['urls'],
url_rule_options=resource['url_rule_options'])

if self.blueprint is not None:
self.app.register_blueprint(self.blueprint)
else:
for resource in self.resources:
self.route(**resource)

def route(self, resource, view, *urls, **kwargs):
"""Create an api view.
Expand All @@ -43,15 +46,93 @@ def route(self, resource, view, *urls, **kwargs):
view_func = resource.as_view(view)
url_rule_options = kwargs.get('url_rule_options') or dict()

if self.app is not None:
for url in urls:
self.app.add_url_rule(url, view_func=view_func, **url_rule_options)
elif self.blueprint is not None:
if self.blueprint is not None:
resource.view = '.'.join([self.blueprint.name, resource.view])
for url in urls:
self.blueprint.add_url_rule(url, view_func=view_func, **url_rule_options)
elif self.app is not None:
for url in urls:
self.app.add_url_rule(url, view_func=view_func, **url_rule_options)
else:
self.resources.append({'resource': resource,
'view': view,
'urls': urls,
'url_rule_options': url_rule_options})

self.resource_registry.append(resource)

def oauth_manager(self, oauth_manager):
"""Use the oauth manager to enable oauth for API
:param oauth_manager: the oauth manager
"""
for resource in self.resource_registry:
if getattr(resource, 'disable_oauth', None) is not True:
for method in getattr(resource, 'methods', ('GET', 'POST', 'PATCH', 'DELETE')):
scope = self.get_scope(resource, method)
setattr(resource,
method.lower(),
oauth_manager.require_oauth(scope)(getattr(resource, method.lower())))

def scope_setter(self, func):
"""Plug oauth scope setter function to the API
:param callable func: the callable to use a scope getter
"""
self.get_scope = func

@staticmethod
def get_scope(resource, method):
"""Compute the name of the scope for oauth
:param Resource resource: the resource manager
:param str method: an http method
:return str: the name of the scope
"""
if ResourceList in inspect.getmro(resource) and method == 'GET':
prefix = 'list'
else:
method_to_prefix = {'GET': 'get',
'POST': 'create',
'PATCH': 'update',
'DELETE': 'delete'}
prefix = method_to_prefix[method]

return '_'.join([prefix, resource.schema.opts.type_])

def permission_manager(self, permission_manager):
"""Use permission manager to enable permission for API
:param callable permission_manager: the permission manager
"""
self.check_permissions = permission_manager

for resource in self.resource_registry:
if getattr(resource, 'disable_permission', None) is not True:
for method in getattr(resource, 'methods', ('GET', 'POST', 'PATCH', 'DELETE')):
setattr(resource,
method.lower(),
self.has_permission()(getattr(resource, method.lower())))

def has_permission(self, *args, **kwargs):
"""Decorator used to check permissions before to call resource manager method
"""
def wrapper(view):
@wraps(view)
def decorated(*view_args, **view_kwargs):
self.check_permissions(view, view_args, view_kwargs, *args, **kwargs)
return view(*view_args, **view_kwargs)
return decorated
return wrapper

@staticmethod
def check_permissions(view, view_args, view_kwargs, *args, **kwargs):
"""The function use to check permissions
:param callable view: the view
:param list view_args: view args
:param dict view_kwargs: view kwargs
:param list args: decorator args
:param dict kwargs: decorator kwargs
"""
raise NotImplementedError
Loading

0 comments on commit 723786b

Please sign in to comment.