Skip to content

Commit

Permalink
[api] New, allow_browser_login option to enable cookie sessions
Browse files Browse the repository at this point in the history
  • Loading branch information
dpgaspar committed Mar 28, 2019
1 parent bcaef94 commit 017f0ea
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 36 deletions.
18 changes: 17 additions & 1 deletion docs/rest_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ Next, let's see how to create a private method::

...
@expose('/private')
@protect
@protect()
def rison_json(self):
return self.response(200, message="This is private")

Expand Down Expand Up @@ -350,6 +350,22 @@ user Model::
...
return user

Optionally you can enable signed cookie sessions (from flask-login) on the
API. You can do it class or method wide::

class MyFirstApi(BaseApi):
allow_browser_login = True

The previous example will enable cookie sessions on the all class::

class MyFirstApi(BaseApi):

@expose('/private')
@protect(allow_browser_login=True)
def private(self)
....

On the previous example, we are enabling signed cookies on the ``private`` method

Model REST Api
--------------
Expand Down
17 changes: 11 additions & 6 deletions flask_appbuilder/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,11 @@ class ExampleApi(BaseApi):
base_permissions = ['can_get']
"""
allow_browser_login = False
"""
Will allow flask-login cookie authorization on the API
default is False.
"""
extra_args = None

def __init__(self):
Expand Down Expand Up @@ -768,10 +773,10 @@ def merge_search_filters(self, response, **kwargs):
response[API_FILTERS_RES_KEY] = search_filters

@expose('/_info', methods=['GET'])
@protect()
@safe
@protect
@rison
@permission_name('get')
@permission_name('info')
@merge_response_func(BaseApi.merge_current_user_permissions, API_PERMISSIONS_RIS_KEY)
@merge_response_func(merge_add_field_info, API_ADD_COLUMNS_RIS_KEY)
@merge_response_func(merge_edit_field_info, API_EDIT_COLUMNS_RIS_KEY)
Expand All @@ -788,7 +793,7 @@ def info(self, **kwargs):

@expose('/', methods=['GET'])
@expose('/<pk>', methods=['GET'])
@protect
@protect()
@safe
@permission_name('get')
def get(self, pk=None):
Expand All @@ -797,7 +802,7 @@ def get(self, pk=None):
return self._get_item(pk)

@expose('/', methods=['POST'])
@protect
@protect()
@safe
@permission_name('post')
def post(self):
Expand Down Expand Up @@ -827,7 +832,7 @@ def post(self):
return self.response_400(message=str(e.orig))

@expose('/<pk>', methods=['PUT'])
@protect
@protect()
@safe
@permission_name('put')
def put(self, pk):
Expand Down Expand Up @@ -857,7 +862,7 @@ def put(self, pk):
return self.response_400(message=str(e.orig))

@expose('/<pk>', methods=['DELETE'])
@protect
@protect()
@safe
@permission_name('delete')
def delete(self, pk):
Expand Down
86 changes: 57 additions & 29 deletions flask_appbuilder/security/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,42 +17,60 @@
log = logging.getLogger(__name__)


def protect(f):
def protect(allow_browser_login=False):
"""
Use this decorator to enable granular security permissions
to your API methods (BaseApi and child classes).
Permissions will be associated to a role, and roles are associated to users.
allow_browser_login will accept signed cookies obtained from the normal MVC app::
class MyApi(BaseApi):
@expose('/dosonmething', methods=['GET'])
@protect(allow_browser_login=True)
@safe
def do_something(self):
....
@expose('/dosonmethingelse', methods=['GET'])
@protect()
@safe
def do_something_else(self):
....
By default the permission's name is the methods name.
"""
if hasattr(f, '_permission_name'):
permission_str = f._permission_name
else:
permission_str = f.__name__

def wraps(self, *args, **kwargs):
permission_str = "{}{}".format(PERMISSION_PREFIX, f._permission_name)
if current_app.appbuilder.sm.is_item_public(
permission_str,
self.__class__.__name__
):
return f(self, *args, **kwargs)
verify_jwt_in_request()
if current_app.appbuilder.sm.has_access(
permission_str,
self.__class__.__name__
):
return f(self, *args, **kwargs)
def _protect(f):
if hasattr(f, '_permission_name'):
permission_str = f._permission_name
else:
log.warning(
LOGMSG_ERR_SEC_ACCESS_DENIED.format(
permission_str = f.__name__

def wraps(self, *args, **kwargs):
permission_str = "{}{}".format(PERMISSION_PREFIX, f._permission_name)
if current_app.appbuilder.sm.is_item_public(
permission_str,
self.__class__.__name__
):
return f(self, *args, **kwargs)
if not (self.allow_browser_login or allow_browser_login):
verify_jwt_in_request()
if current_app.appbuilder.sm.has_access(
permission_str,
self.__class__.__name__
):
return f(self, *args, **kwargs)
else:
log.warning(
LOGMSG_ERR_SEC_ACCESS_DENIED.format(
permission_str,
self.__class__.__name__
)
)
)
return self.response_401()
f._permission_name = permission_str
return functools.update_wrapper(wraps, f)
return self.response_401()
f._permission_name = permission_str
return functools.update_wrapper(wraps, f)
return functools.update_wrapper(_protect, allow_browser_login)


def has_access(f):
Expand All @@ -76,10 +94,18 @@ def wraps(self, *args, **kwargs):
return f(self, *args, **kwargs)
else:
log.warning(
LOGMSG_ERR_SEC_ACCESS_DENIED.format(permission_str, self.__class__.__name__)
LOGMSG_ERR_SEC_ACCESS_DENIED.format(
permission_str,
self.__class__.__name__
)
)
flash(as_unicode(FLAMSG_ERR_SEC_ACCESS_DENIED), "danger")
return redirect(url_for(self.appbuilder.sm.auth_view.__class__.__name__ + ".login", next=request.url))
return redirect(
url_for(
self.appbuilder.sm.auth_view.__class__.__name__ + ".login",
next=request.url
)
)
f._permission_name = permission_str
return functools.update_wrapper(wraps, f)

Expand Down Expand Up @@ -107,7 +133,10 @@ def wraps(self, *args, **kwargs):
return f(self, *args, **kwargs)
else:
log.warning(
LOGMSG_ERR_SEC_ACCESS_DENIED.format(permission_str, self.__class__.__name__)
LOGMSG_ERR_SEC_ACCESS_DENIED.format(
permission_str,
self.__class__.__name__
)
)
response = make_response(
jsonify(
Expand All @@ -120,7 +149,6 @@ def wraps(self, *args, **kwargs):
)
response.headers['Content-Type'] = "application/json"
return response
return redirect(url_for(self.appbuilder.sm.auth_view.__class__.__name__ + ".login", next=request.url))
f._permission_name = permission_str
return functools.update_wrapper(wraps, f)

Expand Down

0 comments on commit 017f0ea

Please sign in to comment.