Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

Writer api #18

Merged
merged 15 commits into from
May 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,18 +128,29 @@ ______

**BRIDGEQL_AUTHENTICATION_DECORATOR**

Default: `''` (Empty string)
Default:
```
{
'reader': '',
'writer': ''
}
```

Above setting allows you to use an authentication decorator to authenticate your client's requests.
You can provide a custom authentication decorator whichever suits your application usecase, e.g login_required, same_subnet, etc.
You can provide a custom authentication decorator,
for reader and writer separately, whichever suits your application usecase, e.g login_required, same_subnet, etc.

Default value for `BRIDGEQL_AUTHENTICATION_DECORATOR` will allow you to access API without authentication.

```python
BRIDGEQL_AUTHENTICATION_DECORATOR = 'bridgeql.auth.basic_auth'
BRIDGEQL_AUTHENTICATION_DECORATOR = {
'reader': 'bridgeql.auth.basic_auth',
'writer': 'bridgeql.auth.basic_auth'
}
```

`bridgeql.auth.basic_auth` is available as a basic authentication method where you can pass authorization header as `Authorization: Basic base64(username:password)` for each request.
`bridgeql.auth.basic_auth` is available as a basic authentication method where you can pass authorization header as
`Authorization: Basic base64(username:password)` for each request.
____

### Build & Run
Expand Down
2 changes: 1 addition & 1 deletion bridgeql/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"""

__title__ = 'BridgeQL'
__version__ = '0.1.16'
__version__ = '0.2.0'
__license__ = 'BSD 2-Clause'
__copyright__ = 'Copyright © 2023 VMware, Inc. All rights reserved.'

Expand Down
18 changes: 14 additions & 4 deletions bridgeql/django/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,18 @@ def wrap(request, *args, **kwargs):
return wrap


if bridgeql_settings.BRIDGEQL_AUTHENTICATION_DECORATOR:
auth_decorator = load_function(
bridgeql_settings.BRIDGEQL_AUTHENTICATION_DECORATOR)
reader_auth = bridgeql_settings.BRIDGEQL_AUTHENTICATION_DECORATOR.get(
'reader', None
)
writer_auth = bridgeql_settings.BRIDGEQL_AUTHENTICATION_DECORATOR.get(
'writer', None
)
if reader_auth:
read_auth_decorator = load_function(reader_auth)
else:
def auth_decorator(func): return func
def read_auth_decorator(func): return func

if writer_auth:
write_auth_decorator = load_function(writer_auth)
else:
def write_auth_decorator(func): return func
66 changes: 46 additions & 20 deletions bridgeql/django/bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,23 @@
# SPDX-License-Identifier: BSD-2-Clause

import json
from django.views.decorators.http import require_GET

from bridgeql.django import logger
from bridgeql.django.auth import auth_decorator
from django.views.decorators.http import require_http_methods
from django.views.decorators.csrf import csrf_exempt

from bridgeql.django.auth import read_auth_decorator, write_auth_decorator
from bridgeql.django.exceptions import (
ForbiddenModelOrField,
BridgeqlException,
InvalidRequest
)
from bridgeql.django.helpers import JSONResponse
from bridgeql.django.models import ModelBuilder
from bridgeql.django.helpers import JSONResponse, get_json_request_body
from bridgeql.django.models import ModelBuilder, ModelObject

# TODO refine error handling


@auth_decorator
@require_GET
@require_http_methods(['GET'])
@read_auth_decorator
def read_django_model(request):
params = request.GET.get('payload', None)
try:
Expand All @@ -27,15 +28,40 @@ def read_django_model(request):
qset = mb.queryset() # get the result based on the given parameters
res = {'data': qset, 'message': '', 'success': True}
return JSONResponse(res)
except ForbiddenModelOrField as e:
logger.error(e)
res = {'data': [], 'message': str(e), 'success': False}
return JSONResponse(res, status=403)
except InvalidRequest as e:
logger.error(e)
res = {'data': [], 'message': str(e), 'success': False}
return JSONResponse(res, status=400)
except Exception as e:
logger.exception(e)
res = {'data': [], 'message': str(e), 'success': False}
return JSONResponse(res, status=500)
except BridgeqlException as e:
e.log()
res = {'data': [], 'message': str(e.detail), 'success': False}
return JSONResponse(res, status=e.status_code)


# no session to ride, hence no need for csrf protection
@csrf_exempt
@require_http_methods(['POST', 'PATCH'])
@write_auth_decorator
def write_django_model(request, app_label, model_name, **kwargs):
try:
params = get_json_request_body(request.body)
db_name = params.pop('bridgeql_writer_db', None)
pk = kwargs.pop('pk', None)
mo = ModelObject(app_label, model_name, db_name=db_name, pk=pk)
if mo.instance is None and request.method == 'POST':
obj = mo.create(params)
msg = 'Added new %s model, pk=%s' % (
obj._meta.model.__name__,
obj.pk
)
elif mo.instance and request.method == 'PATCH':
obj = mo.update(params)
msg = '%s updated, pk=%s, fields %s' % (
obj._meta.model.__name__,
obj.pk,
", ".join(params.keys()))
else:
raise InvalidRequest(
'Invalid request method %s for the url' % request.method)
res = {'data': obj.id, 'message': msg, 'success': True}
return JSONResponse(res)
except BridgeqlException as e:
e.log()
res = {'data': [], 'message': str(e.detail), 'success': False}
return JSONResponse(res, status=e.status_code)
43 changes: 38 additions & 5 deletions bridgeql/django/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,37 @@
# Copyright © 2023 VMware, Inc. All rights reserved.
# SPDX-License-Identifier: BSD-2-Clause

from bridgeql.django import logger

class ForbiddenModelOrField(Exception):
pass

class BridgeqlException(Exception):
status_code = 500
default_detail = 'A server error occurred.'

def __init__(self, detail=None):
self.detail = detail
if self.detail is None:
self.detail = self.default_detail

def __str__(self):
return str(self.detail)

def log(self):
if self.status_code >= 500:
logger.exception(self)
else:
logger.error(self)


class ForbiddenModelOrField(BridgeqlException):
status_code = 403
default_detail = 'Unauthorized access to forbidden ' \
'model or field'

class InvalidRequest(Exception):
pass

class InvalidRequest(BridgeqlException):
status_code = 400
default_detail = 'Invalid request received'


class InvalidAppOrModelName(InvalidRequest):
Expand All @@ -23,5 +47,14 @@ class InvalidQueryException(InvalidRequest):
pass


class InvalidBridgeQLSettings(Exception):
class InvalidPKException(InvalidRequest):
pass


class InvalidBridgeQLSettings(BridgeqlException):
pass


class ObjectNotFound(BridgeqlException):
status_code = 404
default_detail = 'Object not found'
26 changes: 19 additions & 7 deletions bridgeql/django/helpers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright © 2023 VMware, Inc. All rights reserved.
# SPDX-License-Identifier: BSD-2-Clause

import os
from datetime import datetime
import json
Expand All @@ -11,15 +10,14 @@
from django.db.models.query import QuerySet
from django.http import HttpResponse


from bridgeql.django.exceptions import InvalidBridgeQLSettings
from bridgeql.django.exceptions import InvalidRequest
from bridgeql.django.settings import bridgeql_settings


class JSONEncoder(json.JSONEncoder):
'''
"""
Encode an object in JSON.
'''
"""

def default(self, obj):
if isinstance(obj, datetime):
Expand All @@ -30,9 +28,9 @@ def default(self, obj):


class JSONResponse(HttpResponse):
'''
"""
Create a response that contains a JSON string.
'''
"""

