2022-06-04 08:41:53 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
2024-01-15 20:20:10 +00:00
|
|
|
# bthlabs-jsonrpc-django | (c) 2022-present Tomek Wójcik | MIT License
|
|
|
|
from __future__ import annotations
|
|
|
|
|
2022-06-04 08:41:53 +00:00
|
|
|
import typing
|
|
|
|
|
2024-01-15 20:20:10 +00:00
|
|
|
from bthlabs_jsonrpc_core.codecs import Codec
|
2022-06-04 08:41:53 +00:00
|
|
|
from django.core.exceptions import PermissionDenied
|
2024-01-15 20:20:10 +00:00
|
|
|
from django.http import HttpRequest, HttpResponse
|
2022-06-04 08:41:53 +00:00
|
|
|
from django.utils.decorators import classonlymethod
|
|
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
|
|
from django.views.generic.base import View
|
|
|
|
|
2024-01-15 20:20:10 +00:00
|
|
|
from bthlabs_jsonrpc_django.codecs import DjangoJSONCodec
|
2022-06-04 08:41:53 +00:00
|
|
|
from bthlabs_jsonrpc_django.executor import DjangoExecutor
|
|
|
|
|
|
|
|
|
|
|
|
class JSONRPCView(View):
|
|
|
|
"""
|
|
|
|
The JSONRPC View. This is the main JSONRPC entry point. Use it to register
|
|
|
|
your JSONRPC endpoints.
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
|
|
|
.. code-block:: python
|
|
|
|
|
|
|
|
from bthlabs_jsonrpc_django import JSONRPCView, is_authenticated
|
|
|
|
|
|
|
|
urlpatterns = [
|
|
|
|
path(
|
|
|
|
'rpc/private',
|
|
|
|
JSONRPCView.as_view(
|
|
|
|
auth_checks=[is_authenticated],
|
|
|
|
namespace='admin',
|
|
|
|
),
|
|
|
|
)
|
|
|
|
path('rpc', JSONRPCView.as_view()),
|
|
|
|
]
|
|
|
|
"""
|
|
|
|
|
|
|
|
# pragma mark - Public class attributes
|
|
|
|
|
2024-01-15 20:20:10 +00:00
|
|
|
#: The executor class.
|
|
|
|
executor: type[DjangoExecutor] = DjangoExecutor
|
|
|
|
|
2022-06-04 08:41:53 +00:00
|
|
|
#: List of auth check functions.
|
|
|
|
auth_checks: list[typing.Callable] = []
|
|
|
|
|
|
|
|
#: Namespace of this endpoint.
|
2024-01-15 20:20:10 +00:00
|
|
|
namespace: str | None = None
|
|
|
|
|
|
|
|
#: The codec class.
|
|
|
|
codec: type[Codec] = DjangoJSONCodec
|
2022-06-04 08:41:53 +00:00
|
|
|
|
|
|
|
# pragma mark - Private interface
|
|
|
|
|
|
|
|
def ensure_auth(self, request: HttpRequest) -> None:
|
|
|
|
"""
|
|
|
|
Runs auth checks (if any) and raises
|
|
|
|
:py:exc:`django.core.exceptions.PermissionDenied` if any of them
|
|
|
|
returns ``False``.
|
|
|
|
|
|
|
|
:meta private:
|
|
|
|
"""
|
|
|
|
if len(self.auth_checks) == []:
|
|
|
|
return
|
|
|
|
|
|
|
|
has_auth = all((
|
|
|
|
auth_check(request)
|
|
|
|
for auth_check
|
|
|
|
in self.auth_checks
|
|
|
|
))
|
|
|
|
if has_auth is False:
|
|
|
|
raise PermissionDenied('This RPC endpoint requires auth.')
|
|
|
|
|
2024-01-15 20:20:10 +00:00
|
|
|
def get_executor(self, request: HttpRequest) -> DjangoExecutor:
|
|
|
|
"""
|
|
|
|
Returns an executor configured for the *request*.
|
|
|
|
|
|
|
|
:meta private:
|
|
|
|
"""
|
|
|
|
return self.executor(
|
|
|
|
request,
|
|
|
|
self.can_call,
|
|
|
|
self.namespace,
|
|
|
|
codec=self.get_codec(request),
|
|
|
|
)
|
|
|
|
|
2022-06-04 08:41:53 +00:00
|
|
|
def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
|
|
|
"""
|
|
|
|
Dispatches the *request*.
|
|
|
|
|
|
|
|
:meta private:
|
|
|
|
"""
|
|
|
|
if request.method.lower() in self.http_method_names:
|
|
|
|
handler = getattr(
|
|
|
|
self, request.method.lower(), self.http_method_not_allowed,
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
handler = self.http_method_not_allowed
|
|
|
|
|
|
|
|
self.ensure_auth(request)
|
|
|
|
|
2024-01-15 20:20:10 +00:00
|
|
|
return handler(request, *args, **kwargs) # type: ignore[misc]
|
2022-06-04 08:41:53 +00:00
|
|
|
|
|
|
|
def post(self, request: HttpRequest) -> HttpResponse:
|
|
|
|
"""
|
|
|
|
The POST handler.
|
|
|
|
|
|
|
|
:meta private:
|
|
|
|
"""
|
2024-01-15 20:20:10 +00:00
|
|
|
executor = self.get_executor(request)
|
2022-06-04 08:41:53 +00:00
|
|
|
|
|
|
|
serializer = executor.execute(request.body)
|
|
|
|
if serializer is None:
|
|
|
|
return HttpResponse('')
|
|
|
|
|
2024-01-15 20:20:10 +00:00
|
|
|
codec = self.get_codec(request)
|
|
|
|
|
|
|
|
return HttpResponse(
|
|
|
|
content=codec.encode(serializer.data),
|
|
|
|
content_type=codec.get_content_type(),
|
|
|
|
)
|
2022-06-04 08:41:53 +00:00
|
|
|
|
|
|
|
# pragma mark - Public interface
|
|
|
|
|
|
|
|
@classonlymethod
|
|
|
|
def as_view(cls, **initkwargs):
|
|
|
|
result = super().as_view(**initkwargs)
|
|
|
|
|
|
|
|
return csrf_exempt(result)
|
|
|
|
|
|
|
|
def can_call(self,
|
|
|
|
request: HttpRequest,
|
|
|
|
method: str,
|
|
|
|
args: list,
|
|
|
|
kwargs: dict) -> bool:
|
|
|
|
"""
|
|
|
|
Hook for subclasses to perform additional per-call permissions checks
|
|
|
|
etc. The default implementation returns ``True``.
|
|
|
|
"""
|
|
|
|
return True
|
2024-01-15 20:20:10 +00:00
|
|
|
|
|
|
|
def get_codec(self, request: HttpRequest) -> Codec:
|
|
|
|
"""Returns a codec configured for the *request*."""
|
|
|
|
return self.codec()
|