From 1c4fabbfa2cc726dbd8bd3ea191b8fd346e586fb Mon Sep 17 00:00:00 2001 From: TehomCD Date: Sun, 1 Mar 2020 19:11:04 -0500 Subject: [PATCH] Add more fields to serializers --- evennia/web/api/permissions.py | 50 +++++++++++---- evennia/web/api/serializers.py | 112 +++++++++++++++++++++++++++++---- evennia/web/api/tests.py | 26 +++++--- evennia/web/api/views.py | 7 ++- requirements.txt | 4 +- 5 files changed, 161 insertions(+), 38 deletions(-) diff --git a/evennia/web/api/permissions.py b/evennia/web/api/permissions.py index dd1a52c952..b68fa0769c 100644 --- a/evennia/web/api/permissions.py +++ b/evennia/web/api/permissions.py @@ -17,11 +17,20 @@ class EvenniaPermission(permissions.BasePermission): update_locks = settings.REST_FRAMEWORK.get("DEFAULT_UPDATE_LOCKS", ["control", "edit"]) def has_permission(self, request, view): - """ - This method is a check that always happens first. If there's an object involved, - such as with retrieve, update, or delete, then the has_object_permission method - is also called if this returns True. If we return False, a permission denied - error is raised. + """Checks for permissions + + Args: + request (Request): The incoming request object. + view (View): The django view we are checking permission for. + + Returns: + bool: If permission is granted or not. If we return False here, a PermissionDenied + error will be raised from the view. + + Notes: + This method is a check that always happens first. If there's an object involved, + such as with retrieve, update, or delete, then the has_object_permission method + is called after this, assuming this returns `True`. """ # Only allow authenticated users to call the API if not request.user.is_authenticated: @@ -37,17 +46,34 @@ class EvenniaPermission(permissions.BasePermission): @staticmethod def check_locks(obj, user, locks): + """Checks access for user for object with given locks + Args: + obj: Object instance we're checking + user (Account): User who we're checking permissions + locks (list): list of lockstrings + + Returns: + bool: True if they have access, False if they don't + """ return any([obj.access(user, lock) for lock in locks]) def has_object_permission(self, request, view, obj): + """Checks object-level permissions after has_permission + + Args: + request (Request): The incoming request object. + view (View): The django view we are checking permission for. + obj: Object we're checking object-level permissions for + + Returns: + bool: If permission is granted or not. If we return False here, a PermissionDenied + error will be raised from the view. + + Notes: + This method assumes that has_permission has already returned True. We check + equivalent Evennia permissions in the request.user to determine if they can + complete the action. """ - This method assumes that has_permission has already returned True. We check - equivalent Evennia permissions in the request.user to determine if they can - complete the action. If so, we return True. Otherwise we return False, and - a permission denied error will be raised. - """ - if request.user.is_superuser: - return True if view.action in ("list", "retrieve"): # access_type is based on the examine command return self.check_locks(obj, request.user, self.view_locks) diff --git a/evennia/web/api/serializers.py b/evennia/web/api/serializers.py index 7060df75ee..117ca0a816 100644 --- a/evennia/web/api/serializers.py +++ b/evennia/web/api/serializers.py @@ -11,8 +11,8 @@ often django model instances, that we can use (deserialization). from rest_framework import serializers -from evennia.objects.models import ObjectDB -from evennia.accounts.models import AccountDB +from evennia.objects.objects import DefaultObject +from evennia.accounts.accounts import DefaultAccount from evennia.scripts.models import ScriptDB from evennia.typeclasses.attributes import Attribute from evennia.typeclasses.tags import Tag @@ -32,36 +32,122 @@ class TagSerializer(serializers.ModelSerializer): class SimpleObjectDBSerializer(serializers.ModelSerializer): class Meta: - model = ObjectDB + model = DefaultObject fields = ["id", "db_key"] class TypeclassSerializerMixin(object): + """Mixin that contains types shared by typeclasses. A note about tags, aliases, and permissions. You + might note that the methods and fields are defined here, but they're included explicitly in each child + class. What gives? It's a DRF error: serializer method fields which are inherited do not resolve correctly + in child classes, and as of this current version (3.11) you must have them in the child classes explicitly + to avoid field errors. + """ db_attributes = AttributeSerializer(many=True) - db_tags = TagSerializer(many=True) - shared_fields = ["id", "db_key", "db_attributes", "db_tags", "db_typeclass_path"] + shared_fields = ["id", "db_key", "db_attributes", "db_typeclass_path", "aliases", "tags", "permissions"] + + def get_tags(self, obj): + """ + Serializes tags from the object's Tagshandler + Args: + obj: Typeclassed object being serialized + + Returns: + List of TagSerializer data + """ + return TagSerializer(obj.tags.get(return_tagobj=True, return_list=True), many=True).data + + def get_aliases(self, obj): + """ + Serializes tags from the object's Aliashandler + Args: + obj: Typeclassed object being serialized + + Returns: + List of TagSerializer data + """ + return TagSerializer(obj.aliases.get(return_tagobj=True, return_list=True), many=True).data + + def get_permissions(self, obj): + """ + Serializes tags from the object's Permissionshandler + Args: + obj: Typeclassed object being serialized + + Returns: + List of TagSerializer data + """ + return TagSerializer(obj.permissions.get(return_tagobj=True, return_list=True), many=True).data class ObjectDBSerializer(TypeclassSerializerMixin, serializers.ModelSerializer): - contents = SimpleObjectDBSerializer(source="locations_set", many=True, read_only=True) + contents = serializers.SerializerMethodField() + exits = serializers.SerializerMethodField() + tags = serializers.SerializerMethodField() + aliases = serializers.SerializerMethodField() + permissions = serializers.SerializerMethodField() class Meta: - model = ObjectDB - fields = ["db_location", "db_home", "contents"] + TypeclassSerializerMixin.shared_fields - read_only_fields = ["id", "db_attributes", "db_tags"] + model = DefaultObject + fields = ["db_location", "db_home", "contents", "exits"] + TypeclassSerializerMixin.shared_fields + read_only_fields = ["id", "db_attributes"] + + def get_exits(self, obj): + """ + Gets exits for the object + Args: + obj: Object being serialized + + Returns: + List of data from SimpleObjectDBSerializer + """ + exits = [ob for ob in obj.contents if ob.destination] + return SimpleObjectDBSerializer(exits, many=True).data + + def get_contents(self, obj): + """ + Gets non-exits for the object + Args: + obj: Object being serialized + + Returns: + List of data from SimpleObjectDBSerializer + """ + non_exits = [ob for ob in obj.contents if not ob.destination] + return SimpleObjectDBSerializer(non_exits, many=True).data -class AccountDBSerializer(TypeclassSerializerMixin, serializers.ModelSerializer): +class AccountSerializer(TypeclassSerializerMixin, serializers.ModelSerializer): + """This uses the DefaultAccount object to have access to the sessions property""" db_key = serializers.CharField(required=False) + session_ids = serializers.SerializerMethodField() + tags = serializers.SerializerMethodField() + aliases = serializers.SerializerMethodField() + permissions = serializers.SerializerMethodField() + + def get_session_ids(self, obj): + """ + Gets a list of session IDs connected to this Account + Args: + obj (DefaultAccount): Account we're grabbing sessions from + + Returns: + List of session IDs + """ + return [sess.sessid for sess in obj.sessions.all() if hasattr(sess, "sessid")] class Meta: - model = AccountDB - fields = ["username"] + TypeclassSerializerMixin.shared_fields - read_only_fields = ["id", "db_attributes", "db_tags"] + model = DefaultAccount + fields = ["username", "session_ids"] + TypeclassSerializerMixin.shared_fields + read_only_fields = ["id", "db_attributes", "db_tags", "session_ids"] class ScriptDBSerializer(TypeclassSerializerMixin, serializers.ModelSerializer): + tags = serializers.SerializerMethodField() + aliases = serializers.SerializerMethodField() + permissions = serializers.SerializerMethodField() + class Meta: model = ScriptDB fields = ["db_interval", "db_persistent", "db_start_delay", diff --git a/evennia/web/api/tests.py b/evennia/web/api/tests.py index 50b35a8bbe..4e17b424b9 100644 --- a/evennia/web/api/tests.py +++ b/evennia/web/api/tests.py @@ -26,6 +26,8 @@ class TestEvenniaRESTApi(EvenniaTest): self.account.is_superuser = True self.account.save() self.client.force_login(self.account) + # scripts do not have default locks. Without them, even superuser access check fails + self.script.locks.add("edit: perm(Admin); examine: perm(Admin); delete: perm(Admin)") def tearDown(self): try: @@ -35,21 +37,27 @@ class TestEvenniaRESTApi(EvenniaTest): def get_view_details(self, action): """Helper function for generating list of named tuples""" - View = namedtuple("View", ["view_name", "obj", "list", "serializer", "create_data"]) + View = namedtuple("View", ["view_name", "obj", "list", "serializer", "create_data", "retrieve_data"]) views = [ View("object-%s" % action, self.obj1, [self.obj1, self.char1, self.exit, self.room1, self.room2, self.obj2, self.char2], serializers.ObjectDBSerializer, - {"db_key": "object-create-test-name"}), + {"db_key": "object-create-test-name"}, + serializers.ObjectDBSerializer(self.obj1).data), View("character-%s" % action, self.char1, [self.char1, self.char2], serializers.ObjectDBSerializer, - {"db_key": "character-create-test-name"}), + {"db_key": "character-create-test-name"}, + serializers.ObjectDBSerializer(self.char1).data), View("exit-%s" % action, self.exit, [self.exit], serializers.ObjectDBSerializer, - {"db_key": "exit-create-test-name"}), + {"db_key": "exit-create-test-name"}, + serializers.ObjectDBSerializer(self.exit).data), View("room-%s" % action, self.room1, [self.room1, self.room2], serializers.ObjectDBSerializer, - {"db_key": "room-create-test-name"}), + {"db_key": "room-create-test-name"}, + serializers.ObjectDBSerializer(self.room1).data), View("script-%s" % action, self.script, [self.script], serializers.ScriptDBSerializer, - {"db_key": "script-create-test-name"}), - View("account-%s" % action, self.account2, [self.account, self.account2], serializers.AccountDBSerializer, - {"username": "account-create-test-name"}), + {"db_key": "script-create-test-name"}, + serializers.ScriptDBSerializer(self.script).data), + View("account-%s" % action, self.account2, [self.account, self.account2], serializers.AccountSerializer, + {"username": "account-create-test-name"}, + serializers.AccountSerializer(self.account2).data), ] return views @@ -62,6 +70,7 @@ class TestEvenniaRESTApi(EvenniaTest): ) response = self.client.get(view_url) self.assertEqual(response.status_code, 200) + self.assertDictEqual(response.data, view.retrieve_data) def test_update(self): views = self.get_view_details("detail") @@ -78,6 +87,7 @@ class TestEvenniaRESTApi(EvenniaTest): self.assertEqual(response.status_code, 200) view.obj.refresh_from_db() self.assertEqual(getattr(view.obj, field), new_key) + self.assertEqual(response.data[field], new_key) def test_delete(self): views = self.get_view_details("detail") diff --git a/evennia/web/api/views.py b/evennia/web/api/views.py index 0055431e0d..1c5988f1db 100644 --- a/evennia/web/api/views.py +++ b/evennia/web/api/views.py @@ -14,7 +14,7 @@ from evennia.objects.models import ObjectDB from evennia.objects.objects import DefaultCharacter, DefaultExit, DefaultRoom from evennia.accounts.models import AccountDB from evennia.scripts.models import ScriptDB -from evennia.web.api.serializers import ObjectDBSerializer, AccountDBSerializer, ScriptDBSerializer, AttributeSerializer +from evennia.web.api.serializers import ObjectDBSerializer, AccountSerializer, ScriptDBSerializer, AttributeSerializer from evennia.web.api.filters import ObjectDBFilterSet, AccountDBFilterSet, ScriptDBFilterSet from evennia.web.api.permissions import EvenniaPermission @@ -26,7 +26,8 @@ class TypeclassViewSetMixin(object): """ # permission classes determine who is authorized to call the view permission_classes = [EvenniaPermission] - # the filter backend allows for retrieval views to have filter arguments passed to it + # the filter backend allows for retrieval views to have filter arguments passed to it, + # for example: mygame.com/api/objects?db_key=bob to find matches based on objects having a db_key of bob filter_backends = [DjangoFilterBackend] @action(detail=True, methods=["put", "post"]) @@ -93,7 +94,7 @@ class ExitViewSet(ObjectDBViewSet): class AccountDBViewSet(TypeclassViewSetMixin, ModelViewSet): """Viewset for Account objects""" - serializer_class = AccountDBSerializer + serializer_class = AccountSerializer queryset = AccountDB.objects.all() filterset_class = AccountDBFilterSet diff --git a/requirements.txt b/requirements.txt index 3da37476d4..8ebbe7cce7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,8 +5,8 @@ attrs >= 19.2.0 django >= 2.2.5, < 2.3 twisted >= 19.2.1, < 20.0.0 pytz -djangorestframework >= 3.10.3 -django-filter >= 2.2.0 +djangorestframework >= 3.10.3, < 3.12 +django-filter >= 2.2.0, < 2.3 django-sekizai inflect autobahn >= 17.9.3