def __init__(self, content,
content_type='application/json; charset=utf-8', status=200,
Expand Down Expand Up @@ -62,3 +60,17 @@ def get_local_apps():

def get_allowed_apps():
return bridgeql_settings.BRIDGEQL_ALLOWED_APPS or get_local_apps()


def get_json_request_body(body):
try:
params = json.loads(body)
payload = params.get('payload', None)
if payload is None:
raise InvalidRequest('payload is not present in request body')
if not isinstance(payload, dict):
raise InvalidRequest(
'Incorrect payload type, Expected dict, got %s' % type(payload))
return payload
except ValueError as e:
raise InvalidRequest(str(e))
62 changes: 61 additions & 1 deletion bridgeql/django/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@
# SPDX-License-Identifier: BSD-2-Clause

from django.apps import apps
from django.core.exceptions import FieldDoesNotExist, FieldError
from django.core.exceptions import (
FieldDoesNotExist,
FieldError,
ValidationError,
ObjectDoesNotExist
)
from django.db.models import QuerySet
from django.db.models.base import ModelBase
from django.db.utils import IntegrityError

from bridgeql.django import logger
from bridgeql.django.exceptions import (
Expand All @@ -14,6 +20,8 @@
InvalidAppOrModelName,
InvalidModelFieldName,
InvalidQueryException,
InvalidPKException,
ObjectNotFound
)
from bridgeql.django.fields import Field, FieldAttributes
from bridgeql.django.query import Query
Expand Down Expand Up @@ -138,6 +146,58 @@ def validate_fields(self, query_fields):
return True


class ModelObject(object):
def __init__(self, app_label, model_name, **kwargs):
self.model_config = ModelConfig(app_label, model_name)
self.instance = None
self.db_name = kwargs.pop('bridgeql_writer_db', None)
pk = kwargs.pop('pk', None)
if pk:
obj_manager = self.model_config.model.objects
if self.db_name:
obj_manager = obj_manager.using(self.db_name)
# throw error if more than one value found
try:
self.instance = obj_manager.get(pk=pk)
except ValueError as e:
raise InvalidPKException(str(e))
except ObjectDoesNotExist as e:
raise ObjectNotFound(str(e))

def update(self, params):
# TODO check if there are any restricted fields in data
try:
for key, val in params.items():
if hasattr(self.instance, key):
setattr(self.instance, key, val)
else:
raise InvalidRequest('%s does not have field %s'
% (self.instance._meta.model.__name__,
key))
setattr(self.instance, key, val)
# Perform validation
self.instance.validate_unique()
self.instance.save()
except (ValidationError, IntegrityError, AttributeError) as e:
raise InvalidRequest(str(e))
return self.instance

def create(self, params):
# TODO validate data
save_kwargs = {}
# if db_name is None then save() function will use default db
if self.db_name:
save_kwargs['using'] = self.db_name
self.instance = self.model_config.model(**params)
# Perform validation
try:
self.instance.validate_unique()
self.instance.save(**save_kwargs)
except (ValidationError, IntegrityError) as e:
raise InvalidRequest(str(e))
return self.instance


class ModelBuilder(object):
_QUERYSET_OPTS = [
('exclude', 'exclude', dict),
Expand Down
Loading