Skip to content

Commit

Permalink
Combine authorisation decorators
Browse files Browse the repository at this point in the history
  • Loading branch information
datamel committed Apr 15, 2021
1 parent 905b97a commit 428c8bc
Showing 1 changed file with 59 additions and 90 deletions.
149 changes: 59 additions & 90 deletions cylc/uiserver/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from jupyterhub.services.auth import HubOAuthenticated
from tornado import web, websocket
from tornado.ioloop import IOLoop
from functools import partial

from .websockets import authenticated as websockets_authenticated

Expand Down Expand Up @@ -61,7 +62,7 @@ def _user_action_allowed(username, action):
if roleuser and roleuser == username:
return True
# There's a number of ways to find groups but this is the only way
# to include non-local-machine groups while targetnig a specific user.
# to include non-local-machine groups while targeting a specific user.
# Requires a gid so supply 0 or some 'max integer' and remove from result
# see https://www.geeksforgeeks.org/python-os-getgrouplist-method/
group_ids = os.getgrouplist(username, 0)
Expand All @@ -72,99 +73,59 @@ def _user_action_allowed(username, action):
if rolegroup and rolegroup in users_groups:
return True

# TODO look to refactor can_* into a single function
#
# e.g.
#
# back to authorise decorator function that takes:
# authorise(can_read)
# which can be the function needed to do the checks


def can_read(fun):
if iscoroutinefunction(fun):
async def _inner(self, *args, **kwargs):
allowed, username = _can_read(self)
if not allowed:
logger.warn(f'Authorisation failed for {username}')
raise web.HTTPError(403)
return await fun(self, *args, **kwargs)
return _inner
else:
def _inner(self, *args, **kwargs):
allowed, username = _can_read(self)
if not allowed:
logger.warn(f'Authorisation failed for {username}')
raise web.HTTPError(403)
return fun(self, *args, **kwargs)
return _inner


def _can_read(self):
user = self.get_current_user()

def authorise(*outer_args, **outer_kwargs):
def authorise_inner(fun):
if iscoroutinefunction(fun):
async def decorated_func(self, *args, **kwargs):
for function in outer_args:
if function is callable():
allowed, username = function(handler=self)
if not allowed:
logger.warn(f'Authorisation failed for {username}')
raise web.HTTPError(403)
return await fun(self, *args, **kwargs)
return decorated_func
else:
def decorated_func(self):
for function in outer_args:
# todo raise / log
if function is callable():
allowed, username = function(handler=self)
if not allowed:
logger.warn(f'Authorisation failed for {username}')
raise web.HTTPError(403)
return fun(self)
return decorated_func
return authorise_inner


def can_read(**kwargs):
if "handler" not in kwargs:
return
user = kwargs["handler"].get_current_user()
username = user.get('name', '?')
if username == ME or _user_action_allowed(username, "readers"):
return True, username
return False, username


def can_write(fun):
if iscoroutinefunction(fun):
async def _inner(self, *args, **kwargs):
allowed, username = _can_write(self)
if not allowed:
logger.warn(f'Authorisation failed for {username}')
raise web.HTTPError(403)
return await fun(self, *args, **kwargs)
return _inner
else:
def _inner(self, *args, **kwargs):
allowed, username = _can_write(self)
if not allowed:
logger.warn(f'Authorisation failed for {username}')
raise web.HTTPError(403)
return fun(self, *args, **kwargs)
return _inner


def _can_write(self):
user = self.get_current_user()
def can_write(**kwargs):
if "handler" not in kwargs:
return
user = kwargs["handler"].get_current_user()
username = user.get('name', '?')
if username == ME or _user_action_allowed(username, "writers"):
return True, username
return False, username


def can_execute(mutators=None):
def can_execute_in(fun):
if iscoroutinefunction(fun):
async def _inner(self, *args, **kwargs):
allowed, username = _can_execute(self, mutators)
if not allowed:
logger.warn(f'Authorisation failed for {username}')
raise web.HTTPError(403)
return await fun(self, *args, **kwargs)
return _inner
else:
def _inner(self, *args, **kwargs):
allowed, username = _can_execute(self, mutators)
if not allowed:
logger.warn(f'Authorisation failed for {username}')
raise web.HTTPError(403)
return fun(self, *args, **kwargs)
return _inner
return can_execute_in


def _get_graphql_operation(req):
data = req.parse_body()
query, _, operation_name, _ = req.get_graphql_params(
req.request, data)
return operation_name, query


def _can_execute(req, mutators):
user = req.get_current_user()
def can_execute(**kwargs):
if "handler" not in kwargs or "mutators" not in kwargs:
return
handler = kwargs["handler"]
mutators = kwargs["mutators"]
user = handler.get_current_user()
username = user.get('name', '?')

if username == ME:
Expand All @@ -178,15 +139,23 @@ def _can_execute(req, mutators):
# Probably will mean all mutators needs execute?
# Same goes for where operation_name is missing as per first call
# Currently treating as if present below
# Main aim = fail secure

if isinstance(req, TornadoGraphQLHandler) and mutators is not None:
operation_name, query = _get_graphql_operation(req)
if isinstance(handler, TornadoGraphQLHandler) and mutators is not None:
operation_name, query = _get_graphql_operation(handler)
if operation_name in mutators or operation_name is None:
if _user_action_allowed(username, CAN_EXECUTE):
return True, username
return False, username


def _get_graphql_operation(req):
data = req.parse_body()
query, _, operation_name, _ = req.get_graphql_params(
req.request, data)
return operation_name, query


class BaseHandler(HubOAuthenticated, web.RequestHandler):

def set_default_headers(self) -> None:
Expand All @@ -209,7 +178,7 @@ def initialize(self, path):

@web.addslash
@web.authenticated
@can_read
@authorise(can_read)
def get(self):
"""Render the UI prototype."""
index = os.path.join(self._static, "index.html")
Expand All @@ -227,7 +196,7 @@ def set_default_headers(self) -> None:
self.set_header("Content-Type", 'application/json')

@web.authenticated
@can_read
@authorise(can_read)
def get(self):
self.write(json.dumps(self.get_current_user()))

Expand Down Expand Up @@ -282,8 +251,8 @@ async def execute(self, *args, **kwargs):
)

@web.authenticated
@can_write
@can_execute(mutators=["play", "stop"])
@authorise(can_write,
partial(can_execute, mutators=["play", "stop"]))
async def post(self) -> None:
try:
await super().run("post")
Expand All @@ -302,13 +271,13 @@ def select_subprotocol(self, subprotocols):
return GRAPHQL_WS

@websockets_authenticated
@can_read
@authorise(can_read)
def get(self, *args, **kwargs):
# forward this call so we can authenticate/authorise it
return websocket.WebSocketHandler.get(self, *args, **kwargs)

@websockets_authenticated
@can_read
@authorise(can_read)
def open(self, *args, **kwargs):
IOLoop.current().spawn_callback(self.subscription_server.handle, self,
self.context)
Expand Down

0 comments on commit 428c8bc

Please sign in to comment.