Skip to content

Commit

Permalink
refactor: add introductory type hints to flask-oso (#1606)
Browse files Browse the repository at this point in the history
* refactor: add introductory type hints to flask/oso

* fix: failing test

Co-authored-by: Sam Scott <[email protected]>
  • Loading branch information
Kevin Kirsche and samscott89 authored Aug 4, 2022
1 parent b87c967 commit 36b5ee7
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 24 deletions.
65 changes: 42 additions & 23 deletions languages/python/flask-oso/flask_oso/flask_oso.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from flask import g, current_app, request, Request
from flask import g, current_app, request, Flask
from flask.wrappers import Request, Response
from werkzeug.exceptions import Forbidden
from typing import Any, Callable, NoReturn, Optional

from oso import OsoError
from oso import Oso, OsoError

from .context import _app_context

Expand Down Expand Up @@ -34,11 +36,14 @@ class FlaskOso:
Call `authorize(resource=flask.request)` before every request.
"""

def __init__(self, oso=None, app=None):
_app: Optional[Flask]
_oso: Optional[Oso]

def __init__(self, oso: Optional[Oso] = None, app: Optional[Flask] = None) -> None:
self._app = app
self._oso = None

def unauthorized():
def unauthorized() -> NoReturn:
raise Forbidden("Unauthorized")

self._unauthorized_action = unauthorized
Expand All @@ -57,7 +62,7 @@ def unauthorized():

# Initialization

def set_oso(self, oso):
def set_oso(self, oso: Oso) -> None:
"""Set the oso instance to use for authorization
Must be called if ``oso`` is not provided to the constructor.
Expand All @@ -68,15 +73,15 @@ def set_oso(self, oso):
self._oso = oso
self._oso.register_class(Request)

def init_app(self, app):
def init_app(self, app: Flask) -> None:
"""Initialize ``app`` for use with this instance of ``FlaskOso``.
Must be called if ``app`` isn't provided to the constructor.
"""
app.teardown_appcontext(self.teardown)
app.before_request(self._provide_oso)

def set_get_actor(self, func):
def set_get_actor(self, func: Callable[[], Any]) -> None:
"""Provide a function that oso will use to get the current actor.
:param func: A function to call with no parameters to get the actor if
Expand All @@ -85,7 +90,7 @@ def set_get_actor(self, func):
"""
self._get_actor = func

def set_unauthorized_action(self, func):
def set_unauthorized_action(self, func: Callable[[], Any]) -> None:
"""Set a function that will be called to handle an authorization failure.
The default behavior is to raise a Forbidden exception, returning a 403
Expand All @@ -98,7 +103,7 @@ def set_unauthorized_action(self, func):

# Middleware-like functions that affect every request.

def require_authorization(self, app=None):
def require_authorization(self, app: Optional[Flask] = None) -> None:
"""Enforce authorization on every request to ``app``.
:param app: The app to require authorization for. Can be omitted if
Expand All @@ -112,11 +117,12 @@ def require_authorization(self, app=None):
request.
"""
if app is None:
app = self.app

app = self._app
if app is None:
raise OsoError("Cannot require authorization without Flask app object")
app.after_request(self._require_authorization)

def perform_route_authorization(self, app=None):
def perform_route_authorization(self, app: Optional[Flask] = None) -> None:
"""Perform route authorization before every request.
Route authorization will call :py:meth:`oso.Oso.is_allowed` with the
Expand All @@ -128,13 +134,17 @@ def perform_route_authorization(self, app=None):
constructor.
"""
if app is None:
app = self.app
app = self._app
if app is None:
raise OsoError(
"Cannot perform route authorization without Flask app object"
)

app.before_request(self._perform_route_authorization)

# During request decorator or functions.

def skip_authorization(self, reason=None):
def skip_authorization(self, reason: Optional[str] = None) -> None:
"""Opt-out of authorization for the current request.
Will prevent ``require_authorization`` from causing an error.
Expand All @@ -143,7 +153,13 @@ def skip_authorization(self, reason=None):
"""
_authorize_called()

def authorize(self, resource, *, actor=None, action=None):
def authorize(
self,
resource: Any,
*,
actor: Optional[Any] = None,
action: Optional[str] = None
) -> None:
"""Check whether the current request should be allowed.
Calls :py:meth:`oso.Oso.is_allowed` to check authorization. If a request
Expand Down Expand Up @@ -179,7 +195,10 @@ def authorize(self, resource, *, actor=None, action=None):
# We use *is* here because == would actually need to get the request object.
# We want to check that the resource is the proxy.
if resource is request:
resource = request._get_current_object()
resource = request._get_current_object() # type: ignore

if self.oso is None:
raise OsoError("Cannot perform authorization without oso instance")

allowed = self.oso.is_allowed(actor, action, resource)
_authorize_called()
Expand All @@ -188,32 +207,32 @@ def authorize(self, resource, *, actor=None, action=None):
self._unauthorized_action()

@property
def app(self):
def app(self) -> Flask:
return self._app or current_app

@property
def oso(self):
def oso(self) -> Optional[Oso]:
return self._oso

@property
def current_actor(self):
def current_actor(self) -> Any:
return self._get_actor()

# Before / after
def _provide_oso(self):
def _provide_oso(self) -> None:
top = _app_context()
if not hasattr(top, "oso_flask_oso"):
top.oso_flask_oso = self

def _perform_route_authorization(self):
def _perform_route_authorization(self) -> None:
if not request.url_rule:
# If request isn't going to match any route, just return and
# let flask handle it the normal way.
return

self.authorize(resource=request)

def _require_authorization(self, response):
def _require_authorization(self, response: Response) -> Response:
if not request.url_rule:
# No rule matched this request
# Skip requiring authorization.
Expand All @@ -230,6 +249,6 @@ def teardown(self, exception):
pass


def _authorize_called():
def _authorize_called() -> None:
"""Mark current request as authorized."""
_app_context().oso_flask_authorize_called = True
1 change: 1 addition & 0 deletions languages/python/flask-oso/requirements-test.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
types-setuptools==63.2.0
pytest==7.0.1
2 changes: 1 addition & 1 deletion languages/python/flask-oso/tests/test_flask_oso.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def test_initialization_with_set(flask_app, oso, simple_policy, app_ctx, user):
# Establish that an improperly initialized flask oso throws an exception.
flask_oso = FlaskOso()
flask_oso.set_get_actor(lambda: user)
with pytest.raises(AttributeError):
with pytest.raises(OsoError):
flask_oso.authorize(action="read", resource="resource")

# Works after set oso.
Expand Down

1 comment on commit 36b5ee7

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rust Benchmark

Benchmark suite Current: 36b5ee7 Previous: b87c967 Ratio
rust_get_attribute 42431 ns/iter (± 1167) 43702 ns/iter (± 2090) 0.97
n_plus_one/100 2253595 ns/iter (± 3460) 2189544 ns/iter (± 8845) 1.03
n_plus_one/500 10911978 ns/iter (± 57963) 10566177 ns/iter (± 143423) 1.03
n_plus_one/1000 21636707 ns/iter (± 38578) 21017645 ns/iter (± 190292) 1.03
unify_once 894 ns/iter (± 19) 952 ns/iter (± 55) 0.94
unify_twice 2512 ns/iter (± 45) 2513 ns/iter (± 76) 1.00
many_rules 61471 ns/iter (± 1189) 61931 ns/iter (± 1350) 0.99
fib/5 525785 ns/iter (± 7604) 525840 ns/iter (± 7124) 1.00
prime/3 17941 ns/iter (± 616) 17689 ns/iter (± 762) 1.01
prime/23 17946 ns/iter (± 555) 17692 ns/iter (± 760) 1.01
prime/43 17948 ns/iter (± 576) 17758 ns/iter (± 745) 1.01
prime/83 17972 ns/iter (± 550) 17714 ns/iter (± 711) 1.01
prime/255 16588 ns/iter (± 545) 16295 ns/iter (± 595) 1.02
indexed/100 5575 ns/iter (± 565) 5841 ns/iter (± 714) 0.95
indexed/500 6506 ns/iter (± 1491) 7584 ns/iter (± 1963) 0.86
indexed/1000 7750 ns/iter (± 209) 9802 ns/iter (± 633) 0.79
indexed/10000 19150 ns/iter (± 1804) 29900 ns/iter (± 2369) 0.64
not 6030 ns/iter (± 68) 5860 ns/iter (± 112) 1.03
double_not 12251 ns/iter (± 168) 12335 ns/iter (± 235) 0.99
De_Morgan_not 8032 ns/iter (± 167) 7905 ns/iter (± 169) 1.02
load_policy 894541 ns/iter (± 4180) 884978 ns/iter (± 86000) 1.01
partial_and/1 30630 ns/iter (± 1153) 31507 ns/iter (± 1332) 0.97
partial_and/5 105385 ns/iter (± 2594) 110677 ns/iter (± 3179) 0.95
partial_and/10 202383 ns/iter (± 5119) 211646 ns/iter (± 5007) 0.96
partial_and/20 418308 ns/iter (± 8065) 426141 ns/iter (± 6448) 0.98
partial_and/40 897847 ns/iter (± 11986) 910196 ns/iter (± 12807) 0.99
partial_and/80 2059431 ns/iter (± 15713) 2048950 ns/iter (± 6530) 1.01
partial_and/100 2734061 ns/iter (± 19677) 2718431 ns/iter (± 20598) 1.01
partial_rule_depth/1 95964 ns/iter (± 3317) 101920 ns/iter (± 4247) 0.94
partial_rule_depth/5 322003 ns/iter (± 7318) 334398 ns/iter (± 7753) 0.96
partial_rule_depth/10 712419 ns/iter (± 18269) 728037 ns/iter (± 12087) 0.98
partial_rule_depth/20 2032241 ns/iter (± 17542) 2044629 ns/iter (± 10676) 0.99
partial_rule_depth/40 7346611 ns/iter (± 60281) 7523956 ns/iter (± 131318) 0.98
partial_rule_depth/80 39463748 ns/iter (± 205679) 45838833 ns/iter (± 315023) 0.86
partial_rule_depth/100 71931113 ns/iter (± 382002) 85399316 ns/iter (± 566261) 0.84

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.