Repository: busykoala/fastapi-opa (language: python)

Issue:
Add GraphQL analyzer
**Is your feature request related to a problem? Please describe.**
FastAPI can be used with a GraphQL endpoint. If this is the case it would be great if there was a way to retrieve information by the raw query to send a payload including that to OPA before it hits the endpoint.

**Additional context**
Relies on #11 



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index d5d9858..3fdd5c8 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -20,7 +20,7 @@ jobs:
       run: |
         python -m pip install --upgrade pip
         python -m pip install poetry
-        python -m poetry install
+        python -m poetry install --extras "graphql"
     - name: Codestyle Checks
       run: |
         python -m poetry run flake8 .
diff --git a/CHANGELOG.md b/CHANGELOG.md
index db9311c..86cc4d0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Change Log
 
+## [1.1.0] - 2021-05-06
+
+- Allow custom injectables to enrich the payload sent to OPA.
+- Add GraphQLAnalysis to parse raw GraphQL payloads and add an injectable to
+  send additional data to OPA allowing fine-grained authorization policies.
+
 ## [1.0.1] - 2021-04-25
 
 - Remove uvicorn as a pkg dependency.
diff --git a/README.md b/README.md
index 8c3c8f0..517076e 100644
--- a/README.md
+++ b/README.md
@@ -30,12 +30,12 @@ the user is allowed to get any information from the endpoint.
 ## Installation
 
 ```bash
-poetry add fastapi-opa
+poetry add [--extras "graphql"] fastapi-opa 
 ```
 
 ## How to get started
 
