'''
Copyright 2013 Cosnita Radu Viorel
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
.. codeauthor:: Radu Viorel Cosnita <radu.cosnita@gmail.com>
.. py:module:: fantastico.oauth2.oauth2_decorators
'''
from fantastico.exceptions import FantasticoControllerInvalidError
from fantastico.mvc.base_controller import BaseController
from fantastico.oauth2.security_context import SecurityContext
import inspect
[docs]class RequiredScopes(object):
'''This class provides the decorator for enforcing fantastico to authorize requests against ROA resources and MVC controllers.
.. code-block:: python
# enforce authorization for MVC controllers.
@ControllerProvider()
class SecuredController(BaseController):
@RequiredScopes(scopes=["greet.verbose", "greet.read"])
@Controller(url="/secured-controller/ui/index")
def say_hello(self, request):
return "<html><body><h1>Hello world</body></html>"
.. code-block:: python
# enforce authorization for ROA resources.
@Resource(name="app-setting", url="/app-settings", version=1.0)
@RequiredScopes(create="app_setting.create",
read="app_setting.read",
update="app_setting.update",
delete="app_setting.delete"})
class AppSetting(BASEMODEL):
id = Column("id", Integer, primary_key=True, autoincrement=True)
name = Column("name", String(50), unique=True, nullable=False)
value = Column("value", Text, nullable=False)
def __init__(self, name, value):
self.name = name
self.value = value
'''
@property
[docs] def scopes(self):
'''This property returns the currently set scopes (including create, read, update, delete).'''
return self._scopes
@property
[docs] def create_scopes(self):
'''This property returns the scopes required for create calls.'''
return self._create_scopes
@property
[docs] def read_scopes(self):
'''This property returns the scopes required for read calls.'''
return self._read_scopes
@property
[docs] def update_scopes(self):
'''This property returns the scopes required for update calls.'''
return self._update_scopes
@property
[docs] def delete_scopes(self):
'''This property returns the scopes required for delete calls.'''
return self._delete_scopes
def __init__(self, scopes=None, create=None, read=None, update=None, delete=None):
self._scopes = set(self._get_list_from_param(scopes))
self._create_scopes = self._get_list_from_param(create)
self._read_scopes = self._get_list_from_param(read)
self._update_scopes = self._get_list_from_param(update)
self._delete_scopes = self._get_list_from_param(delete)
tmp_scopes = []
tmp_scopes.extend(self._create_scopes)
tmp_scopes.extend(self._read_scopes)
tmp_scopes.extend(self._update_scopes)
tmp_scopes.extend(self._delete_scopes)
for scope in tmp_scopes:
self._scopes.add(scope)
self._scopes = list(self._scopes)
self._scopes.sort()
def _get_list_from_param(self, param_value):
'''This method ensures param_value is a list. In case param value is string it is transformed to a list with
one element.'''
param_value = param_value or []
if isinstance(param_value, list):
return param_value
if isinstance(param_value, str):
return [param_value]
raise ValueError("param value %s is not supported for scopes." % param_value)
def __call__(self, orig_fn):
'''This method is invoked once when decorated function is wrapped.'''
if inspect.isfunction(orig_fn):
return self._handle_fn_decoration(orig_fn)
elif inspect.isclass(orig_fn):
return self._handle_class_decoration(orig_fn)
def _handle_class_decoration(self, orig_class):
'''This method appends get_required_scopes method to the given class in order to make it aware of mandatory scopes
which must be available when invoked.'''
orig_class.get_required_scopes = lambda inst = None: self
return orig_class
def _handle_fn_decoration(self, orig_fn):
'''This method decorates a given method and enrich it's behavior so that request security context is also aware of
required scopes.'''
def new_fn(*args, **kwargs):
'''This method replaces the original decorated method and it first injects security scopes into security context and
then it executes the original method.'''
request = self._get_request_from_args(args)
self.inject_scopes_in_security(request)
return orig_fn(*args, **kwargs)
new_fn.__name__ = orig_fn.__name__
new_fn.__doc__ = orig_fn.__doc__
new_fn.__module__ = orig_fn.__module__
return new_fn
def _get_request_from_args(self, args):
'''This method obtains the current request argument from a list of arguments.'''
try:
request = args[0]
if isinstance(request, BaseController):
request = args[1]
except IndexError as ex:
raise FantasticoControllerInvalidError(ex)
return request
[docs] def inject_scopes_in_security(self, request):
'''This method injects the request scopes into request security context.'''
security_ctx = request.context.security
request.context.security = SecurityContext(access_token=security_ctx.access_token,
required_scopes=self)