From c52e7769436fa4adc773ed91ebb2ab8070f9aaf4 Mon Sep 17 00:00:00 2001 From: Kyle Rockman Date: Sun, 17 Sep 2017 08:10:14 -0500 Subject: [PATCH] Add Ownership of a namespace --- .../js/components/DashboardListView.jsx | 4 +- .../js/components/TerraformNamespaceItem.jsx | 15 ++++--- .../js/components/TerraformNamespaceList.jsx | 12 ++++++ estate/core/views/base.py | 20 ++++++++- estate/settings/drf.py | 3 +- estate/terraform/admin/namespace.py | 4 +- .../migrations/0009_auto_20170917_0156.py | 42 +++++++++++++++++++ estate/terraform/models/namespace.py | 6 +-- estate/terraform/views/file.py | 13 +++++- estate/terraform/views/namespace.py | 21 +++++++--- estate/terraform/views/template.py | 19 +++++++-- package.json | 1 + 12 files changed, 134 insertions(+), 26 deletions(-) create mode 100644 estate/terraform/migrations/0009_auto_20170917_0156.py diff --git a/estate/assets/js/components/DashboardListView.jsx b/estate/assets/js/components/DashboardListView.jsx index 8ed213a..86ee396 100644 --- a/estate/assets/js/components/DashboardListView.jsx +++ b/estate/assets/js/components/DashboardListView.jsx @@ -90,7 +90,9 @@ class DashboardListView extends React.Component { let data = [] each(this.props.data, (item) => { if (item) { - item.link = + if (item.is_owner) { + item.link = + } item.modified = new Date(Date.parse(item.modified)).toLocaleString() data.push(item) } diff --git a/estate/assets/js/components/TerraformNamespaceItem.jsx b/estate/assets/js/components/TerraformNamespaceItem.jsx index d82da1c..a9a30f1 100644 --- a/estate/assets/js/components/TerraformNamespaceItem.jsx +++ b/estate/assets/js/components/TerraformNamespaceItem.jsx @@ -234,7 +234,7 @@ class TerraformNamespaceItem extends React.Component { this.props.unlockNamespace(this.props.namespace.pk) } createFilePane(props) { - var locked = this.props.namespace.is_uneditable + var locked = this.props.namespace.is_readonly var index = findIndex(this.state.files, {slug: props.match.params.file}) if (index == -1){ return null @@ -259,7 +259,7 @@ class TerraformNamespaceItem extends React.Component { ) } createFileList() { - var locked = this.props.namespace.is_uneditable + var locked = this.props.namespace.is_readonly var url = this.props.match.url var count = 0 var elements = [] @@ -307,7 +307,7 @@ class TerraformNamespaceItem extends React.Component { ) } createTemplatePane(props) { - var locked = this.props.namespace.is_uneditable + var locked = this.props.namespace.is_readonly var index = findIndex(this.state.templates, {slug: props.match.params.template}) if (index == -1){ return null @@ -349,7 +349,7 @@ class TerraformNamespaceItem extends React.Component { ) } createTemplateList() { - var locked = this.props.namespace.is_uneditable + var locked = this.props.namespace.is_readonly var url = this.props.match.url var count = 0 var elements = [] @@ -446,7 +446,7 @@ class TerraformNamespaceItem extends React.Component { ) } createExperimentPane() { - var locked = this.props.namespace.is_uneditable + var locked = this.props.namespace.is_readonly var data = this.props.experimentOutput var output = join(data.output, "") return ( @@ -501,9 +501,12 @@ class TerraformNamespaceItem extends React.Component { if (this.props.namespace == null) { return null } + if (this.props.namespace.is_owner == false){ + return null + } const url = this.props.match.url const namespace = this.props.namespace - const locked = namespace.is_uneditable + const locked = namespace.is_readonly return (
diff --git a/estate/assets/js/components/TerraformNamespaceList.jsx b/estate/assets/js/components/TerraformNamespaceList.jsx index a0a4855..3320412 100644 --- a/estate/assets/js/components/TerraformNamespaceList.jsx +++ b/estate/assets/js/components/TerraformNamespaceList.jsx @@ -23,6 +23,18 @@ const TerraformNamespacesTableColumns = [ accessor: "description", sortable: false, }, + { + Header: "Owning Group", + accessor: "owner", + maxWidth: 200, + sortable: false, + }, + { + Header: "Locked", + accessor: "locking_user", + maxWidth: 200, + sortable: false, + }, { Header: "Modified", accessor: "modified", diff --git a/estate/core/views/base.py b/estate/core/views/base.py index 14857a2..bc7c719 100644 --- a/estate/core/views/base.py +++ b/estate/core/views/base.py @@ -1,6 +1,6 @@ from __future__ import absolute_import from django.db.models.query import QuerySet -from rest_framework import serializers, decorators, response +from rest_framework import serializers, decorators, response, permissions class HistoricalSerializer(serializers.ModelSerializer): @@ -31,3 +31,21 @@ def history(self, request, *args, **kwargs): instance = self.get_object() serializer = self.get_serializer(instance.history.all(), many=True, is_history=True) return response.Response(serializer.data) + + +class IsOwner(permissions.BasePermission): + + def has_object_permission(self, request, view, obj): + if obj.owner: + return request.user.groups.filter(name=obj.owner).count() == 1 + else: + return True + + +class OwnsNamespace(permissions.BasePermission): + + def has_object_permission(self, request, view, obj): + if obj.namespace.owner: + return request.user.groups.filter(name=obj.namespace.owner).count() == 1 + else: + return True diff --git a/estate/settings/drf.py b/estate/settings/drf.py index 4b3f6de..e4a0aec 100644 --- a/estate/settings/drf.py +++ b/estate/settings/drf.py @@ -31,7 +31,6 @@ def api_exception_handler(exc, context): 'PAGE_SIZE': 10, 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.BasicAuthentication', - 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.TokenAuthentication', ), 'DEFAULT_PERMISSION_CLASSES': ( @@ -62,7 +61,7 @@ def api_exception_handler(exc, context): }, 'LOGIN_URL': 'rest_framework:login', 'LOGOUT_URL': 'rest_framework:logout', - 'USE_SESSION_AUTH': True, + 'USE_SESSION_AUTH': False, 'APIS_SORTER': 'alpha', 'JSON_EDITOR': True, 'VALIDATOR_URL': None diff --git a/estate/terraform/admin/namespace.py b/estate/terraform/admin/namespace.py index 229664d..83e8aa2 100644 --- a/estate/terraform/admin/namespace.py +++ b/estate/terraform/admin/namespace.py @@ -6,8 +6,8 @@ class NamespaceAdmin(admin.ModelAdmin): - list_display = ['pk', 'title', 'description', 'modified'] - list_editable = ['title', 'description'] + list_display = ['pk', 'title', 'owner', 'locked', 'locking_user'] + list_editable = ['title', 'owner'] list_filter = ['title'] search_fields = ['slug', 'title'] list_per_page = 10 diff --git a/estate/terraform/migrations/0009_auto_20170917_0156.py b/estate/terraform/migrations/0009_auto_20170917_0156.py new file mode 100644 index 0000000..3ea2345 --- /dev/null +++ b/estate/terraform/migrations/0009_auto_20170917_0156.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-09-17 01:56 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('terraform', '0008_auto_20170905_2028'), + ] + + operations = [ + migrations.AlterField( + model_name='historicalnamespace', + name='locked', + field=models.BooleanField(default=False, verbose_name='locked'), + ), + migrations.AlterField( + model_name='historicalnamespace', + name='owner', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='auth.Group'), + ), + migrations.AlterField( + model_name='namespace', + name='locked', + field=models.BooleanField(default=False, verbose_name='locked'), + ), + migrations.AlterField( + model_name='namespace', + name='locking_user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='locked_namespaces', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='namespace', + name='owner', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='namespaces', to='auth.Group'), + ), + ] diff --git a/estate/terraform/models/namespace.py b/estate/terraform/models/namespace.py index 7d9d594..a790b24 100644 --- a/estate/terraform/models/namespace.py +++ b/estate/terraform/models/namespace.py @@ -14,9 +14,9 @@ class Namespace(EstateAbstractBase): - owner = models.CharField(_('owner'), max_length=80) - locked = models.BooleanField(default=False) - locking_user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True) + owner = models.ForeignKey("auth.Group", related_name="namespaces", null=True, blank=True) + locked = models.BooleanField(_('locked'), default=False) + locking_user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="locked_namespaces", null=True, blank=True) # TODO: Add tags history = HistoricalRecordsWithoutDelete(excluded_fields=['slug']) diff --git a/estate/terraform/views/file.py b/estate/terraform/views/file.py index 2c9bbc7..6228024 100644 --- a/estate/terraform/views/file.py +++ b/estate/terraform/views/file.py @@ -1,7 +1,7 @@ from __future__ import absolute_import from django.apps import apps -from rest_framework import serializers, viewsets -from estate.core.views import HistoricalSerializer, HistoryMixin +from rest_framework import serializers, viewsets, filters +from estate.core.views import HistoricalSerializer, HistoryMixin, OwnsNamespace Namespace = apps.get_model('terraform.Namespace') File = apps.get_model('terraform.File') @@ -20,9 +20,18 @@ class Meta: historical_fields = ("pk", "slug", "title", "namespace", "description", "content", "disable") +class FileFilter(filters.FilterSet): + + class Meta: + model = File + fields = ["title", "namespace"] + + class FileApiView(HistoryMixin, viewsets.ModelViewSet): queryset = File.objects.all() serializer_class = FileSerializer + filter_class = FileFilter + permission_classes = (OwnsNamespace, ) filter_fields = ('slug',) search_fields = ('title',) ordering_fields = ('title', 'created', 'modified') diff --git a/estate/terraform/views/namespace.py b/estate/terraform/views/namespace.py index 570a530..68ebaed 100644 --- a/estate/terraform/views/namespace.py +++ b/estate/terraform/views/namespace.py @@ -2,7 +2,7 @@ import django_filters from django.apps import apps from rest_framework import serializers, viewsets, filters, decorators, response -from estate.core.views import HistoricalSerializer, HistoryMixin +from estate.core.views import HistoricalSerializer, HistoryMixin, IsOwner from .file import FileSerializer from .template import TemplateInstanceSerializer from ..terraform import Terraform @@ -13,22 +13,30 @@ class NamespaceSerializer(HistoricalSerializer): description = serializers.CharField(default="", allow_blank=True) - owner = serializers.CharField(default="", allow_blank=True) + owner = serializers.SlugRelatedField(slug_field="name", read_only=True) files = FileSerializer(many=True, read_only=True, is_history=True) templates = TemplateInstanceSerializer(many=True, read_only=True, is_history=True) locking_user = serializers.SlugRelatedField(slug_field="username", read_only=True) + is_owner = serializers.SerializerMethodField(read_only=True) is_unlockable = serializers.SerializerMethodField(read_only=True) - is_uneditable = serializers.SerializerMethodField(read_only=True) + is_readonly = serializers.SerializerMethodField(read_only=True) class Meta: model = Namespace - fields = ("pk", "slug", "title", "description", "owner", "files", "templates", "locked", "locking_user", "is_unlockable", "is_uneditable", "created", "modified") + fields = ("pk", "slug", "title", "description", "owner", "files", "templates", "locked", "locking_user", "is_owner", "is_unlockable", "is_readonly", "created", "modified") historical_fields = ("pk", "slug", "title", "description", "owner", "locked", "locking_user", "historical_files", "historical_templates") + def get_is_owner(self, instance): + if instance.owner: + return self.context["request"].user.groups.filter(name=instance.owner).count() == 1 + else: + return True + def get_is_unlockable(self, instance): - return instance.is_unlockable(self.context["request"].user) + is_owner = self.get_is_owner(instance) + return all([instance.is_unlockable(self.context["request"].user), is_owner]) - def get_is_uneditable(self, instance): + def get_is_readonly(self, instance): result = False if instance.locked is True: if instance.is_unlockable(self.context["request"].user) is not True: @@ -51,6 +59,7 @@ class NamespaceApiView(HistoryMixin, viewsets.ModelViewSet): queryset = Namespace.objects.all() serializer_class = NamespaceSerializer filter_class = NamespaceFilter + permission_classes = (IsOwner, ) search_fields = ('title',) ordering_fields = ('title', 'created', 'modified') diff --git a/estate/terraform/views/template.py b/estate/terraform/views/template.py index 9811df3..216547f 100644 --- a/estate/terraform/views/template.py +++ b/estate/terraform/views/template.py @@ -1,9 +1,9 @@ from __future__ import absolute_import import json from django.apps import apps -from rest_framework import serializers, viewsets, decorators, exceptions, status, response +from rest_framework import serializers, viewsets, decorators, exceptions, status, response, filters from semantic_version import Version -from estate.core.views import HistoricalSerializer, HistoryMixin +from estate.core.views import HistoricalSerializer, HistoryMixin, OwnsNamespace from estate.core import renderer Namespace = apps.get_model("terraform.Namespace") @@ -20,15 +20,19 @@ class TemplateSerializer(HistoricalSerializer): version_increment = serializers.ChoiceField(choices=["major", "minor", "patch", "initial"], write_only=True) body = serializers.CharField(default="", allow_blank=True, validators=[renderer.is_valid_template]) body_mode = serializers.SerializerMethodField() + is_owner = serializers.SerializerMethodField() class Meta: model = Template - fields = ("pk", "slug", "title", "description", "version", "version_increment", "json_schema", "ui_schema", "body", "body_mode", "created", "modified") + fields = ("pk", "slug", "title", "description", "version", "version_increment", "json_schema", "ui_schema", "body", "body_mode", "is_owner", "created", "modified") historical_fields = ("pk", "slug", "title", "description", "version", "json_schema", "ui_schema", "body") def get_body_mode(self, instance): return renderer.get_style(instance.body) + def get_is_owner(self, instance): + return True + def create(self, validated_data): version_increment = validated_data.pop("version_increment", "initial") if version_increment != "initial": @@ -123,9 +127,18 @@ def update(self, instance, validated_data): return super(TemplateInstanceSerializer, self).update(instance, validated_data) +class TemplateInstanceFilter(filters.FilterSet): + + class Meta: + model = TemplateInstance + fields = ["title", "namespace"] + + class TemplateInstanceApiView(HistoryMixin, viewsets.ModelViewSet): queryset = TemplateInstance.objects.all() serializer_class = TemplateInstanceSerializer + filter_class = TemplateInstanceFilter + permission_classes = (OwnsNamespace, ) filter_fields = ("slug",) search_fields = ("title",) ordering_fields = ("title", "created", "modified") diff --git a/package.json b/package.json index dccf392..d9f1be0 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "rc-select": "6.8.6", "react": "15.5.4", "@skidding/react-codemirror": "^1.0.0", + "react-diff": "0.0.7", "react-dom": "15.5.4", "react-jsonschema-form": "0.48.2", "react-hot-loader": "3.0.0-beta.6",