Skip to content

Commit 723786b

Browse files
committed
Merge branch 'release-0.12.0'
2 parents 0679641 + 67a91be commit 723786b

File tree

6 files changed

+301
-36
lines changed

6 files changed

+301
-36
lines changed

docs/index.rst

+4
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ Flask-REST-JSONAPI has lot of features:
2929
* Sparse fieldsets
3030
* Pagination
3131
* Sorting
32+
* OAuth
33+
* Permission management
3234

3335

3436
User's Guide
@@ -52,6 +54,8 @@ Flask-REST-JSONAPI with Flask.
5254
pagination
5355
sorting
5456
errors
57+
permission
58+
oauth
5559

5660
API Reference
5761
-------------

docs/oauth.rst

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
.. _oauth:
2+
3+
OAuth
4+
=====
5+
6+
.. currentmodule:: flask_rest_jsonapi
7+
8+
Flask-REST-JSONAPI support OAuth via `Flask-OAuthlib <https://github.com/lepture/flask-oauthlib>`_
9+
10+
Example:
11+
12+
.. code-block:: python
13+
14+
from flask import Flask
15+
from flask_rest_jsonapi import Api
16+
from flask_oauthlib.provider import OAuth2Provider
17+
18+
app = Flask(__name__)
19+
oauth2 = OAuth2Provider()
20+
21+
api = Api()
22+
api.init_app(app)
23+
api.oauth_manager(oauth2)
24+
25+
26+
In this example Flask-REST-JSONAPI will protect all your resource methods with this decorator ::
27+
28+
oauth2.require_oauth(<scope>)
29+
30+
The pattern of the scope is like that ::
31+
32+
<action>_<resource_type>
33+
34+
Where action is:
35+
36+
* list: for the get method of a ResourceList
37+
* create: for the post method of a ResourceList
38+
* get: for the get method of a ResourceDetail
39+
* update: for the patch method of a ResourceDetail
40+
* delete: for the delete method of a ResourceDetail
41+
42+
Example ::
43+
44+
list_person
45+
46+
If you want to customize the scope you can provide a function that computes your custom scope. The function have to looks like that:
47+
48+
.. code-block:: python
49+
50+
def get_scope(resource, method):
51+
"""Compute the name of the scope for oauth
52+
53+
:param Resource resource: the resource manager
54+
:param str method: an http method
55+
:return str: the name of the scope
56+
"""
57+
return 'custom_scope'
58+
59+
Usage example:
60+
61+
.. code-block:: python
62+
63+
from flask import Flask
64+
from flask_rest_jsonapi import Api
65+
from flask_oauthlib.provider import OAuth2Provider
66+
67+
app = Flask(__name__)
68+
oauth2 = OAuth2Provider()
69+
70+
api = Api()
71+
api.init_app(app)
72+
api.oauth_manager(oauth2)
73+
api.scope_setter(get_scope)
74+
75+
.. note::
76+
77+
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.
78+
79+
If you want to disable OAuth or make custom methods protection for a resource you can add this option to the resource manager.
80+
81+
Example:
82+
83+
.. code-block:: python
84+
85+
from flask_rest_jsonapi import ResourceList
86+
from your_project.extensions import oauth2
87+
88+
class PersonList(ResourceList):
89+
disable_oauth = True
90+
91+
@oauth2.require_oauth('custom_scope')
92+
def get(*args, **kwargs):
93+
return 'Hello world !'

docs/permission.rst

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
.. _permission:
2+
3+
Permission
4+
==========
5+
6+
.. currentmodule:: flask_rest_jsonapi
7+
8+
Flask-REST-JSONAPI provides a very agnostic permission system.
9+
10+
Example:
11+
12+
.. code-block:: python
13+
14+
from flask import Flask
15+
from flask_rest_jsonapi import Api
16+
from your_project.permission import permission_manager
17+
18+
app = Flask(__name__)
19+
20+
api = Api()
21+
api.init_app(app)
22+
api.permission_manager(permission_manager)
23+
24+
In this previous example, the API will check permission before each method call with the permission_manager function.
25+
26+
The permission manager must be a function that looks like this:
27+
28+
.. code-block:: python
29+
30+
def permission_manager(view, view_args, view_kwargs, *args, **kwargs):
31+
"""The function use to check permissions
32+
33+
:param callable view: the view
34+
:param list view_args: view args
35+
:param dict view_kwargs: view kwargs
36+
:param list args: decorator args
37+
:param dict kwargs: decorator kwargs
38+
"""
39+
40+
.. note::
41+
42+
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.
43+
44+
If permission is denied I recommand to raise exception like that:
45+
46+
.. code-block:: python
47+
48+
raise JsonApiException(<error_source>,
49+
<error_details>,
50+
title='Permission denied',
51+
status='403')
52+
53+
You can disable the permission system or make custom permission checking management of a resource like that:
54+
55+
.. code-block:: python
56+
57+
from flask_rest_jsonapi import ResourceList
58+
from your_project.extensions import api
59+
60+
class PersonList(ResourceList):
61+
disable_permission = True
62+
63+
@api.has_permission('custom_arg', custom_kwargs='custom_kwargs')
64+
def get(*args, **kwargs):
65+
return 'Hello world !'
66+
67+
.. warning::
68+
69+
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.
70+
71+
Example:
72+
73+
.. code-block:: python
74+
75+
from flask import Flask
76+
from flask_rest_jsonapi import Api
77+
from flask_oauthlib.provider import OAuth2Provider
78+
from your_project.permission import permission_manager
79+
80+
app = Flask(__name__)
81+
oauth2 = OAuth2Provider()
82+
83+
api = Api()
84+
api.init_app(app)
85+
api.permission_manager(permission_manager) # initialize permission system first
86+
api.oauth_manager(oauth2) # initialize oauth support second

