Skip to content
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
4 changes: 2 additions & 2 deletions .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: Run tests

on:
push:
branches: [ master ]
branches: [ master, generate-frontend ]
pull_request:
branches: [ master ]
branches: [ master, generate-frontend ]

jobs:
build:
Expand Down
4 changes: 3 additions & 1 deletion simple_api/adapters/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from simple_api.object.registry import object_storage
from simple_api.object.meta_types import build_object_info, build_action_info
from simple_api.object.meta_types import build_object_info, build_action_info, build_type_info


class TemplateGenerator:
Expand All @@ -16,7 +16,9 @@ def generate(adapter, extra_actions=None):
action.set_name(action_name)

object_info = build_object_info()
type_info = build_type_info()
action_info = build_action_info(extra_actions)
extra_actions["__types"] = type_info
extra_actions["__objects"] = object_info
extra_actions["__actions"] = action_info

Expand Down
22 changes: 11 additions & 11 deletions simple_api/django_object/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from simple_api.django_object.utils import determine_items, remove_item
from simple_api.object.actions import Action, ToActionMixin, SetReferencesMixin
from simple_api.object.datatypes import ObjectType, BooleanType
from .permissions import permission_class_with_get_fn
from ..utils import ensure_tuple


class WithObjectMixin:
Expand All @@ -23,7 +25,7 @@ def __init__(self, parameters=None, data=None, return_value=None, exec_fn=None,
self.data = data or {}
self.return_value = return_value or ObjectType("self")
self.exec_fn = exec_fn
self.permissions = permissions
self.permissions = ensure_tuple(permissions)
self.kwargs = kwargs

# these attributes exist to ensure that 1) the input from the user is stored unmodified in the attributes above
Expand Down Expand Up @@ -111,17 +113,15 @@ def fn(request, params, obj, **kwargs):
# injects get_fn into the permission classes so that this information can be used to determine permissions
def determine_permissions(self):
if self._determined_permissions is None:
self._determined_permissions = self.permissions
if self._determined_permissions is None:
return
if not isinstance(self._determined_permissions, (list, tuple)):
self._determined_permissions = self._determined_permissions,

self._determined_permissions = []
self.determine_get_fn()
instantiated_permissions = []
for pc in self._determined_permissions:
instantiated_permissions.append(pc(get_fn=self._determined_get_fn))
self._determined_permissions = instantiated_permissions
for pc in self.permissions:
# here we extract the information from a permission class and use it to build a new class with the
# function to get the related object embedded; this is because permissions are supposed to be classes
# and not their instances and since django actions are implemented as composition with respect to
# actions, there would be a problem with actions seeing instantiated permission classes, which would
# create problems later
self._determined_permissions.append(permission_class_with_get_fn(pc, self._determined_get_fn))

def determine_parameters(self):
if self._determined_parameters is None:
Expand Down
6 changes: 1 addition & 5 deletions simple_api/django_object/django_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from simple_api.django_object.filters import generate_filters
from simple_api.django_object.converter import determine_simple_api_fields
from simple_api.django_object.utils import get_pk_field
from simple_api.object.datatypes import StringType
from simple_api.object.object import Object, ObjectMeta
from simple_api.object.registry import object_storage
from simple_api.django_object.registry import model_django_object_storage
Expand Down Expand Up @@ -45,8 +44,6 @@ def __new__(mcs, name, bases, attrs, **kwargs):
cls.custom_fields, cls.input_custom_fields, cls.output_custom_fields,
)

output_fields["__str__"] = StringType(resolver=lambda *a, **kw: kw["parent_val"]())

for f in input_fields:
assert f not in fields, "Redefinition of `{}` field.".format(f)
cls.in_fields = {**fields, **input_fields}
Expand All @@ -60,8 +57,7 @@ def __new__(mcs, name, bases, attrs, **kwargs):
object_stub.add_attr("output_fields", output_fields)

# create filters and List type for potential listing actions
cls.filter_type = ObjectMeta("{}Filters".format(cls.__name__), (Object,), {"fields": generate_filters(cls),
"hidden": True})
cls.filter_type = ObjectMeta("{}Filters".format(cls.__name__), (Object,), {"fields": generate_filters(cls)})
object_stub.add_attr("filter_type", cls.filter_type)
create_associated_list_type(cls)

