Skip to content

Commit

Permalink
Merge pull request #86 from maykinmedia/feature/object-type-permissions
Browse files Browse the repository at this point in the history
Feature/object type permissions
  • Loading branch information
annashamray committed Oct 26, 2020
2 parents 9226e6b + 978f905 commit 2c6cbe6
Show file tree
Hide file tree
Showing 17 changed files with 460 additions and 21 deletions.
12 changes: 11 additions & 1 deletion src/objects/accounts/admin.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.utils.translation import ugettext_lazy as _

from hijack_admin.admin import HijackUserAdminMixin

from .models import User
from .models import ObjectPermission, User


@admin.register(User)
class _UserAdmin(UserAdmin, HijackUserAdminMixin):
list_display = UserAdmin.list_display + ("hijack_field",)
fieldsets = UserAdmin.fieldsets + (
(_("Object permissions"), {"fields": ("object_permissions",)}),
)
raw_id_fields = ("object_permissions",)


@admin.register(ObjectPermission)
class ObjectPermissionAdmin(admin.ModelAdmin):
list_display = ("object_type", "mode")
8 changes: 8 additions & 0 deletions src/objects/accounts/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.utils.translation import ugettext_lazy as _

from djchoices import ChoiceItem, DjangoChoices


class PermissionModes(DjangoChoices):
read_only = ChoiceItem("read_only", _("Read-only"))
read_and_write = ChoiceItem("read_and_write", _("Read and write"))
57 changes: 57 additions & 0 deletions src/objects/accounts/migrations/0002_auto_20201012_1522.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Generated by Django 2.2.12 on 2020-10-12 13:22

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("accounts", "0001_initial"),
]

operations = [
migrations.CreateModel(
name="ObjectPermission",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"object_type",
models.URLField(
help_text="Url reference to OBJECTTYPE in Objecttypes API",
verbose_name="object type",
),
),
(
"mode",
models.CharField(
choices=[
("read_only", "Read-only"),
("read_and_write", "Read and write"),
],
help_text="Permission mode",
max_length=20,
verbose_name="mode",
),
),
],
options={
"verbose_name": "object permission",
"verbose_name_plural": "object permissions",
},
),
migrations.AddField(
model_name="user",
name="object_permissions",
field=models.ManyToManyField(
blank=True, related_name="users", to="accounts.ObjectPermission"
),
),
]
25 changes: 25 additions & 0 deletions src/objects/accounts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _

from .constants import PermissionModes
from .managers import UserManager


Expand Down Expand Up @@ -37,6 +38,9 @@ class User(AbstractBaseUser, PermissionsMixin):
),
)
date_joined = models.DateTimeField(_("date joined"), default=timezone.now)
object_permissions = models.ManyToManyField(
"accounts.ObjectPermission", related_name="users", blank=True
)

objects = UserManager()

Expand All @@ -57,3 +61,24 @@ def get_full_name(self):
def get_short_name(self):
"Returns the short name for the user."
return self.first_name

def get_permission_for_object_type(self, object_type):
if not self.object_permissions.filter(object_type=object_type).exists():
return None
return self.object_permissions.get(object_type=object_type)


class ObjectPermission(models.Model):
object_type = models.URLField(
_("object type"), help_text=_("Url reference to OBJECTTYPE in Objecttypes API")
)
mode = models.CharField(
_("mode"),
max_length=20,
choices=PermissionModes.choices,
help_text=_("Permission mode"),
)

class Meta:
verbose_name = _("object permission")
verbose_name_plural = _("object permissions")
38 changes: 38 additions & 0 deletions src/objects/accounts/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from rest_framework.permissions import SAFE_METHODS, BasePermission
from vng_api_common.permissions import bypass_permissions

from objects.accounts.constants import PermissionModes


class ObjectBasedPermission(BasePermission):
def has_permission(self, request, view):
if bypass_permissions(request):
return True

# user should be authenticated
if not (request.user and request.user.is_authenticated):
return False

# detail actions are processed in has_object_permission method
if view.action != "create":
return True

object_type = request.data["type"]
object_permission = request.user.get_permission_for_object_type(object_type)
return bool(
object_permission
and object_permission.mode == PermissionModes.read_and_write
)

def has_object_permission(self, request, view, obj):
if bypass_permissions(request):
return True

object_permission = request.user.get_permission_for_object_type(obj.object_type)
if not object_permission:
return False

if request.method in SAFE_METHODS:
return True

return bool(object_permission.mode == PermissionModes.read_and_write)
29 changes: 29 additions & 0 deletions src/objects/accounts/tests/factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import factory
import factory.fuzzy

from ..constants import PermissionModes


class UserFactory(factory.django.DjangoModelFactory):
username = factory.Sequence(lambda n: f"user-{n}")

class Meta:
model = "accounts.User"


class ObjectPermissionFactory(factory.django.DjangoModelFactory):
object_type = factory.Faker("url")
mode = factory.fuzzy.FuzzyChoice(choices=PermissionModes.values)

class Meta:
model = "accounts.ObjectPermission"

@factory.post_generation
def users(self, create, extracted, **kwargs):
# optional M2M, do nothing when no arguments are passed
if not create:
return

if extracted:
for user in extracted:
self.users.add(user)
10 changes: 10 additions & 0 deletions src/objects/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from rest_framework.response import Response
from vng_api_common.search import SearchMixin

from objects.accounts.permissions import ObjectBasedPermission
from objects.core.models import Object

from .filters import ObjectFilterSet
Expand All @@ -23,6 +24,15 @@ class ObjectViewSet(SearchMixin, GeoMixin, viewsets.ModelViewSet):
filterset_class = ObjectFilterSet
lookup_field = "uuid"
search_input_serializer_class = ObjectSearchSerializer
permission_classes = [ObjectBasedPermission]

def get_queryset(self):
base = super().get_queryset()

if self.action not in ("list", "search"):
return base

return base.filter_for_user(self.request.user)

@swagger_auto_schema(responses={"200": HistoryRecordSerializer(many=True)})
@action(detail=True, methods=["get"], serializer_class=HistoryRecordSerializer)
Expand Down
1 change: 0 additions & 1 deletion src/objects/conf/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.TokenAuthentication"
],
"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
# test
"TEST_REQUEST_DEFAULT_FORMAT": "json",
}
Expand Down
3 changes: 3 additions & 0 deletions src/objects/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _

from .query import ObjectQuerySet
from .utils import check_objecttype


Expand All @@ -17,6 +18,8 @@ class Object(models.Model):
_("object type"), help_text=_("Url reference to OBJECTTYPE in Objecttypes API")
)

objects = ObjectQuerySet.as_manager()

@property
def current_record(self):
today = date.today()
Expand Down
7 changes: 7 additions & 0 deletions src/objects/core/query.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.db import models


class ObjectQuerySet(models.QuerySet):
def filter_for_user(self, user):
allowed_object_types = user.object_permissions.values("object_type")
return self.filter(object_type__in=models.Subquery(allowed_object_types))

0 comments on commit 2c6cbe6

Please sign in to comment.