-Checkout the wiki for a complete environment setup with Keycloak and Open Policy Agent:  
+:bulb: Checkout the wiki for a complete environment setup with Keycloak and Open Policy Agent:  
 [Getting Started with FastAPI app with Authentication and Authorization](https://github.com/busykoala/fastapi-opa/wiki#dev-setup)
 
 The package provides a very easy way to integrate authentication and
@@ -152,8 +152,28 @@ easily implement it and inject it into OPAMiddleware
 (`fastapi_opa.auth.auth_interface.AuthInterface`), or you can open a pull
 request if you would like to contribute to the package.
 
+## Custom Payload Enrichment
+
+In `fastapi_opa.opa.opa_config.Injectable` an interface is provided to add
+more information to the payload sent to OPA.
+
+The injectables can be added to the `OPAConfig`. Let's look at an example:
+
+```python
+class FancyInjectable(Injectable):
+    async def extract(self, request: Request) -> List:
+        return ["some", "custom", "stuff"]
+
+fancy_inj = FancyInjectable("fancy_key")
+
+opa_config = OPAConfig(
+    authentication=oidc_auth, opa_host=opa_host, injectables=[fancy_inj]
+)
+```
+
+:bulb: For GraphQL there is a ready to use injectable in
+`fastapi_opa.opa.enrichment.graphql_enrichment.GraphQLInjectable`
+
 ## Roadmap
 
 - Add other authentication flows
-- Allow more flexible way of opa jwt enrichment
-- Implement GraphQL query parser as a jwt enrichment strategy
diff --git a/fastapi_opa/opa/enrichment/__init__.py b/fastapi_opa/opa/enrichment/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/fastapi_opa/opa/enrichment/graphql_enrichment.py b/fastapi_opa/opa/enrichment/graphql_enrichment.py
new file mode 100644
index 0000000..70274da
--- /dev/null
+++ b/fastapi_opa/opa/enrichment/graphql_enrichment.py
@@ -0,0 +1,111 @@
+from dataclasses import dataclass
+from json import JSONDecodeError
+from typing import Any
+from typing import Dict
+from typing import List
+from typing import Optional
+from typing import Tuple
+from typing import Union
+
+from graphql import GraphQLCoreBackend
+from graphql import GraphQLField
+from graphql import GraphQLObjectType
+from graphql import GraphQLSchema
+from graphql import GraphQLString
+from graphql.language.ast import ListType
+from graphql.language.ast import NamedType
+from graphql.language.ast import OperationDefinition
+from graphql.language.ast import SelectionSet
+from graphql.language.ast import VariableDefinition
+from starlette.requests import Request
+
+from fastapi_opa.opa.opa_config import Injectable
+
+
+@dataclass
+class OperationData:
+    name: str
+    operation: str
+    variables: Dict[str, str]
+    selection_set: Any
+
+
+class GraphQLAnalysis:
+    type = GraphQLObjectType(
+        "Type", lambda: {"type": GraphQLField(GraphQLString)}
+    )
+    schema = GraphQLSchema(type)
+    backend = GraphQLCoreBackend()
+
+    def __init__(self, payload: Dict) -> None:
+        self.operations = []
+        operation_defs = self.get_operation_defs(payload)
+        for operation_def in operation_defs:
+            self.operations.append(
+                OperationData(
+                    name=(
+                        operation_def.name.value if operation_def.name else ""
+                    ),
+                    operation=operation_def.operation,
+                    variables=self.extract_variables(
+                        operation_def.variable_definitions
+                    ),
+                    selection_set=self.extract_selection_set(
+                        operation_def.selection_set, []
+                    ),
+                )
+            )
+
+    def get_operation_defs(self, payload: Dict) -> List[OperationDefinition]:
+        gql_query = payload.get("query")
+        doc = self.backend.document_from_string(
+            schema=self.schema, document_string=gql_query
+        )
+        return doc.document_ast.definitions
+
+    def extract_selection_set(
+        self, selection_set: Union[SelectionSet, Tuple], result: List
+    ) -> List:
+        if isinstance(selection_set, SelectionSet):
+            result_part = []
+            for field in selection_set.selections:
+                result_part.append(field.name.value)
+                self.extract_selection_set(field.selection_set, result_part)
+            result.append(result_part)
+        return result
+
+    def extract_variables(
+        self, variable_definitions: List[VariableDefinition]
+    ) -> Dict:
+        variables = {}
+        if not variable_definitions:
+            return {}
+        for var_def in variable_definitions:
+            variables[var_def.variable.name.value] = self.deep_extract_type(
+                var_def.type
+            )
+        return variables
+
+    def deep_extract_type(
+        self,
+        item_type: Union[ListType, NamedType],
+        type_str: Optional[str] = "{}",
+    ) -> str:
+        if isinstance(item_type, ListType):
+            return self.deep_extract_type(item_type.type, "[{}]")
+        else:
+            return type_str.format(item_type.name.value)
+
+
+class GraphQLInjectable(Injectable):
+    async def extract(self, request: Request) -> List:
+        payload = await self.get_payload(request)
+        analyser = GraphQLAnalysis(payload)
+        return [op_data.__dict__ for op_data in analyser.operations]
+
+    @staticmethod
+    async def get_payload(request):
+        try:
+            return await request.json()
+        except JSONDecodeError:
+            return
diff --git a/fastapi_opa/opa/opa_config.py b/fastapi_opa/opa/opa_config.py
index 42553d9..126d8fa 100644
--- a/fastapi_opa/opa/opa_config.py
+++ b/fastapi_opa/opa/opa_config.py
@@ -1,7 +1,29 @@
+from abc import ABC
+from abc import abstractmethod
+from typing import List
+from typing import Optional
+
+from starlette.requests import Request
+
 from fastapi_opa.auth.auth_interface import AuthInterface
 
 
+class Injectable(ABC):
+    def __init__(self, key: str) -> None:
+        self.key = key
+
+    @abstractmethod
+    async def extract(self, request: Request) -> List:
+        pass
+
+
 class OPAConfig:
-    def __init__(self, authentication: AuthInterface, opa_host: str) -> None:
+    def __init__(
+        self,
+        authentication: AuthInterface,
+        opa_host: str,
+        injectables: Optional[List[Injectable]] = None,
+    ) -> None:
         self.authentication = authentication
         self.opa_url = f"{opa_host.rstrip('/')}/v1/data/httpapi/authz"
+        self.injectables = injectables
diff --git a/fastapi_opa/opa/opa_middleware.py b/fastapi_opa/opa/opa_middleware.py
index dcf73e1..47c00f6 100644
--- a/fastapi_opa/opa/opa_middleware.py
+++ b/fastapi_opa/opa/opa_middleware.py
@@ -5,6 +5,7 @@
 import requests
 from fastapi.responses import JSONResponse
 from starlette.requests import HTTPConnection
+from starlette.requests import Request
 from starlette.responses import RedirectResponse
 from starlette.types import ASGIApp
 from starlette.types import Receive
@@ -43,6 +44,13 @@ async def __call__(
 
         # Check OPA decision for info provided in user_info
         is_authorized = False
+        # Enrich user_info if injectables are provided
+        request = Request(scope, receive, send)
+        if self.config.injectables:
+            for injectable in self.config.injectables:
+                user_info_or_auth_redirect[
+                    injectable.key
+                ] = await injectable.extract(request)
         user_info_or_auth_redirect["request_method"] = scope.get("method")
         # fmt: off
         user_info_or_auth_redirect["request_path"] = scope.get("path").split("/")[1:]  # noqa
diff --git a/poetry.lock b/poetry.lock
index ecb7e32..f7e976a 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,3 +1,11 @@
+[[package]]
+name = "aniso8601"
+version = "7.0.0"
+description = "A library for parsing ISO 8601 strings."
+category = "main"
+optional = true
+python-versions = "*"
+
 [[package]]
 name = "appdirs"
 version = "1.4.4"
@@ -216,6 +224,55 @@ python-versions = ">=3.5"
 gitdb = ">=4.0.1,<5"
 typing-extensions = ">=3.7.4.0"
 
+[[package]]
+name = "graphene"
+version = "2.1.8"
+description = "GraphQL Framework for Python"
+category = "main"
+optional = true
+python-versions = "*"
+
+[package.dependencies]
+aniso8601 = ">=3,<=7"
+graphql-core = ">=2.1,<3"
+graphql-relay = ">=2,<3"
+six = ">=1.10.0,<2"
+
+[package.extras]
+django = ["graphene-django"]
+sqlalchemy = ["graphene-sqlalchemy"]
+test = ["pytest", "pytest-benchmark", "pytest-cov", "pytest-mock", "snapshottest", "coveralls", "promise", "six", "mock", "pytz", "iso8601"]
+
+[[package]]
+name = "graphql-core"
+version = "2.3.2"
+description = "GraphQL implementation for Python"
+category = "main"
+optional = true
+python-versions = "*"
+
+[package.dependencies]
+promise = ">=2.3,<3"
+rx = ">=1.6,<2"
+six = ">=1.10.0"
+
+[package.extras]
+gevent = ["gevent (>=1.1)"]
+test = ["six (1.14.0)", "pyannotate (1.2.0)", "pytest (4.6.10)", "pytest-django (3.9.0)", "pytest-cov (2.8.1)", "coveralls (1.11.1)", "cython (0.29.17)", "gevent (1.5.0)", "pytest-benchmark (3.2.3)", "pytest-mock (2.0.0)"]
+
+[[package]]
+name = "graphql-relay"
+version = "2.0.1"
+description = "Relay implementation for Python"
+category = "main"
+optional = true
+python-versions = "*"
+
+[package.dependencies]
+graphql-core = ">=2.2,<3"
+promise = ">=2.2,<3"
+six = ">=1.12"
+
 [[package]]
 name = "idna"
 version = "2.10"
@@ -311,7 +368,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 
 [[package]]
 name = "pbr"
-version = "5.5.1"
+version = "5.6.0"
 description = "Python Build Reasonableness"
 category = "dev"
 optional = false
@@ -331,6 +388,20 @@ importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
 [package.extras]
 dev = ["pre-commit", "tox"]
 
+[[package]]
+name = "promise"
+version = "2.3"
+description = "Promises/A+ implementation for Python"
+category = "main"
+optional = true
+python-versions = "*"
+
+[package.dependencies]
+six = "*"
+
+[package.extras]
+test = ["pytest (>=2.7.3)", "pytest-cov", "coveralls", "futures", "pytest-benchmark", "mock"]
+
 [[package]]
 name = "py"
 version = "1.10.0"
@@ -381,7 +452,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 
 [[package]]
 name = "pyjwt"
-version = "2.0.1"
+version = "2.1.0"
 description = "JSON Web Token implementation in Python"
 category = "main"
 optional = false
@@ -406,7 +477,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
 
 [[package]]
 name = "pytest"
-version = "6.2.3"
+version = "6.2.4"
 description = "pytest: simple powerful testing with Python"
 category = "dev"
 optional = false
@@ -428,11 +499,11 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xm
 
 [[package]]
 name = "pytest-mock"
-version = "3.5.1"
+version = "3.6.0"
 description = "Thin-wrapper around the mock package for easier use with pytest"
 category = "dev"
 optional = false
-python-versions = ">=3.5"
+python-versions = ">=3.6"
 
 [package.dependencies]
 pytest = ">=5.0"
@@ -485,11 +556,19 @@ urllib3 = ">=1.21.1,<1.27"
 security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
 socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"]
 
+[[package]]
+name = "rx"
+version = "1.6.1"
+description = "Reactive Extensions (Rx) for Python"
+category = "main"
+optional = true
+python-versions = "*"
+
 [[package]]
 name = "six"
-version = "1.15.0"
+version = "1.16.0"
 description = "Python 2 and 3 compatibility utilities"
-category = "dev"
+category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
 
@@ -542,7 +621,7 @@ python-versions = "*"
 
 [[package]]
 name = "typing-extensions"
-version = "3.7.4.3"
+version = "3.10.0.0"
 description = "Backported and Experimental Type Hints for Python 3.5+"
 category = "dev"
 optional = false
@@ -573,12 +652,19 @@ python-versions = ">=3.6"
 docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
 testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
 
+[extras]
+graphql = ["graphene"]
+
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.6"
-content-hash = "7c897b14021c4fb4dae7be7811fa688c7c5ca01f9c7574ccdcc2ef962f6babe4"
+content-hash = "ef09080157dc9d6497bfee76cb21e34b74a08133bce6dc3035a0d28f09db9b5d"
 
 [metadata.files]
+aniso8601 = [
+    {file = "aniso8601-7.0.0-py2.py3-none-any.whl", hash = "sha256:d10a4bf949f619f719b227ef5386e31f49a2b6d453004b21f02661ccc8670c7b"},
+    {file = "aniso8601-7.0.0.tar.gz", hash = "sha256:513d2b6637b7853806ae79ffaca6f3e8754bdd547048f5ccc1420aec4b714f1e"},
+]
 appdirs = [
     {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
     {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
@@ -695,6 +781,18 @@ gitpython = [
     {file = "GitPython-3.1.15-py3-none-any.whl", hash = "sha256:a77824e516d3298b04fb36ec7845e92747df8fcfee9cacc32dd6239f9652f867"},
     {file = "GitPython-3.1.15.tar.gz", hash = "sha256:05af150f47a5cca3f4b0af289b73aef8cf3c4fe2385015b06220cbcdee48bb6e"},
 ]
+graphene = [
+    {file = "graphene-2.1.8-py2.py3-none-any.whl", hash = "sha256:09165f03e1591b76bf57b133482db9be6dac72c74b0a628d3c93182af9c5a896"},
+    {file = "graphene-2.1.8.tar.gz", hash = "sha256:2cbe6d4ef15cfc7b7805e0760a0e5b80747161ce1b0f990dfdc0d2cf497c12f9"},
+]
+graphql-core = [
+    {file = "graphql-core-2.3.2.tar.gz", hash = "sha256:aac46a9ac524c9855910c14c48fc5d60474def7f99fd10245e76608eba7af746"},
+    {file = "graphql_core-2.3.2-py2.py3-none-any.whl", hash = "sha256:44c9bac4514e5e30c5a595fac8e3c76c1975cae14db215e8174c7fe995825bad"},
+]
+graphql-relay = [
+    {file = "graphql-relay-2.0.1.tar.gz", hash = "sha256:870b6b5304123a38a0b215a79eace021acce5a466bf40cd39fa18cb8528afabb"},
+    {file = "graphql_relay-2.0.1-py3-none-any.whl", hash = "sha256:ac514cb86db9a43014d7e73511d521137ac12cf0101b2eaa5f0a3da2e10d913d"},
+]
 idna = [
     {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
     {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
@@ -732,13 +830,16 @@ pathspec = [
     {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"},
 ]
 pbr = [
-    {file = "pbr-5.5.1-py2.py3-none-any.whl", hash = "sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00"},
-    {file = "pbr-5.5.1.tar.gz", hash = "sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9"},
+    {file = "pbr-5.6.0-py2.py3-none-any.whl", hash = "sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4"},
+    {file = "pbr-5.6.0.tar.gz", hash = "sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd"},
 ]
 pluggy = [
     {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
     {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
 ]
+promise = [
+    {file = "promise-2.3.tar.gz", hash = "sha256:dfd18337c523ba4b6a58801c164c1904a9d4d1b1747c7d5dbf45b693a49d93d0"},
+]
 py = [
     {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
     {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
@@ -780,20 +881,20 @@ pyflakes = [
     {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"},
 ]
 pyjwt = [
-    {file = "PyJWT-2.0.1-py3-none-any.whl", hash = "sha256:b70b15f89dc69b993d8a8d32c299032d5355c82f9b5b7e851d1a6d706dffe847"},
-    {file = "PyJWT-2.0.1.tar.gz", hash = "sha256:a5c70a06e1f33d81ef25eecd50d50bd30e34de1ca8b2b9fa3fe0daaabcf69bf7"},
+    {file = "PyJWT-2.1.0-py3-none-any.whl", hash = "sha256:934d73fbba91b0483d3857d1aff50e96b2a892384ee2c17417ed3203f173fca1"},
+    {file = "PyJWT-2.1.0.tar.gz", hash = "sha256:fba44e7898bbca160a2b2b501f492824fc8382485d3a6f11ba5d0c1937ce6130"},
 ]
 pyparsing = [
     {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
     {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
 ]
 pytest = [
-    {file = "pytest-6.2.3-py3-none-any.whl", hash = "sha256:6ad9c7bdf517a808242b998ac20063c41532a570d088d77eec1ee12b0b5574bc"},
-    {file = "pytest-6.2.3.tar.gz", hash = "sha256:671238a46e4df0f3498d1c3270e5deb9b32d25134c99b7d75370a68cfbe9b634"},
+    {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"},
+    {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"},
 ]
 pytest-mock = [
-    {file = "pytest-mock-3.5.1.tar.gz", hash = "sha256:a1e2aba6af9560d313c642dae7e00a2a12b022b80301d9d7fc8ec6858e1dd9fc"},
-    {file = "pytest_mock-3.5.1-py3-none-any.whl", hash = "sha256:379b391cfad22422ea2e252bdfc008edd08509029bcde3c25b2c0bd741e0424e"},
+    {file = "pytest-mock-3.6.0.tar.gz", hash = "sha256:f7c3d42d6287f4e45846c8231c31902b6fa2bea98735af413a43da4cf5b727f1"},
+    {file = "pytest_mock-3.6.0-py3-none-any.whl", hash = "sha256:952139a535b5b48ac0bb2f90b5dd36b67c7e1ba92601f3a8012678c4bd7f0bcc"},
 ]
 python-dateutil = [
     {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"},
@@ -877,9 +978,13 @@ requests = [
     {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"},
     {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"},
 ]
+rx = [
+    {file = "Rx-1.6.1-py2.py3-none-any.whl", hash = "sha256:7357592bc7e881a95e0c2013b73326f704953301ab551fbc8133a6fadab84105"},
+    {file = "Rx-1.6.1.tar.gz", hash = "sha256:13a1d8d9e252625c173dc795471e614eadfe1cf40ffc684e08b8fff0d9748c23"},
+]
 six = [
-    {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
-    {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
+    {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
+    {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
 ]
 smmap = [
     {file = "smmap-4.0.0-py2.py3-none-any.whl", hash = "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2"},
@@ -930,9 +1035,9 @@ typed-ast = [
     {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"},
 ]
 typing-extensions = [
-    {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"},
-    {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"},
-    {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"},
+    {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"},
+    {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"},
+    {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"},
 ]
 urllib3 = [
     {file = "urllib3-1.26.4-py2.py3-none-any.whl", hash = "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df"},
diff --git a/pyproject.toml b/pyproject.toml
index 875115a..3fd300c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "fastapi-opa"
-version = "1.0.1"
+version = "1.1.0"
 description = "Fastapi OPA middleware incl. auth flow."
 authors = ["Matthias Osswald <m@osswald.li>"]
 license = "GPL-3.0-or-later"
@@ -13,6 +13,8 @@ python = "^3.6"
 fastapi = "^0.63.0"
 requests = "^2.25.1"
 PyJWT = {extras = ["crypto"], version = "^2.0.1"}
+graphene = {version = "^2.1.8", optional = true}
+
 
 [tool.poetry.dev-dependencies]
 flake9 = "^3.8.3"
@@ -25,6 +27,9 @@ mock = "^4.0.3"
 freezegun = "^1.1.0"
 Authlib = "^0.15.3"
 
+[tool.poetry.extras]
+graphql = ["graphene"]
+
 [build-system]
 requires = ["poetry-core>=1.0.0"]
 build-backend = "poetry.core.masonry.api"


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 0cfa70e..49c3b31 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -6,7 +6,9 @@
 
 from fastapi_opa import OPAConfig
 from fastapi_opa import OPAMiddleware
+from fastapi_opa.opa.enrichment.graphql_enrichment import GraphQLInjectable
 from tests.utils import AuthenticationDummy
+from tests.utils import OPAInjectableExample
 
 
 @pytest.fixture
@@ -25,3 +27,45 @@ async def root() -> Dict:
         }
 
     yield TestClient(app)
+
+
+@pytest.fixture
+def injected_client():
+    opa_host = "http://localhost:8181"
+    oidc_auth = AuthenticationDummy()
+    injectable = OPAInjectableExample("example_injectable")
+    opa_config = OPAConfig(
+        authentication=oidc_auth, opa_host=opa_host, injectables=[injectable]
+    )
+
+    app = FastAPI()
+    app.add_middleware(OPAMiddleware, config=opa_config)
+
+    @app.get("/")
+    async def root() -> Dict:
+        return {
+            "msg": "success",
+        }
+
+    yield TestClient(app)
+
+
+@pytest.fixture
+def gql_injected_client():
+    opa_host = "http://localhost:8181"
+    oidc_auth = AuthenticationDummy()
+    injectable = GraphQLInjectable("gql_injectable")
+    opa_config = OPAConfig(
+        authentication=oidc_auth, opa_host=opa_host, injectables=[injectable]
+    )
+
+    app = FastAPI()
+    app.add_middleware(OPAMiddleware, config=opa_config)
+
+    @app.get("/")
+    async def root() -> Dict:
+        return {
+            "msg": "success",
+        }
+
+    yield TestClient(app)
diff --git a/tests/test_data/__init__.py b/tests/test_data/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_data/graphql_queries.py b/tests/test_data/graphql_queries.py
new file mode 100644
index 0000000..eea5df5
--- /dev/null
+++ b/tests/test_data/graphql_queries.py
@@ -0,0 +1,146 @@
+from fastapi_opa.opa.enrichment.graphql_enrichment import OperationData
+
+GQL_TEST_CASES = [
+    (
+        {
+            "operationName": "getStudents",
+            "variables": {"subject": "Physics", "enrolled": True},
+            "query": """query getStudents(
+                $subject: String,
+                $enrolled: Boolean){
+                students(
+                    subject: $subject,
+                    enrolled: $enrolled){
+                        Student {
+                            name
+                            subject
+                            enrolled
+                        }
+                    }
+                }
+            """,
+        },
+        [
+            OperationData(
+                name="getStudents",
+                operation="query",
+                variables={"subject": "String", "enrolled": "Boolean"},
+                selection_set=[
+                    ["students", ["Student", ["name", "subject", "enrolled"]]]
+                ],
+            )
+        ],
+    ),
+    (
+        {
+            "operationName": "getStudents",
+            "variables": {"subject": "Physics", "enrolled": True},
+            "query": """query getStudents(
+            $subject: String,
+            $enrolled: Boolean){
+            students(
+                subject: $subject,
+                enrolled: $enrolled){
+                    Student {
+                        name
+                        subject
+                        friends {
+                            nickname
+                        }
+                        enrolled
+                    }
+                }
+            }
+        """,
+        },
+        [
+            OperationData(
+                name="getStudents",
+                operation="query",
+                variables={"subject": "String", "enrolled": "Boolean"},
+                selection_set=[
+                    [
+                        "students",
+                        [
+                            "Student",
+                            [
+                                "name",
+                                "subject",
+                                "friends",
+                                ["nickname"],
+                                "enrolled",
+                            ],
+                        ],
+                    ]
+                ],
+            )
+        ],
+    ),
+    (
+        {
+            "operationName": "addStudent",
+            "variables": {"name": "Peter", "subject": "Programming"},
+            "query": """mutation addStudent(
+            $name: String,
+            $subject: String){
+            student(
+                name: $name,
+                subject: $subject){
+                    Student {
+                        name
+                        subject
+                    }
+                }
+            }
+        """,
+        },
+        [
+            OperationData(
+                name="addStudent",
+                operation="mutation",
+                variables={"name": "String", "subject": "String"},
+                selection_set=[["student", ["Student", ["name", "subject"]]]],
+            )
+        ],
+    ),
+    (
+        {
+            "operationName": "addStudent",
+            "variables": {
+                "name": "Peter",
+                "subject": "Programming",
+                "friends": ["Anna", "Berta"],
+            },
+            "query": """mutation addStudent(
+            $name: String,
+            $subject: String,
+            $friends: [String]){
+            student(
+                name: $name,
+                subject: $subject
+                friends: $friends){
+                    Student {
+                        name
+                        subject
+                        friends
+                    }
+                }
+            }
+        """,
+        },
+        [
+            OperationData(
+                name="addStudent",
+                operation="mutation",
+                variables={
+                    "name": "String",
+                    "subject": "String",
+                    "friends": "[String]",
+                },
+                selection_set=[
+                    ["student", ["Student", ["name", "subject", "friends"]]]
+                ],
+            )
+        ],
+    ),
+]
diff --git a/tests/test_enrichment_graphql.py b/tests/test_enrichment_graphql.py
new file mode 100644
index 0000000..55324e8
--- /dev/null
+++ b/tests/test_enrichment_graphql.py
@@ -0,0 +1,43 @@
+import json
+
+import pytest
+from mock import patch
+
+from fastapi_opa.opa.enrichment.graphql_enrichment import GraphQLAnalysis
+from tests.test_data.graphql_queries import GQL_TEST_CASES
+
+
+@pytest.mark.parametrize("test_payload,expected", GQL_TEST_CASES)
+def test_query_parsing(test_payload, expected):
+    gql_analysis = GraphQLAnalysis(payload=test_payload)
+
+    assert expected == gql_analysis.operations
+
+
+def test_gql_injection(gql_injected_client):
+    with patch("fastapi_opa.opa.opa_middleware.requests.post") as req:
+        payload = GQL_TEST_CASES[0][0]
+        gql_injected_client.get("/", json=payload)
+
+    expected_payload = {
+        "stuff": "some info",
+        "username": "John Doe",
+        "role": "Administrator",
+        "gql_injectable": [
+            {
+                "name": "getStudents",
+                "operation": "query",
+                "selection_set": [
+                    ["students", ["Student", ["name", "subject", "enrolled"]]]
+                ],
+                "variables": {"enrolled": "Boolean", "subject": "String"},
+            }
+        ],
+        "request_method": "GET",
+        "request_path": [""],
+    }
+
+    actual_payload = json.loads(req.call_args_list[0][1].get("data")).get(
+        "input"
+    )
+    assert expected_payload == actual_payload
diff --git a/tests/test_opa.py b/tests/test_opa.py
index 9a80212..4ca159d 100644
--- a/tests/test_opa.py
+++ b/tests/test_opa.py
@@ -57,3 +57,21 @@ def test_not_allowing_opa_flow(client):
     assert expected_url == url
     assert expected_payload == payload
     assert {"message": "Unauthorized"} == response.json()
+
+
+def test_function_injection(injected_client):
+    with patch("fastapi_opa.opa.opa_middleware.requests.post") as req:
+        payload = {"some": "data"}
+        injected_client.get("/", json=payload)
+
+    expected_payload = {
+        "stuff": "some info",
+        "username": "John Doe",
+        "role": "Administrator",
+        "example_injectable": [{"some": "data"}],
+        "request_method": "GET",
+        "request_path": [""],
+    }
+
+    payload = json.loads(req.call_args_list[0][1].get("data")).get("input")
+    assert expected_payload == payload
diff --git a/tests/utils.py b/tests/utils.py
index a50505b..8bfd65b 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -1,11 +1,15 @@
+from json import JSONDecodeError
 from typing import Dict
+from typing import List
 from typing import Union
 
 from mock import Mock
+from starlette.requests import Request
 from starlette.responses import RedirectResponse
 
 from fastapi_opa.auth import OIDCConfig
 from fastapi_opa.auth.auth_interface import AuthInterface
+from fastapi_opa.opa.opa_config import Injectable
 
 
 def mock_response(status_code, json_data=None):
@@ -28,6 +32,18 @@ def authenticate(
         }
 
 
+class OPAInjectableExample(Injectable):
+    async def extract(self, request: Request) -> List:
+        return [await self.get_payload(request)]
+
+    @staticmethod
+    async def get_payload(request):
+        try:
+            return await request.json()
+        except JSONDecodeError:
+            return
+
+
 # ***************************
 # OIDC Utils
 # ***************************