flask_rest_jsonapi/api.py

+101-20
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,38 @@
11
# -*- coding: utf-8 -*-
22

3-
from flask import Blueprint
3+
import inspect
4+
from functools import wraps
45

6+
from flask_rest_jsonapi.resource import ResourceList
57

6-
class Api(object):
7-
8-
def __init__(self, app=None):
9-
self.app = None
10-
self.blueprint = None
118

12-
if app is not None:
13-
if isinstance(app, Blueprint):
14-
self.blueprint = app
15-
else:
16-
self.app = app
9+
class Api(object):
1710

11+
def __init__(self, app=None, blueprint=None):
12+
self.app = app
13+
self.blueprint = blueprint
1814
self.resources = []
15+
self.resource_registry = []
1916

20-
def init_app(self, app=None):
17+
def init_app(self, app=None, blueprint=None):
2118
"""Update flask application with our api
2219
2320
:param Application app: a flask application
2421
"""
25-
if self.app is None:
22+
if app is not None:
2623
self.app = app
2724

25+
if blueprint is not None:
26+
self.blueprint = blueprint
27+
28+
for resource in self.resources:
29+
self.route(resource['resource'],
30+
resource['view'],
31+
*resource['urls'],
32+
url_rule_options=resource['url_rule_options'])
33+
2834
if self.blueprint is not None:
2935
self.app.register_blueprint(self.blueprint)
30-
else:
31-
for resource in self.resources:
32-
self.route(**resource)
3336

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

46-
if self.app is not None:
47-
for url in urls:
48-
self.app.add_url_rule(url, view_func=view_func, **url_rule_options)
49-
elif self.blueprint is not None:
49+
if self.blueprint is not None:
5050
resource.view = '.'.join([self.blueprint.name, resource.view])
5151
for url in urls:
5252
self.blueprint.add_url_rule(url, view_func=view_func, **url_rule_options)
53+
elif self.app is not None:
54+
for url in urls:
55+
self.app.add_url_rule(url, view_func=view_func, **url_rule_options)
5356
else:
5457
self.resources.append({'resource': resource,
5558
'view': view,
5659
'urls': urls,
5760
'url_rule_options': url_rule_options})
61+
62+
self.resource_registry.append(resource)
63+
64+
def oauth_manager(self, oauth_manager):
65+
"""Use the oauth manager to enable oauth for API
66+
67+
:param oauth_manager: the oauth manager
68+
"""
69+
for resource in self.resource_registry:
70+
if getattr(resource, 'disable_oauth', None) is not True:
71+
for method in getattr(resource, 'methods', ('GET', 'POST', 'PATCH', 'DELETE')):
72+
scope = self.get_scope(resource, method)
73+
setattr(resource,
74+
method.lower(),
75+
oauth_manager.require_oauth(scope)(getattr(resource, method.lower())))
76+
77+
def scope_setter(self, func):
78+
"""Plug oauth scope setter function to the API
79+
80+
:param callable func: the callable to use a scope getter
81+
"""
82+
self.get_scope = func
83+
84+
@staticmethod
85+
def get_scope(resource, method):
86+
"""Compute the name of the scope for oauth
87+
88+
:param Resource resource: the resource manager
89+
:param str method: an http method
90+
:return str: the name of the scope
91+
"""
92+
if ResourceList in inspect.getmro(resource) and method == 'GET':
93+
prefix = 'list'
94+
else:
95+
method_to_prefix = {'GET': 'get',
96+
'POST': 'create',
97+
'PATCH': 'update',
98+
'DELETE': 'delete'}
99+
prefix = method_to_prefix[method]
100+
101+
return '_'.join([prefix, resource.schema.opts.type_])
102+
103+
def permission_manager(self, permission_manager):
104+
"""Use permission manager to enable permission for API
105+
106+
:param callable permission_manager: the permission manager
107+
"""
108+
self.check_permissions = permission_manager
109+
110+
for resource in self.resource_registry:
111+
if getattr(resource, 'disable_permission', None) is not True:
112+
for method in getattr(resource, 'methods', ('GET', 'POST', 'PATCH', 'DELETE')):
113+
setattr(resource,
114+
method.lower(),
115+
self.has_permission()(getattr(resource, method.lower())))
116+
117+
def has_permission(self, *args, **kwargs):
118+
"""Decorator used to check permissions before to call resource manager method
119+
"""
120+
def wrapper(view):
121+
@wraps(view)
122+
def decorated(*view_args, **view_kwargs):
123+
self.check_permissions(view, view_args, view_kwargs, *args, **kwargs)
124+
return view(*view_args, **view_kwargs)
125+
return decorated
126+
return wrapper
127+
128+
@staticmethod
129+
def check_permissions(view, view_args, view_kwargs, *args, **kwargs):
130+
"""The function use to check permissions
131+
132+
:param callable view: the view
133+
:param list view_args: view args
134+
:param dict view_kwargs: view kwargs
135+
:param list args: decorator args
136+
:param dict kwargs: decorator kwargs
137+
"""
138+
raise NotImplementedError

0 commit comments

Comments
 (0)