Expand Down
24 changes: 15 additions & 9 deletions simple_api/django_object/permissions.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
from simple_api.object.permissions import BasePermission


class DjangoPermission(BasePermission):
def __init__(self, get_fn=None, **kwargs):
self.get_fn = get_fn
super().__init__(**kwargs)
def permission_class_with_get_fn(permission_class, get_fn):
# takes a permission class and embeds the get_fn of the object into it; this is so that it can be passed around
# as a class and there is no need to instantiate it (otherwise we would run into trying to instantiate something
# that has already been instantiated)
class _DjangoPermission:
def has_permission(self, **kwargs):
obj = kwargs.pop("obj")
if obj is None:
obj = get_fn(**kwargs)
return permission_class().has_permission(obj=obj, **kwargs)

def error_message(self, **kwargs):
return permission_class().error_message(**kwargs)
return _DjangoPermission

def has_permission(self, exclude_classes=(), **kwargs):
obj = kwargs.pop("obj")
if obj is None and self.get_fn is not None:
obj = self.get_fn(**kwargs)
return super().has_permission(exclude_classes=(DjangoPermission,) + exclude_classes, obj=obj, **kwargs)

class DjangoPermission(BasePermission):
def permission_statement(self, request, obj, **kwargs):
raise NotImplementedError

Expand Down
39 changes: 39 additions & 0 deletions simple_api/object/meta_resolvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ def object_info(**kwargs):
for cls in object_storage.storage.values():
if getattr(cls, "hidden", False):
continue
if not cls.actions:
continue
item = {
"name": cls.__name__,
"pk_field": getattr(cls, "pk_field", None),
Expand All @@ -19,18 +21,52 @@ def object_info(**kwargs):
return out


def type_info(**kwargs):
out = []
for cls in object_storage.storage.values():
if getattr(cls, "hidden", False):
continue

fields = []
for field_name, field in cls.out_fields.items():
if not field_name.startswith("__"):
fields.append(build_field_info(field_name, field))

item = {
"typename": cls.__name__,
"fields": fields
}
out.append(item)
return out


def build_action_info_fn(actions):
dummy_cls = AttrDict(__name__="", actions=deepcopy(actions))
return build_actions_resolver(dummy_cls, with_object=False)


def build_field_info(field_name, field):
return {
"name": field_name,
"typename": str(field)
}


def build_actions_resolver(cls, with_object=True):
def actions_resolver(**kwargs):
out = []
for action in cls.actions.values():
if action.with_object != with_object or action.hidden:
continue

params = []
for param_name, param in action.parameters.items():
params.append(build_field_info(param_name, param))

data = []
for field_name, field in action.data.items():
data.append(build_field_info(field_name, field))

try:
action.has_permission(**kwargs)
permitted = True
Expand All @@ -47,6 +83,9 @@ def actions_resolver(**kwargs):
"permitted": permitted,
"deny_reason": deny_reason,
"retry_in": action.retry_in,
"return_type": str(action.return_value),
"parameters": params,
"data": data,
}
out.append(action_item)
return out
Expand Down
27 changes: 26 additions & 1 deletion simple_api/object/meta_types.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,32 @@
from simple_api.object.actions import Action
from simple_api.object.meta_resolvers import object_info, build_action_info_fn
from simple_api.object.meta_resolvers import object_info, type_info, build_action_info_fn
from simple_api.object.object import Object
from simple_api.object.datatypes import PlainListType, ObjectType, DurationType, StringType, BooleanType


class FieldInfo(Object):
fields = {
"name": StringType(),
"typename": StringType()
}
hidden = True


class TypeInfo(Object):
fields = {
"typename": StringType(),
"fields": PlainListType(ObjectType(FieldInfo))
}
hidden = True


class ActionInfo(Object):
fields = {
"name": StringType(),
"parameters": PlainListType(ObjectType(FieldInfo)),
"data": PlainListType(ObjectType(FieldInfo)),
"return_type": StringType(),

"permitted": BooleanType(),
"deny_reason": StringType(nullable=True),
"retry_in": DurationType(nullable=True),
Expand All @@ -28,6 +48,11 @@ def build_object_info():
exec_fn=object_info)


def build_type_info():
return Action(return_value=PlainListType(ObjectType(TypeInfo)),
exec_fn=type_info)


