diff --git a/packages/flask-aserto/src/flask_aserto/__init__.py b/packages/flask-aserto/src/flask_aserto/__init__.py index 487b383..5f21f36 100644 --- a/packages/flask-aserto/src/flask_aserto/__init__.py +++ b/packages/flask-aserto/src/flask_aserto/__init__.py @@ -1,235 +1,5 @@ -from dataclasses import dataclass -from functools import wraps -from typing import Any, Callable, Optional, TypeVar, Union, cast, overload +from .middleware import AsertoMiddleware +from .check import CheckMiddleware, CheckOptions +from ._defaults import AuthorizationError -from aserto.client import AuthorizerOptions, Identity, IdentityType, ResourceContext -from aserto.client.authorizer import AuthorizerClient -from flask import Flask, jsonify -from flask.wrappers import Response - -from ._defaults import ( - DEFAULT_DISPLAY_STATE_MAP_ENDPOINT, - DEFAULT_RESOURCE_CONTEXT_PROVIDER_FOR_DISPLAY_STATE_MAP, - DEFAULT_RESOURCE_CONTEXT_PROVIDER_FOR_ENDPOINT, - IdentityMapper, - ResourceMapper, - StringMapper, - create_default_policy_path_resolver, -) - -Handler = TypeVar("Handler", bound=Callable[..., Any]) - - -@dataclass(frozen=True) -class AuthorizationError(Exception): - policy_instance_name: str - policy_path: str - - -class AsertoMiddleware: - def __init__( - self, - *, - authorizer_options: AuthorizerOptions, - policy_path_root: str, - identity_provider: IdentityMapper, - policy_instance_name: Optional[str] = None, - policy_instance_label: Optional[str] = None, - policy_path_resolver: Optional[StringMapper] = None, - resource_context_provider: Optional[ResourceMapper] = None, - ): - self._authorizer_options = authorizer_options - self._identity_provider = identity_provider - self._policy_instance_name = policy_instance_name - self._policy_instance_label = policy_instance_label - self._policy_path_root = policy_path_root - - self._policy_path_resolver = ( - policy_path_resolver - if policy_path_resolver is not None - else create_default_policy_path_resolver(policy_path_root) - ) - - self._resource_context_provider = ( - resource_context_provider - if resource_context_provider is not None - else DEFAULT_RESOURCE_CONTEXT_PROVIDER_FOR_ENDPOINT - ) - - def _generate_client(self) -> AuthorizerClient: - identity = self._identity_provider() - - return AuthorizerClient( - identity=identity, - options=self._authorizer_options, - ) - - def _with_overrides(self, **kwargs: Any) -> "AsertoMiddleware": - return ( - self - if not kwargs - else AsertoMiddleware( - authorizer_options=kwargs.get("authorizer", self._authorizer_options), - policy_path_root=kwargs.get("policy_path_root", self._policy_path_root), - identity_provider=kwargs.get("identity_provider", self._identity_provider), - policy_instance_name=kwargs.get("policy_instance_name", self._policy_instance_name), - policy_instance_label=kwargs.get( - "policy_instance_label", self._policy_instance_label - ), - policy_path_resolver=kwargs.get("policy_path_resolver", self._policy_path_resolver), - resource_context_provider=kwargs.get( - "resource_context_provider", self._resource_context_provider - ), - ) - ) - - @overload - def is_allowed(self, decision: str) -> bool: - ... - - @overload - def is_allowed( - self, - decision: str, - *, - authorizer_options: AuthorizerOptions = ..., - identity_provider: IdentityMapper = ..., - policy_instance_name: str = ..., - policy_instance_label: str = ..., - policy_path_root: str = ..., - policy_path_resolver: StringMapper = ..., - resource_context_provider: ResourceMapper = ..., - ) -> bool: - ... - - def is_allowed(self, decision: str, **kwargs: Any) -> bool: - return self._with_overrides(**kwargs)._is_allowed(decision) - - def _is_allowed(self, decision: str) -> bool: - client = self._generate_client() - resource_context = self._resource_context_provider() - policy_path = self._policy_path_resolver() - - decisions = client.decisions( - policy_path=policy_path, - decisions=(decision,), - policy_instance_name=self._policy_instance_name, - policy_instance_label=self._policy_instance_label, - resource_context=resource_context, - ) - return decisions[decision] - - @overload - def authorize(self, handler: Handler) -> Handler: - ... - - @overload - def authorize( - self, - *, - authorizer_options: AuthorizerOptions = ..., - identity_provider: IdentityMapper = ..., - policy_instance_name: str = ..., - policy_instance_label: str = ..., - policy_path_root: str = ..., - policy_path_resolver: StringMapper = ..., - ) -> Callable[[Handler], Handler]: - ... - - def authorize( # type: ignore[misc] - self, - *args: Any, - **kwargs: Any, - ) -> Union[Handler, Callable[[Handler], Handler]]: - arguments_error = TypeError( - f"{self.authorize.__name__}() expects either exactly 1 callable" - " 'handler' argument or at least 1 'options' argument" - ) - - handler: Optional[Handler] = None - - if not args and kwargs.keys() == {"handler"}: - handler = kwargs["handler"] - elif not kwargs and len(args) == 1: - (handler,) = args - - if handler is not None: - if not callable(handler): - raise arguments_error - return self._authorize(handler) - - if args: - raise arguments_error - - return self._with_overrides(**kwargs)._authorize - - def _authorize(self, handler: Handler) -> Handler: - if self._policy_instance_name == None: - raise TypeError(f"{self._policy_instance_name}() should not be None") - - if self._policy_instance_label == None: - self._policy_instance_label = self._policy_instance_name - - @wraps(handler) - def decorated(*args: Any, **kwargs: Any) -> Response: - client = self._generate_client() - resource_context = self._resource_context_provider() - policy_path = self._policy_path_resolver() - - decisions = client.decisions( - policy_path=policy_path, - decisions=("allowed",), - policy_instance_name=self._policy_instance_name, - policy_instance_label=self._policy_instance_label, - resource_context=resource_context, - ) - - if not decisions["allowed"]: - raise AuthorizationError(policy_instance_name=self._policy_instance_name, policy_path=policy_path) # type: ignore[arg-type] - - return handler(*args, **kwargs) - - return cast(Handler, decorated) - - def register_display_state_map( - self, - app: Flask, - *, - endpoint: str = DEFAULT_DISPLAY_STATE_MAP_ENDPOINT, - resource_context_provider: Optional[ResourceMapper] = None, - ) -> Flask: - @app.route(endpoint, methods=["GET", "POST"]) - def __displaystatemap() -> Response: - nonlocal resource_context_provider - if resource_context_provider is None: - resource_context_provider = DEFAULT_RESOURCE_CONTEXT_PROVIDER_FOR_DISPLAY_STATE_MAP - - client = self._generate_client() - resource_context = resource_context_provider() - - display_state_map = client.decision_tree( - policy_path_root=self._policy_path_root, - decisions=["visible", "enabled"], - policy_instance_name=self._policy_instance_name, - policy_instance_label=self._policy_instance_label, - resource_context=resource_context, - policy_path_separator="SLASH", - ) - return jsonify(display_state_map) - - return app - - -__all__ = [ - "AsertoMiddleware", - "AuthorizationError", - "AuthorizerClient", - "AuthorizerOptions", - "Handler", - "Identity", - "IdentityMapper", - "IdentityType", - "ResourceContext", - "ResourceMapper", - "StringMapper", -] +__all__ = ["AsertoMiddleware", "AuthorizationError", "CheckMiddleware", "CheckOptions"] diff --git a/packages/flask-aserto/src/flask_aserto/_defaults.py b/packages/flask-aserto/src/flask_aserto/_defaults.py index b06fedc..0f2a18e 100644 --- a/packages/flask-aserto/src/flask_aserto/_defaults.py +++ b/packages/flask-aserto/src/flask_aserto/_defaults.py @@ -1,5 +1,6 @@ import re -from typing import Callable +from dataclasses import dataclass +from typing import Callable, TypeVar, Any from aserto.client import Identity, ResourceContext from flask import request @@ -10,11 +11,26 @@ "DEFAULT_RESOURCE_CONTEXT_PROVIDER_FOR_DISPLAY_STATE_MAP", ] +@dataclass +class Obj: + id: str + objType: str + + IdentityMapper = Callable[[], Identity] StringMapper = Callable[[], str] +ObjectMapper = Callable[[], Obj] ResourceMapper = Callable[[], ResourceContext] DEFAULT_DISPLAY_STATE_MAP_ENDPOINT = "/__displaystatemap" +@dataclass(frozen=True) +class AuthorizationError(Exception): + policy_instance_name: str + policy_path: str + + +Handler = TypeVar("Handler", bound=Callable[..., Any]) + def DEFAULT_RESOURCE_CONTEXT_PROVIDER_FOR_ENDPOINT() -> ResourceContext: return request.view_args or {} diff --git a/packages/flask-aserto/src/flask_aserto/aio/__init__.py b/packages/flask-aserto/src/flask_aserto/aio/__init__.py index d7b0e70..974bdba 100644 --- a/packages/flask-aserto/src/flask_aserto/aio/__init__.py +++ b/packages/flask-aserto/src/flask_aserto/aio/__init__.py @@ -1,244 +1,6 @@ -from asyncio import gather -from dataclasses import dataclass -from functools import wraps -from typing import Any, Awaitable, Callable, Optional, TypeVar, Union, cast, overload +from .middleware import AsertoMiddleware +from .check import CheckMiddleware, CheckOptions +from ._defaults import AuthorizationError -from aserto.client import AuthorizerOptions, Identity, IdentityType, ResourceContext -from aserto.client.authorizer.aio import AuthorizerClient -from flask import Flask, jsonify -from flask.wrappers import Response -from ._defaults import ( - DEFAULT_DISPLAY_STATE_MAP_ENDPOINT, - DEFAULT_RESOURCE_CONTEXT_PROVIDER_FOR_DISPLAY_STATE_MAP, - DEFAULT_RESOURCE_CONTEXT_PROVIDER_FOR_ENDPOINT, - IdentityMapper, - ResourceMapper, - StringMapper, - create_default_policy_path_resolver, -) - - -@dataclass(frozen=True) -class AuthorizationError(Exception): - policy_instance_name: str - policy_path: str - - -Handler = TypeVar("Handler", bound=Callable[..., Awaitable[Any]]) - - -class AsertoMiddleware: - def __init__( - self, - *, - authorizer_options: AuthorizerOptions, - policy_path_root: str, - identity_provider: IdentityMapper, - policy_instance_name: Optional[str] = None, - policy_instance_label: Optional[str] = None, - policy_path_resolver: Optional[StringMapper] = None, - resource_context_provider: Optional[ResourceMapper] = None, - ) -> None: - self._authorizer_options = authorizer_options - self._identity_provider = identity_provider - self._policy_instance_name = policy_instance_name - self._policy_instance_label = policy_instance_label - self._policy_path_root = policy_path_root - - self._policy_path_resolver = ( - policy_path_resolver - if policy_path_resolver is not None - else create_default_policy_path_resolver(policy_path_root) - ) - - self._resource_context_provider = ( - resource_context_provider - if resource_context_provider is not None - else DEFAULT_RESOURCE_CONTEXT_PROVIDER_FOR_ENDPOINT() - ) - - async def _generate_client(self) -> AuthorizerClient: - identity = await self._identity_provider() - - return AuthorizerClient( - identity=identity, - options=self._authorizer_options, - ) - - def _with_overrides(self, **kwargs: Any) -> "AsertoMiddleware": - return ( - self - if not kwargs - else AsertoMiddleware( - authorizer_options=kwargs.get("authorizer", self._authorizer_options), - policy_path_root=kwargs.get("policy_path_root", self._policy_path_root), - identity_provider=kwargs.get("identity_provider", self._identity_provider), - policy_instance_name=kwargs.get("policy_instance_name", self._policy_instance_name), - policy_instance_label=kwargs.get( - "policy_instance_label", self._policy_instance_label - ), - policy_path_resolver=kwargs.get("policy_path_resolver", self._policy_path_resolver), - resource_context_provider=kwargs.get( - "resource_context_provider", self._resource_context_provider - ), - ) - ) - - @overload - async def is_allowed(self, decision: str) -> bool: - ... - - @overload - async def is_allowed( - self, - decision: str, - *, - authorizer_options: AuthorizerOptions = ..., - identity_provider: IdentityMapper = ..., - policy_instance_name: str = ..., - policy_instance_label: str = ..., - policy_path_root: str = ..., - policy_path_resolver: StringMapper = ..., - resource_context_provider: ResourceContext = ..., - ) -> bool: - ... - - async def is_allowed(self, decision: str, **kwargs: Any) -> bool: - return await self._with_overrides(**kwargs)._is_allowed(decision) - - async def _is_allowed(self, decision: str) -> bool: - client = await self._generate_client() - resource_context = await self._resource_context_provider() - policy_path = await self._policy_path_resolver() - - decisions = await client.decisions( - policy_path=policy_path, - decisions=(decision,), - policy_instance_name=self._policy_instance_name, - policy_instance_label=self._policy_instance_label, - resource_context=resource_context, - ) - return decisions[decision] - - @overload - async def authorize(self, handler: Handler) -> Handler: - ... - - @overload - async def authorize( - self, - *, - authorizer_options: AuthorizerOptions = ..., - identity_provider: IdentityMapper = ..., - policy_instance_name: str = ..., - policy_instance_label: str = ..., - policy_path_root: str = ..., - policy_path_resolver: StringMapper = ..., - ) -> Callable[[Handler], Handler]: - ... - - async def authorize( - self, - *args: Any, - **kwargs: Any, - ) -> Union[Handler, Callable[[Handler], Handler]]: - arguments_error = TypeError( - f"{self.authorize.__name__}() expects either exactly 1 callable" - " 'handler' argument or at least 1 'options' argument" - ) - - handler: Optional[Handler] = None - - if not args and kwargs.keys() == {"handler"}: - handler = kwargs["handler"] - elif not kwargs and len(args) == 1: - (handler,) = args - - if handler is not None: - if not callable(handler): - raise arguments_error - return self._authorize(handler) - - if args: - raise arguments_error - - return self._with_overrides(**kwargs)._authorize - - def _authorize(self, handler: Handler) -> Handler: - if self._policy_instance_name == None: - raise TypeError(f"{self._policy_instance_name}() should not be None") - - if self._policy_instance_label == None: - self._policy_instance_label = self._policy_instance_name - - @wraps(handler) - async def decorated(*args: Any, **kwargs: Any) -> Response: - client, policy_path, resource_context = await gather( - self._generate_client(), - self._policy_path_resolver(), - self._resource_context_provider(), - ) - - decisions = await client.decisions( - policy_path=policy_path, - decisions=("allowed",), - policy_instance_name=self._policy_instance_name, - policy_instance_label=self._policy_instance_label, - resource_context=resource_context, - ) - - if not decisions["allowed"]: - raise AuthorizationError( - policy_instance_name=self._policy_instance_name or "", policy_path=policy_path - ) - - return await handler(*args, **kwargs) - - return cast(Handler, decorated) - - def register_display_state_map( - self, - app: Flask, - *, - endpoint: str = DEFAULT_DISPLAY_STATE_MAP_ENDPOINT, - resource_context_provider: Optional[ResourceMapper] = None, - ) -> Flask: - @app.route(endpoint, methods=["GET", "POST"]) - async def __displaystatemap() -> Response: - nonlocal resource_context_provider - if resource_context_provider is None: - resource_context_provider = ( - DEFAULT_RESOURCE_CONTEXT_PROVIDER_FOR_DISPLAY_STATE_MAP() - ) - - client, resource_context = await gather( - self._generate_client(), - resource_context_provider(), - ) - - display_state_map = await client.decision_tree( - policy_path_root=self._policy_path_root, - decisions=["visible", "enabled"], - policy_instance_name=self._policy_instance_name, - policy_instance_label=self._policy_instance_label, - resource_context=resource_context, - policy_path_separator="SLASH", - ) - return jsonify(display_state_map) - - return app - - -__all__ = [ - "AsertoMiddleware", - "AuthorizationError", - "AuthorizerClient", - "AuthorizerOptions", - "Handler", - "Identity", - "IdentityMapper", - "IdentityType", - "ResourceMapper", - "StringMapper", -] +__all__ = ["AsertoMiddleware", "AuthorizationError", "CheckMiddleware", "CheckOptions"] diff --git a/packages/flask-aserto/src/flask_aserto/aio/_defaults.py b/packages/flask-aserto/src/flask_aserto/aio/_defaults.py index 9742617..8c6b24d 100644 --- a/packages/flask-aserto/src/flask_aserto/aio/_defaults.py +++ b/packages/flask-aserto/src/flask_aserto/aio/_defaults.py @@ -1,5 +1,6 @@ import re -from typing import Awaitable, Callable +from dataclasses import dataclass +from typing import Awaitable, Callable, Any, TypeVar from aserto.client import Identity, ResourceContext from flask import request @@ -12,10 +13,25 @@ ] +@dataclass +class Obj: + id: str + objType: str + +@dataclass(frozen=True) +class AuthorizationError(Exception): + policy_instance_name: str + policy_path: str + + +Handler = TypeVar("Handler", bound=Callable[..., Awaitable[Any]]) + + DEFAULT_DISPLAY_STATE_MAP_ENDPOINT = "/__displaystatemap" IdentityMapper = Callable[[], Awaitable[Identity]] StringMapper = Callable[[], Awaitable[str]] +ObjectMapper = Callable[[], Awaitable[Obj]] ResourceMapper = Callable[[], Awaitable[ResourceContext]] diff --git a/packages/flask-aserto/src/flask_aserto/aio/check.py b/packages/flask-aserto/src/flask_aserto/aio/check.py new file mode 100644 index 0000000..8599585 --- /dev/null +++ b/packages/flask-aserto/src/flask_aserto/aio/check.py @@ -0,0 +1,192 @@ +from dataclasses import dataclass +from functools import wraps +from typing import Any, Callable, Optional, Union, cast, TYPE_CHECKING +if TYPE_CHECKING: + from .middleware import AsertoMiddleware + +from aserto.client import ResourceContext +from flask.wrappers import Response + +from ._defaults import ( + IdentityMapper, + StringMapper, + ResourceMapper, + ObjectMapper, + Obj, + AuthorizationError, + Handler +) + +@dataclass(frozen=True) +class CheckOptions: + """ + Check options class used to create a new instance of Check Middleware + """ + objId: Optional[str] = "" + objType: Optional[str] = "" + objIdMapper: Optional[StringMapper] = None + objMapper: Optional[ObjectMapper] = None + relationName: Optional[str] = "" + relationMapper: Optional[StringMapper] = None + subjType: Optional[str] = "" + subjMapper: Optional[IdentityMapper] = None + policyPath: Optional[str] = "" + policyPathMapper: Optional[StringMapper] = None + + + +def build_resource_context_mapper( + opts: CheckOptions +) -> ResourceMapper: + + async def resource() -> ResourceContext: + objid = ( + opts.objId + if opts.objId is not None + else "" + ) + objtype = ( + opts.objType + if opts.objType is not None + else "" + ) + + obj = ( + await opts.objMapper() + if opts.objMapper is not None + else Obj(id=objid, objType=objtype) + ) + + obj.id = ( + await opts.objIdMapper() + if opts.objIdMapper is not None + else obj.id + ) + + relation = ( + await opts.relationMapper() + if opts.relationMapper is not None + else opts.relationName + ) + + subjType = ( + opts.subjType + if opts.subjType != "" + else "user" + ) + + return {"relation": relation, + "object_type": obj.objType, + "object_id": obj.id, + "subject_type": subjType} + + return resource + +class CheckMiddleware: + def __init__( + self, + *, + options: CheckOptions, + aserto_middleware: "AsertoMiddleware", + ): + self._aserto_middleware = aserto_middleware + + self._identity_provider = ( + options.subjMapper + if options.subjMapper is not None + else aserto_middleware._identity_provider + ) + + self._resource_context_provider = build_resource_context_mapper(options) + self._options = options + + def _with_overrides(self, **kwargs: Any) -> "CheckMiddleware": + return ( + self + if not kwargs + else CheckMiddleware( + aserto_middleware=self._aserto_middleware, + options = CheckOptions( + relationName=kwargs.get("relation_name", self._options.relationName), + relationMapper=kwargs.get("relation_mapper", self._options.relationMapper), + policyPath=kwargs.get("policy_path", self._options.policyPath), + subjMapper=kwargs.get("identity_provider", self._identity_provider), + objId=kwargs.get("object_id", self._options.objId), + objType=kwargs.get("object_type", self._options.objType), + objIdMapper=kwargs.get("object_id_mapper", self._options.objIdMapper), + objMapper=kwargs.get("object_mapper", self._options.objMapper), + subjType=self._options.subjType, + policyPathMapper=self._options.policyPathMapper, + ), + ) + ) + + def _build_policy_path_mapper(self) -> StringMapper: + async def mapper() -> str: + policy_path = "" + if self._options.policyPathMapper is not None: + policy_path = await self._options.policyPathMapper() + if policy_path == "": + policy_path = "check" + if self._aserto_middleware._policy_path_root != "": + policy_path = self._aserto_middleware._policy_path_root + "." + policy_path + return policy_path + + return mapper + + async def authorize( + self, + *args: Any, + **kwargs: Any, + ) -> Union[Handler, Callable[[Handler], Handler]]: + arguments_error = TypeError( + f"{self.authorize.__name__}() expects either exactly 1 callable" + " 'handler' argument or at least 1 'options' argument" + ) + + handler: Optional[Handler] = None + + if not args and kwargs.keys() == {"handler"}: + handler = kwargs["handler"] + elif not kwargs and len(args) == 1: + (handler,) = args + + if handler is not None: + if not callable(handler): + raise arguments_error + return self._authorize(handler) + + if args: + raise arguments_error + + return self._with_overrides(**kwargs)._authorize + + def _authorize(self, handler: Handler) -> Handler: + if self._aserto_middleware._policy_instance_name == None: + raise TypeError(f"{self._aserto_middleware._policy_instance_name}() should not be None") + + if self._aserto_middleware._policy_instance_label == None: + self._aserto_middleware._policy_instance_label = self._aserto_middleware._policy_instance_name + + @wraps(handler) + async def decorated(*args: Any, **kwargs: Any) -> Response: + + policy_mapper = self._build_policy_path_mapper() + resource_context = await self._resource_context_provider() + decision = await self._aserto_middleware.is_allowed( + decision="allowed", + authorizer_options=self._aserto_middleware._authorizer_options, + identity_provider=self._identity_provider, + policy_instance_name=self._aserto_middleware._policy_instance_name or "", + policy_instance_label=self._aserto_middleware._policy_instance_label or "", + policy_path_root=self._aserto_middleware._policy_path_root, + policy_path_resolver=policy_mapper, + resource_context_provider=resource_context, + ) + + if not decision: + raise AuthorizationError(policy_instance_name=self._aserto_middleware._policy_instance_name, policy_path=policy_mapper()) # type: ignore[arg-type] + + return await handler(*args, **kwargs) + + return cast(Handler, decorated) \ No newline at end of file diff --git a/packages/flask-aserto/src/flask_aserto/aio/middleware.py b/packages/flask-aserto/src/flask_aserto/aio/middleware.py new file mode 100644 index 0000000..9adbfeb --- /dev/null +++ b/packages/flask-aserto/src/flask_aserto/aio/middleware.py @@ -0,0 +1,245 @@ +from asyncio import gather +from functools import wraps +from typing import Any, Callable, Optional, Union, cast, overload + +from aserto.client import AuthorizerOptions, ResourceContext +from aserto.client.authorizer.aio import AuthorizerClient +from flask import Flask, jsonify +from flask.wrappers import Response + +from .check import CheckMiddleware, CheckOptions + +from ._defaults import ( + DEFAULT_DISPLAY_STATE_MAP_ENDPOINT, + DEFAULT_RESOURCE_CONTEXT_PROVIDER_FOR_DISPLAY_STATE_MAP, + DEFAULT_RESOURCE_CONTEXT_PROVIDER_FOR_ENDPOINT, + IdentityMapper, + ResourceMapper, + StringMapper, + create_default_policy_path_resolver, + Handler, + ObjectMapper, + AuthorizationError +) + + +class AsertoMiddleware: + def __init__( + self, + *, + authorizer_options: AuthorizerOptions, + policy_path_root: str, + identity_provider: IdentityMapper, + policy_instance_name: Optional[str] = None, + policy_instance_label: Optional[str] = None, + policy_path_resolver: Optional[StringMapper] = None, + resource_context_provider: Optional[ResourceMapper] = None, + ) -> None: + self._authorizer_options = authorizer_options + self._identity_provider = identity_provider + self._policy_instance_name = policy_instance_name + self._policy_instance_label = policy_instance_label + self._policy_path_root = policy_path_root + + self._policy_path_resolver = ( + policy_path_resolver + if policy_path_resolver is not None + else create_default_policy_path_resolver(policy_path_root) + ) + + self._resource_context_provider = ( + resource_context_provider + if resource_context_provider is not None + else DEFAULT_RESOURCE_CONTEXT_PROVIDER_FOR_ENDPOINT() + ) + + async def _generate_client(self) -> AuthorizerClient: + identity = await self._identity_provider() + + return AuthorizerClient( + identity=identity, + options=self._authorizer_options, + ) + + def _with_overrides(self, **kwargs: Any) -> "AsertoMiddleware": + return ( + self + if not kwargs + else AsertoMiddleware( + authorizer_options=kwargs.get("authorizer", self._authorizer_options), + policy_path_root=kwargs.get("policy_path_root", self._policy_path_root), + identity_provider=kwargs.get("identity_provider", self._identity_provider), + policy_instance_name=kwargs.get("policy_instance_name", self._policy_instance_name), + policy_instance_label=kwargs.get( + "policy_instance_label", self._policy_instance_label + ), + policy_path_resolver=kwargs.get("policy_path_resolver", self._policy_path_resolver), + resource_context_provider=kwargs.get( + "resource_context_provider", self._resource_context_provider + ), + ) + ) + + @overload + async def is_allowed(self, decision: str) -> bool: + ... + + @overload + async def is_allowed( + self, + decision: str, + *, + authorizer_options: AuthorizerOptions = ..., + identity_provider: IdentityMapper = ..., + policy_instance_name: str = ..., + policy_instance_label: str = ..., + policy_path_root: str = ..., + policy_path_resolver: StringMapper = ..., + resource_context_provider: ResourceContext = ..., + ) -> bool: + ... + + async def is_allowed(self, decision: str, **kwargs: Any) -> bool: + return await self._with_overrides(**kwargs)._is_allowed(decision) + + async def _is_allowed(self, decision: str) -> bool: + client = await self._generate_client() + resource_context = await self._resource_context_provider() + policy_path = await self._policy_path_resolver() + + decisions = await client.decisions( + policy_path=policy_path, + decisions=(decision,), + policy_instance_name=self._policy_instance_name, + policy_instance_label=self._policy_instance_label, + resource_context=resource_context, + ) + return decisions[decision] + + @overload + async def authorize(self, handler: Handler) -> Handler: + ... + + @overload + async def authorize( + self, + *, + authorizer_options: AuthorizerOptions = ..., + identity_provider: IdentityMapper = ..., + policy_instance_name: str = ..., + policy_instance_label: str = ..., + policy_path_root: str = ..., + policy_path_resolver: StringMapper = ..., + ) -> Callable[[Handler], Handler]: + ... + + async def authorize( + self, + *args: Any, + **kwargs: Any, + ) -> Union[Handler, Callable[[Handler], Handler]]: + arguments_error = TypeError( + f"{self.authorize.__name__}() expects either exactly 1 callable" + " 'handler' argument or at least 1 'options' argument" + ) + + handler: Optional[Handler] = None + + if not args and kwargs.keys() == {"handler"}: + handler = kwargs["handler"] + elif not kwargs and len(args) == 1: + (handler,) = args + + if handler is not None: + if not callable(handler): + raise arguments_error + return self._authorize(handler) + + if args: + raise arguments_error + + return self._with_overrides(**kwargs)._authorize + + def _authorize(self, handler: Handler) -> Handler: + if self._policy_instance_name == None: + raise TypeError(f"{self._policy_instance_name}() should not be None") + + if self._policy_instance_label == None: + self._policy_instance_label = self._policy_instance_name + + @wraps(handler) + async def decorated(*args: Any, **kwargs: Any) -> Response: + client, policy_path, resource_context = await gather( + self._generate_client(), + self._policy_path_resolver(), + self._resource_context_provider(), + ) + + decisions = await client.decisions( + policy_path=policy_path, + decisions=("allowed",), + policy_instance_name=self._policy_instance_name, + policy_instance_label=self._policy_instance_label, + resource_context=resource_context, + ) + + if not decisions["allowed"]: + raise AuthorizationError( + policy_instance_name=self._policy_instance_name or "", policy_path=policy_path + ) + + return await handler(*args, **kwargs) + + return cast(Handler, decorated) + + def check( + self, + objId: Optional[str] = "", + objType: Optional[str] = "", + objIdMapper: Optional[StringMapper] = None, + objMapper: Optional[ObjectMapper] = None, + relationName: Optional[str] = "", + relationMapper: Optional[StringMapper] = None, + subjType: Optional[str] = "", + subjMapper: Optional[IdentityMapper] = None, + policyPath: Optional[str] = "", + policyPathMapper: Optional[StringMapper] = None, + ) -> CheckMiddleware: + opts = CheckOptions( + objId=objId, objType=objType,objIdMapper=objIdMapper, + objMapper=objMapper, relationName=relationName, relationMapper=relationMapper, + subjType=subjType, subjMapper=subjMapper, policyPath=policyPath, policyPathMapper=policyPathMapper) + return CheckMiddleware(options=opts, aserto_middleware=self) + + def register_display_state_map( + self, + app: Flask, + *, + endpoint: str = DEFAULT_DISPLAY_STATE_MAP_ENDPOINT, + resource_context_provider: Optional[ResourceMapper] = None, + ) -> Flask: + @app.route(endpoint, methods=["GET", "POST"]) + async def __displaystatemap() -> Response: + nonlocal resource_context_provider + if resource_context_provider is None: + + resource_context_provider = ( + DEFAULT_RESOURCE_CONTEXT_PROVIDER_FOR_DISPLAY_STATE_MAP() + ) + + client, resource_context = await gather( + self._generate_client(), + resource_context_provider(), + ) + + display_state_map = await client.decision_tree( + policy_path_root=self._policy_path_root, + decisions=["visible", "enabled"], + policy_instance_name=self._policy_instance_name, + policy_instance_label=self._policy_instance_label, + resource_context=resource_context, + policy_path_separator="SLASH", + ) + return jsonify(display_state_map) + + return app diff --git a/packages/flask-aserto/src/flask_aserto/check.py b/packages/flask-aserto/src/flask_aserto/check.py new file mode 100644 index 0000000..1c15cd1 --- /dev/null +++ b/packages/flask-aserto/src/flask_aserto/check.py @@ -0,0 +1,191 @@ +from dataclasses import dataclass +from functools import wraps +from typing import Any, Callable, Optional, Union, cast, TYPE_CHECKING +if TYPE_CHECKING: + from .middleware import AsertoMiddleware + +from aserto.client import ResourceContext +from flask.wrappers import Response + +from ._defaults import ( + IdentityMapper, + StringMapper, + ResourceMapper, + ObjectMapper, + Obj, + AuthorizationError, + Handler +) + +@dataclass(frozen=True) +class CheckOptions: + """ + Check options class used to create a new instance of Check Middleware + """ + objId: Optional[str] = "" + objType: Optional[str] = "" + objIdMapper: Optional[StringMapper] = None + objMapper: Optional[ObjectMapper] = None + relationName: Optional[str] = "" + relationMapper: Optional[StringMapper] = None + subjType: Optional[str] = "" + subjMapper: Optional[IdentityMapper] = None + policyPath: Optional[str] = "" + policyPathMapper: Optional[StringMapper] = None + + + +def build_resource_context_mapper( + opts: CheckOptions +) -> ResourceMapper: + + def resource() -> ResourceContext: + objid = ( + opts.objId + if opts.objId is not None + else "" + ) + objtype = ( + opts.objType + if opts.objType is not None + else "" + ) + + obj = ( + opts.objMapper() + if opts.objMapper is not None + else Obj(id=objid, objType=objtype) + ) + + obj.id = ( + opts.objIdMapper() + if opts.objIdMapper is not None + else obj.id + ) + + relation = ( + opts.relationMapper() + if opts.relationMapper is not None + else opts.relationName + ) + + subjType = ( + opts.subjType + if opts.subjType != "" + else "user" + ) + + return {"relation": relation, + "object_type": obj.objType, + "object_id": obj.id, + "subject_type": subjType} + + return resource + +class CheckMiddleware: + def __init__( + self, + *, + options: CheckOptions, + aserto_middleware: "AsertoMiddleware", + ): + self._aserto_middleware = aserto_middleware + + self._identity_provider = ( + options.subjMapper + if options.subjMapper is not None + else aserto_middleware._identity_provider + ) + + self._resource_context_provider = build_resource_context_mapper(options) + self._options = options + + def _with_overrides(self, **kwargs: Any) -> "CheckMiddleware": + return ( + self + if not kwargs + else CheckMiddleware( + aserto_middleware=self._aserto_middleware, + options = CheckOptions( + relationName=kwargs.get("relation_name", self._options.relationName), + relationMapper=kwargs.get("relation_mapper", self._options.relationMapper), + policyPath=kwargs.get("policy_path", self._options.policyPath), + subjMapper=kwargs.get("identity_provider", self._identity_provider), + objId=kwargs.get("object_id", self._options.objId), + objType=kwargs.get("object_type", self._options.objType), + objIdMapper=kwargs.get("object_id_mapper", self._options.objIdMapper), + objMapper=kwargs.get("object_mapper", self._options.objMapper), + subjType=self._options.subjType, + policyPathMapper=self._options.policyPathMapper, + ), + ) + ) + + def _build_policy_path_mapper(self) -> StringMapper: + def mapper() -> str: + policy_path = "" + if self._options.policyPathMapper is not None: + policy_path = self._options.policyPathMapper() + if policy_path == "": + policy_path = "check" + if self._aserto_middleware._policy_path_root != "": + policy_path = self._aserto_middleware._policy_path_root + "." + policy_path + return policy_path + + return mapper + + def authorize( + self, + *args: Any, + **kwargs: Any, + ) -> Union[Handler, Callable[[Handler], Handler]]: + arguments_error = TypeError( + f"{self.authorize.__name__}() expects either exactly 1 callable" + " 'handler' argument or at least 1 'options' argument" + ) + + handler: Optional[Handler] = None + + if not args and kwargs.keys() == {"handler"}: + handler = kwargs["handler"] + elif not kwargs and len(args) == 1: + (handler,) = args + + if handler is not None: + if not callable(handler): + raise arguments_error + return self._authorize(handler) + + if args: + raise arguments_error + + return self._with_overrides(**kwargs)._authorize + + def _authorize(self, handler: Handler) -> Handler: + if self._aserto_middleware._policy_instance_name == None: + raise TypeError(f"{self._aserto_middleware._policy_instance_name}() should not be None") + + if self._aserto_middleware._policy_instance_label == None: + self._aserto_middleware._policy_instance_label = self._aserto_middleware._policy_instance_name + + @wraps(handler) + def decorated(*args: Any, **kwargs: Any) -> Response: + + policy_mapper = self._build_policy_path_mapper() + decision = self._aserto_middleware.is_allowed( + decision="allowed", + authorizer_options=self._aserto_middleware._authorizer_options, + identity_provider=self._identity_provider, + policy_instance_name=self._aserto_middleware._policy_instance_name or "", + policy_instance_label=self._aserto_middleware._policy_instance_label or "", + policy_path_root=self._aserto_middleware._policy_path_root, + policy_path_resolver=policy_mapper, + resource_context_provider=self._resource_context_provider, + ) + + if not decision: + raise AuthorizationError(policy_instance_name=self._aserto_middleware._policy_instance_name, policy_path=policy_mapper()) # type: ignore[arg-type] + + return handler(*args, **kwargs) + + return cast(Handler, decorated) \ No newline at end of file diff --git a/packages/flask-aserto/src/flask_aserto/middleware.py b/packages/flask-aserto/src/flask_aserto/middleware.py new file mode 100644 index 0000000..eebff1b --- /dev/null +++ b/packages/flask-aserto/src/flask_aserto/middleware.py @@ -0,0 +1,232 @@ +from functools import wraps +from typing import Any, Callable, Optional, Union, cast, overload + +from aserto.client import AuthorizerOptions +from aserto.client.authorizer import AuthorizerClient +from flask import Flask, jsonify +from flask.wrappers import Response + +from ._defaults import ( + DEFAULT_DISPLAY_STATE_MAP_ENDPOINT, + DEFAULT_RESOURCE_CONTEXT_PROVIDER_FOR_DISPLAY_STATE_MAP, + DEFAULT_RESOURCE_CONTEXT_PROVIDER_FOR_ENDPOINT, + create_default_policy_path_resolver, + IdentityMapper, + StringMapper, + ResourceMapper, + ObjectMapper, + AuthorizationError, + Handler +) + +from .check import CheckMiddleware, CheckOptions + +class AsertoMiddleware: + def __init__( + self, + *, + authorizer_options: AuthorizerOptions, + policy_path_root: str, + identity_provider: IdentityMapper, + policy_instance_name: Optional[str]= None, + policy_instance_label: Optional[str]= None, + policy_path_resolver: Optional[StringMapper] = None, + resource_context_provider: Optional[ResourceMapper] = None, + ): + self._authorizer_options = authorizer_options + self._identity_provider = identity_provider + self._policy_instance_name = policy_instance_name + self._policy_instance_label = policy_instance_label + self._policy_path_root = policy_path_root + + self._policy_path_resolver = ( + policy_path_resolver + if policy_path_resolver is not None + else create_default_policy_path_resolver(policy_path_root) + ) + + self._resource_context_provider = ( + resource_context_provider + if resource_context_provider is not None + else DEFAULT_RESOURCE_CONTEXT_PROVIDER_FOR_ENDPOINT + ) + + def _generate_client(self) -> AuthorizerClient: + identity = self._identity_provider() + + return AuthorizerClient( + identity=identity, + options=self._authorizer_options, + ) + + def _with_overrides(self, **kwargs: Any) -> "AsertoMiddleware": + return ( + self + if not kwargs + else AsertoMiddleware( + authorizer_options=kwargs.get("authorizer", self._authorizer_options), + policy_path_root=kwargs.get("policy_path_root", self._policy_path_root), + identity_provider=kwargs.get("identity_provider", self._identity_provider), + policy_instance_name=kwargs.get("policy_instance_name", self._policy_instance_name), + policy_instance_label=kwargs.get("policy_instance_label", self._policy_instance_label), + policy_path_resolver=kwargs.get("policy_path_resolver", self._policy_path_resolver), + resource_context_provider=kwargs.get( + "resource_context_provider", self._resource_context_provider + ), + ) + ) + + @overload + def is_allowed(self, decision: str) -> bool: + ... + + @overload + def is_allowed( + self, + decision: str, + *, + authorizer_options: AuthorizerOptions = ..., + identity_provider: IdentityMapper = ..., + policy_instance_name: str = ..., + policy_instance_label: str = ..., + policy_path_root: str = ..., + policy_path_resolver: StringMapper = ..., + resource_context_provider: ResourceMapper = ..., + ) -> bool: + ... + + def is_allowed(self, decision: str, **kwargs: Any) -> bool: + return self._with_overrides(**kwargs)._is_allowed(decision) + + def _is_allowed(self, decision: str) -> bool: + client = self._generate_client() + resource_context = self._resource_context_provider() + policy_path = self._policy_path_resolver() + + decisions = client.decisions( + policy_path=policy_path, + decisions=(decision,), + policy_instance_name=self._policy_instance_name, + policy_instance_label=self._policy_instance_label, + resource_context=resource_context, + ) + return decisions[decision] + + @overload + def authorize(self, handler: Handler) -> Handler: + ... + + @overload + def authorize( + self, + *, + authorizer_options: AuthorizerOptions = ..., + identity_provider: IdentityMapper = ..., + policy_instance_name: str = ..., + policy_instance_label: str = ..., + policy_path_root: str = ..., + policy_path_resolver: StringMapper = ..., + ) -> Callable[[Handler], Handler]: + ... + + def authorize( + self, + *args: Any, + **kwargs: Any, + ) -> Union[Handler, Callable[[Handler], Handler]]: + arguments_error = TypeError( + f"{self.authorize.__name__}() expects either exactly 1 callable" + " 'handler' argument or at least 1 'options' argument" + ) + + handler: Optional[Handler] = None + + if not args and kwargs.keys() == {"handler"}: + handler = kwargs["handler"] + elif not kwargs and len(args) == 1: + (handler,) = args + + if handler is not None: + if not callable(handler): + raise arguments_error + return self._authorize(handler) + + if args: + raise arguments_error + + return self._with_overrides(**kwargs)._authorize + + def _authorize(self, handler: Handler) -> Handler: + if self._policy_instance_name == None: + raise TypeError(f"{self._policy_instance_name}() should not be None") + + if self._policy_instance_label == None: + self._policy_instance_label = self._policy_instance_name + + @wraps(handler) + def decorated(*args: Any, **kwargs: Any) -> Response: + client = self._generate_client() + resource_context = self._resource_context_provider() + policy_path = self._policy_path_resolver() + + decisions = client.decisions( + policy_path=policy_path, + decisions=("allowed",), + policy_instance_name=self._policy_instance_name, + policy_instance_label=self._policy_instance_label, + resource_context=resource_context, + ) + + if not decisions["allowed"]: + raise AuthorizationError(policy_instance_name=self._policy_instance_name, policy_path=policy_path) # type: ignore[arg-type] + + return handler(*args, **kwargs) + + return cast(Handler, decorated) + + def check( + self, + objId: Optional[str] = "", + objType: Optional[str] = "", + objIdMapper: Optional[StringMapper] = None, + objMapper: Optional[ObjectMapper] = None, + relationName: Optional[str] = "", + relationMapper: Optional[StringMapper] = None, + subjType: Optional[str] = "", + subjMapper: Optional[IdentityMapper] = None, + policyPath: Optional[str] = "", + policyPathMapper: Optional[StringMapper] = None, + ) -> CheckMiddleware: + opts = CheckOptions( + objId=objId, objType=objType,objIdMapper=objIdMapper, + objMapper=objMapper, relationName=relationName, relationMapper=relationMapper, + subjType=subjType, subjMapper=subjMapper, policyPath=policyPath, policyPathMapper=policyPathMapper) + return CheckMiddleware(options=opts, aserto_middleware=self) + + def register_display_state_map( + self, + app: Flask, + *, + endpoint: str = DEFAULT_DISPLAY_STATE_MAP_ENDPOINT, + resource_context_provider: Optional[ResourceMapper] = None, + ) -> Flask: + @app.route(endpoint, methods=["GET", "POST"]) + def __displaystatemap() -> Response: + nonlocal resource_context_provider + if resource_context_provider is None: + resource_context_provider = DEFAULT_RESOURCE_CONTEXT_PROVIDER_FOR_DISPLAY_STATE_MAP + + client = self._generate_client() + resource_context = resource_context_provider() + + display_state_map = client.decision_tree( + policy_path_root=self._policy_path_root, + decisions=["visible", "enabled"], + policy_instance_name=self._policy_instance_name, + policy_instance_label=self._policy_instance_label, + resource_context=resource_context, + policy_path_separator="SLASH", + ) + return jsonify(display_state_map) + + return app