def build_action_info(actions):
return Action(return_value=PlainListType(ObjectType(ActionInfo)),
exec_fn=build_action_info_fn(actions))
4 changes: 3 additions & 1 deletion simple_api/object/object.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from django.utils.decorators import classproperty

from simple_api.object.datatypes import PlainListType, ObjectType
from simple_api.object.datatypes import PlainListType, ObjectType, StringType
from simple_api.object.meta_resolvers import build_actions_resolver
from simple_api.object.registry import object_storage

Expand Down Expand Up @@ -36,6 +36,8 @@ def __new__(mcs, name, bases, attrs, **kwargs):
cls.output_fields = deepcopy(cls.output_fields)
cls.actions = deepcopy(cls.actions)

cls.output_fields["__str__"] = StringType(resolver=lambda *a, **kw: kw["parent_val"]())

if "module" in kwargs:
cls.__module__ = kwargs["module"]

Expand Down
43 changes: 20 additions & 23 deletions simple_api/object/permissions.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,11 @@
from inspect import isclass
from simple_api.utils import ensure_tuple


# instantiates permission classes (if they are not already, maybe due to get_fn injection) and builds a
# function that raises if the permissions are not passed
def build_permissions_fn(permissions):
instantiated_permissions = []
for cls_or_inst in permissions:
if isclass(cls_or_inst) or isinstance(cls_or_inst, LogicalConnector):
instantiated_permissions.append(cls_or_inst())
else:
instantiated_permissions.append(cls_or_inst)

def fn(**kwargs):
for perm in instantiated_permissions:
if not perm.has_permission(**kwargs):
raise PermissionError(perm.error_message(**kwargs))
for perm in permissions:
if not perm().has_permission(**kwargs):
raise PermissionError(perm().error_message(**kwargs))
return fn


Expand All @@ -26,12 +16,21 @@ def __init__(self, **kwargs):
def permission_statement(self, **kwargs):
raise NotImplementedError

def has_permission(self, exclude_classes=(), **kwargs):
def has_permission(self, **kwargs):
# to achieve hierarchical checking for permissions (a subclass calls the permission statement of the superclass
# and only if it passes, executes its own), we need to traverse over the whole linearization order of the
# permission class; however, classes like object, which are naturally also a part of the chain, do not
# contain the `permission_statement` method and therefore should be just skipped; the same is true for abstract
# permission classes which contain the method, but is not implemented - like this one for example: to achieve
# this, we try calling the method and if it turns out to not be implemented, we skip it as well
for cls in reversed(self.__class__.__mro__):
if cls in (object, BasePermission, LogicalResolver) + exclude_classes:
if not hasattr(cls, "permission_statement"):
continue
try:
if not cls.permission_statement(self, **kwargs):
return False
except NotImplementedError:
continue
if not cls.permission_statement(self, **kwargs):
return False
return True

def error_message(self, **kwargs):
Expand All @@ -43,12 +42,10 @@ def __init__(self, *permissions):
self.permissions = permissions

def __call__(self, **kwargs):
instantiated_perms = []
for perm in self.permissions:
assert isclass(perm) or isinstance(perm, LogicalConnector), \
"Permissions in logical connectors must be classes."
instantiated_perms.append(perm(**kwargs))
return LogicalResolver(instantiated_perms, self.resolve_fn)
return LogicalResolver(self.permissions, self.resolve_fn)

def resolve_fn(self, permissions, **kwargs):
raise NotImplementedError
Expand All @@ -69,23 +66,23 @@ def error_message(self, **kwargs):
class Or(LogicalConnector):
def resolve_fn(self, permissions, **kwargs):
for perm in permissions:
if perm.has_permission(**kwargs):
if perm().has_permission(**kwargs):
return True
return False


class And(LogicalConnector):
def resolve_fn(self, permissions, **kwargs):
for perm in permissions:
if not perm.has_permission(**kwargs):
if not perm().has_permission(**kwargs):
return False
return True


class Not(LogicalConnector):
def resolve_fn(self, permissions, **kwargs):
assert len(permissions) == 1, "`Not` accepts only one permission class as parameter."
return not permissions[0].has_permission(**kwargs)
return not permissions[0]().has_permission(**kwargs)


class AllowAll(BasePermission):
Expand Down