diff options
author | Étienne Loks <etienne.loks@iggdrasil.net> | 2021-10-06 18:36:16 +0200 |
---|---|---|
committer | Étienne Loks <etienne.loks@iggdrasil.net> | 2022-12-12 12:20:58 +0100 |
commit | a1a1b524fd02a57bd514ed95580fea8b67e1cede (patch) | |
tree | 0327937f9c376ae95b0777faea227bb628528dab | |
parent | 14c5ccd235d963457485cd907712b43672c5e400 (diff) | |
download | Ishtar-a1a1b524fd02a57bd514ed95580fea8b67e1cede.tar.bz2 Ishtar-a1a1b524fd02a57bd514ed95580fea8b67e1cede.zip |
Syndication - docs, api permissions
Permissions by token, IP and by model.
-rw-r--r-- | archaeological_finds/urls.py | 5 | ||||
-rw-r--r-- | archaeological_finds/views.py | 39 | ||||
-rw-r--r-- | archaeological_finds/views_api.py | 40 | ||||
-rw-r--r-- | archaeological_operations/tests.py | 68 | ||||
-rw-r--r-- | archaeological_operations/urls.py | 5 | ||||
-rw-r--r-- | archaeological_operations/views_api.py | 7 | ||||
-rw-r--r-- | docs/fr/source/media-src/ishtar-syndication.drawio | 1 | ||||
-rw-r--r-- | docs/fr/source/media-src/ishtar-syndication.drawio.png | bin | 0 -> 37844 bytes | |||
-rw-r--r-- | ishtar_common/admin.py | 45 | ||||
-rw-r--r-- | ishtar_common/migrations/0217_auto_20211006_1526.py | 177 | ||||
-rw-r--r-- | ishtar_common/models.py | 5 | ||||
-rw-r--r-- | ishtar_common/models_rest.py | 32 | ||||
-rw-r--r-- | ishtar_common/rest.py | 31 |
13 files changed, 413 insertions, 42 deletions
diff --git a/archaeological_finds/urls.py b/archaeological_finds/urls.py index bc67139d9..532e22ab0 100644 --- a/archaeological_finds/urls.py +++ b/archaeological_finds/urls.py @@ -22,6 +22,7 @@ from django.conf.urls import url from ishtar_common.utils import check_rights, get_urls_for_model from archaeological_finds import views +from archaeological_finds import views_api from archaeological_operations.views import administrativeactfile_document from archaeological_finds import models @@ -593,7 +594,9 @@ urlpatterns = [ ), name="autocomplete-findbasket-write", ), - url(r"api/public/find/$", views.PublicFindAPI.as_view(), name="api-public-find"), + url( + r"api/public/find/$", views_api.PublicFindAPI.as_view(), name="api-public-find" + ), url( r"api/ishtar/base-finds/geo/polygons$", check_rights(["view_find", "view_own_find"])( diff --git a/archaeological_finds/views.py b/archaeological_finds/views.py index 4f5e64475..db8c4b04e 100644 --- a/archaeological_finds/views.py +++ b/archaeological_finds/views.py @@ -20,10 +20,6 @@ from collections import OrderedDict import json -from rest_framework import authentication, permissions -from rest_framework.views import APIView -from rest_framework.response import Response - from django.conf import settings from django.core.exceptions import PermissionDenied from django.db.models import Q @@ -35,8 +31,6 @@ from ishtar_common.utils import ugettext_lazy as _ from django.views.generic import TemplateView from django.views.generic.edit import CreateView, FormView -from ishtar_common.serializers import PublicSerializer - from ishtar_common.models import IshtarUser, get_current_profile from archaeological_operations.models import AdministrativeAct, Operation from archaeological_context_records.models import ContextRecord @@ -1311,39 +1305,6 @@ class QAFindLockView(QABaseLockView): base_url = "find-qa-lock" -class PublicFindAPI(APIView): - authentication_classes = (authentication.TokenAuthentication,) - permission_classes = (permissions.IsAuthenticated,) - - def get_queryset(self): - empty = models.Find.objects.filter(pk=None) - basket_slug = self.request.GET.get("basket", None) - if not basket_slug: - return empty - try: - basket = models.FindBasket.objects.get(slug=basket_slug, public=True) - except models.FindBasket.DoesNotExist: - return empty - q = ( - models.FindBasket.items.through.objects.filter(findbasket_id=basket.id) - .values("find_id") - .order_by("id") - ) - id_list = [bi["find_id"] for bi in q] - clauses = " ".join( - "WHEN id=%s THEN %s" % (pk, i) for i, pk in enumerate(id_list) - ) - - ordering = "CASE {} END".format(clauses) - return models.Find.objects.filter(id__in=id_list).extra( - select={"ordering": ordering}, order_by=("ordering",) - ) - - def get(self, request, format=None): - serializer = PublicSerializer(self.get_queryset(), many=True) - return Response(serializer.data) - - def get_geo_items(request, get_polygons, current_right=None): operation_pk = request.GET.get("operation_pk") context_record_pk = request.GET.get("context_record_pk") diff --git a/archaeological_finds/views_api.py b/archaeological_finds/views_api.py new file mode 100644 index 000000000..66cadeb5e --- /dev/null +++ b/archaeological_finds/views_api.py @@ -0,0 +1,40 @@ +from rest_framework import authentication, permissions +from rest_framework.views import APIView +from rest_framework.response import Response + +from ishtar_common.serializers import PublicSerializer + +from archaeological_finds import models + + +class PublicFindAPI(APIView): + authentication_classes = (authentication.TokenAuthentication,) + permission_classes = (permissions.IsAuthenticated,) + + def get_queryset(self): + empty = models.Find.objects.filter(pk=None) + basket_slug = self.request.GET.get("basket", None) + if not basket_slug: + return empty + try: + basket = models.FindBasket.objects.get(slug=basket_slug, public=True) + except models.FindBasket.DoesNotExist: + return empty + q = ( + models.FindBasket.items.through.objects.filter(findbasket_id=basket.id) + .values("find_id") + .order_by("id") + ) + id_list = [bi["find_id"] for bi in q] + clauses = " ".join( + "WHEN id=%s THEN %s" % (pk, i) for i, pk in enumerate(id_list) + ) + + ordering = "CASE {} END".format(clauses) + return models.Find.objects.filter(id__in=id_list).extra( + select={"ordering": ordering}, order_by=("ordering",) + ) + + def get(self, request, format=None): + serializer = PublicSerializer(self.get_queryset(), many=True) + return Response(serializer.data) diff --git a/archaeological_operations/tests.py b/archaeological_operations/tests.py index e70b701c4..58915dce9 100644 --- a/archaeological_operations/tests.py +++ b/archaeological_operations/tests.py @@ -39,6 +39,9 @@ from django.utils.text import slugify from django.contrib.auth.models import User, Permission from django.utils.translation import ugettext_lazy as _, pgettext, pgettext_lazy +from rest_framework.test import APITestCase +from rest_framework.authtoken.models import Token + from . import models from ishtar_common.views import document_deletion_steps @@ -77,6 +80,8 @@ from ishtar_common.models import ( Document, ValueFormater, Regexp, + ApiUser, + ApiSearchModel, ) from ishtar_common.models_imports import ImporterDefault, ImporterDefaultValues from archaeological_files.models import File, FileType @@ -4419,3 +4424,66 @@ class SeleniumTestsOperations(SeleniumTests): for pk, xpath in from_table: slug_pk = slug + "-" + str(pk) self._test_operation(xpath, slug_pk, copy.deepcopy(geojsons)) + + +class ApiTest(OperationInitTest, APITestCase): + fixtures = FILE_FIXTURES + + def setUp(self): + IshtarSiteProfile.objects.get_or_create(slug="default", active=True) + self.username, self.password, self.user = create_superuser() + self.orgas = self.create_orgas(self.user) + self.create_operation(self.user, self.orgas[0]) + self.create_operation(self.user, self.orgas[0]) + self.create_operation(self.user, self.orgas[0]) + self.create_operation(self.user, self.orgas[0]) + self.auth_token = "Token " + Token.objects.create(user=self.user).key + self.api_user = ApiUser.objects.create(user_ptr=self.user, ip="127.0.0.1") + + def create_api_search_model(self): + return ApiSearchModel.objects.create( + user=self.api_user, + content_type=ContentType.objects.get( + app_label="archaeological_operations", + model="operation" + )) + + def test_permissions(self): + url = reverse("api-search-operation") + response = self.client.get(url, format="json") + # nothing OK + self.assertEqual(response.status_code, 401) + # token + IP + response = self.client.get( + url, format="json", HTTP_AUTHORIZATION=self.auth_token + ) + self.assertEqual(response.status_code, 403) + api_search_model = self.create_api_search_model() + content_type_id = api_search_model.content_type.id + api_search_model.content_type = ContentType.objects.get( + app_label="archaeological_operations", + model="archaeologicalsite" + ) + api_search_model.save() + # token + IP + bad model + response = self.client.get( + url, format="json", HTTP_AUTHORIZATION=self.auth_token + ) + self.assertEqual(response.status_code, 403) + api_search_model.content_type_id = content_type_id + api_search_model.save() + # token + IP + good model + response = self.client.get( + url, format="json", HTTP_AUTHORIZATION=self.auth_token + ) + self.assertEqual(response.status_code, 200) + # token + bad IP + good model + self.api_user.ip = "8.8.8.8" + self.api_user.save() + response = self.client.get( + url, format="json", HTTP_AUTHORIZATION=self.auth_token + ) + self.assertEqual(response.status_code, 403) + self.api_user.ip = "127.0.0.1" + self.api_user.save() + diff --git a/archaeological_operations/urls.py b/archaeological_operations/urls.py index 85a650216..4aee195f4 100644 --- a/archaeological_operations/urls.py +++ b/archaeological_operations/urls.py @@ -21,6 +21,7 @@ from django.conf.urls import url from ishtar_common.utils import check_rights from archaeological_operations import views +from archaeological_operations import views_api from archaeological_operations import models # be carreful: each check_rights must be relevant with ishtar_menu @@ -356,4 +357,8 @@ urlpatterns = [ views.GenerateStatsOperation.as_view(), name="generate-stats-operation", ), + url( + r"api/search/operation/$", views_api.SearchOperationAPI.as_view(), + name="api-search-operation" + ), ] diff --git a/archaeological_operations/views_api.py b/archaeological_operations/views_api.py new file mode 100644 index 000000000..48127ec4b --- /dev/null +++ b/archaeological_operations/views_api.py @@ -0,0 +1,7 @@ +from ishtar_common.rest import SearchAPIView +from archaeological_operations import models + + +class SearchOperationAPI(SearchAPIView): + model = models.Operation + diff --git a/docs/fr/source/media-src/ishtar-syndication.drawio b/docs/fr/source/media-src/ishtar-syndication.drawio new file mode 100644 index 000000000..d1addf4c1 --- /dev/null +++ b/docs/fr/source/media-src/ishtar-syndication.drawio @@ -0,0 +1 @@ +<mxfile host="Electron" modified="2021-10-06T17:03:12.966Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/15.4.0 Chrome/91.0.4472.164 Electron/13.5.0 Safari/537.36" etag="8bPWOHmZ-piZMJ_4vZ0y" version="15.4.0" type="device"><diagram id="C5RBs43oDa-KdzZeNtuy" name="Page-1">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</diagram></mxfile>
\ No newline at end of file diff --git a/docs/fr/source/media-src/ishtar-syndication.drawio.png b/docs/fr/source/media-src/ishtar-syndication.drawio.png Binary files differnew file mode 100644 index 000000000..fc5c7fc81 --- /dev/null +++ b/docs/fr/source/media-src/ishtar-syndication.drawio.png diff --git a/ishtar_common/admin.py b/ishtar_common/admin.py index 07750ffe7..0b73f5708 100644 --- a/ishtar_common/admin.py +++ b/ishtar_common/admin.py @@ -554,8 +554,12 @@ admin_site.register(models.Person, PersonAdmin) class AuthorAdmin(admin.ModelAdmin): list_display = ["person", "author_type"] list_filter = ("author_type",) - search_fields = ("person__name", "person__surname", "person__attached_to__name", - "author_type__label") + search_fields = ( + "person__name", + "person__surname", + "person__attached_to__name", + "author_type__label", + ) model = models.Author autocomplete_fields = ["person"] @@ -2082,3 +2086,40 @@ class DocumentTemplateAdmin(admin.ModelAdmin): admin_site.register(models.DocumentTemplate, DocumentTemplateAdmin) + +class ApiUserAdmin(admin.ModelAdmin): + list_display = ("user_ptr", "ip") + + +admin_site.register(models.ApiUser, ApiUserAdmin) + + +def get_main_content_types_query(): + CONTENT_TYPES = ( + ("archaeological_operations", "operation"), + ) + pks = [] + for app_label, model_name in CONTENT_TYPES: + try: + ct = ContentType.objects.get(app_label=app_label, model=model_name) + pks.append(ct.pk) + except ContentType.DoesNotExist: + pass + return ContentType.objects.filter(pk__in=pks) + + +class ApiSearchModelAdminForm(forms.ModelForm): + class Meta: + model = models.ApiUser + exclude = [] + content_type = forms.ModelChoiceField( + label=_("Content type"), queryset=get_main_content_types_query() + ) + + +class ApiSearchModelAdmin(admin.ModelAdmin): + form = ApiSearchModelAdminForm + list_display = ("user", "content_type") + + +admin_site.register(models.ApiSearchModel, ApiSearchModelAdmin) diff --git a/ishtar_common/migrations/0217_auto_20211006_1526.py b/ishtar_common/migrations/0217_auto_20211006_1526.py new file mode 100644 index 000000000..33299b4c3 --- /dev/null +++ b/ishtar_common/migrations/0217_auto_20211006_1526.py @@ -0,0 +1,177 @@ +# Generated by Django 2.2.24 on 2021-10-06 15:26 + +from django.conf import settings +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('auth', '0011_update_proxy_permissions'), + ('ishtar_common', '0216_auto_20210805_1703'), + ] + + operations = [ + migrations.CreateModel( + name='ApiUser', + fields=[ + ('user_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='apiuser', serialize=False, to=settings.AUTH_USER_MODEL)), + ('ip', models.GenericIPAddressField(verbose_name='IP')), + ], + options={ + 'verbose_name': 'Api - User', + 'verbose_name_plural': 'Api - Users', + }, + ), + migrations.AlterField( + model_name='author', + name='author_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='ishtar_common.AuthorType', verbose_name='Author type'), + ), + migrations.AlterField( + model_name='document', + name='data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='document', + name='history_m2m', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='document', + name='language', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ishtar_common.Language', verbose_name='Language'), + ), + migrations.AlterField( + model_name='document', + name='publisher', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='publish', to='ishtar_common.Organization', verbose_name='Publisher'), + ), + migrations.AlterField( + model_name='document', + name='source', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='ishtar_common.Document', verbose_name='Source'), + ), + migrations.AlterField( + model_name='historicalorganization', + name='data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='historicalorganization', + name='history_m2m', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='historicalperson', + name='data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='historicalperson', + name='history_m2m', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='ishtarsiteprofile', + name='default_language', + field=models.ForeignKey(blank=True, help_text='If set, by default the selected language will be set for localized documents.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='ishtar_common.Language', verbose_name='Default language for documentation'), + ), + migrations.AlterField( + model_name='ishtarsiteprofile', + name='display_srs', + field=models.ForeignKey(blank=True, help_text='Spatial Reference System used for display when no SRS is defined', null=True, on_delete=django.db.models.deletion.SET_NULL, to='ishtar_common.SpatialReferenceSystem', verbose_name='Spatial Reference System for display'), + ), + migrations.AlterField( + model_name='itemkey', + name='group', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ishtar_common.TargetKeyGroup'), + ), + migrations.AlterField( + model_name='itemkey', + name='importer', + field=models.ForeignKey(blank=True, help_text='Specific key to an import', null=True, on_delete=django.db.models.deletion.SET_NULL, to='ishtar_common.Import'), + ), + migrations.AlterField( + model_name='itemkey', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ishtar_common.IshtarUser'), + ), + migrations.AlterField( + model_name='organization', + name='data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='organization', + name='history_m2m', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='organization', + name='organization_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='ishtar_common.OrganizationType', verbose_name='Type'), + ), + migrations.AlterField( + model_name='organization', + name='precise_town', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ishtar_common.Town', verbose_name='Town (precise)'), + ), + migrations.AlterField( + model_name='person', + name='data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='person', + name='history_m2m', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='person', + name='precise_town', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ishtar_common.Town', verbose_name='Town (precise)'), + ), + migrations.AlterField( + model_name='statscache', + name='values', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='targetkey', + name='associated_group', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ishtar_common.TargetKeyGroup'), + ), + migrations.AlterField( + model_name='targetkey', + name='associated_import', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ishtar_common.Import'), + ), + migrations.AlterField( + model_name='targetkey', + name='associated_user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ishtar_common.IshtarUser'), + ), + migrations.AlterField( + model_name='userprofile', + name='profile_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='ishtar_common.ProfileType', verbose_name='Profile type'), + ), + migrations.CreateModel( + name='ApiSearchModel', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('limit_query', models.TextField(blank=True, help_text='Search query add to each request', null=True, verbose_name='Limit query')), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ishtar_common.ApiUser')), + ], + options={ + 'verbose_name': 'Api - Search model', + 'verbose_name_plural': 'Api - Search models', + }, + ), + ] diff --git a/ishtar_common/models.py b/ishtar_common/models.py index 6783a89b5..59248fa43 100644 --- a/ishtar_common/models.py +++ b/ishtar_common/models.py @@ -113,6 +113,11 @@ from ishtar_common.models_imports import ( TargetKeyGroup, ValueFormater, ) +from ishtar_common.models_rest import ( + ApiUser, + ApiSearchModel +) + from ishtar_common.utils import ( get_cache, create_slug, diff --git a/ishtar_common/models_rest.py b/ishtar_common/models_rest.py new file mode 100644 index 000000000..7d321ca92 --- /dev/null +++ b/ishtar_common/models_rest.py @@ -0,0 +1,32 @@ +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.contrib.gis.db import models + +from ishtar_common.utils import ugettext_lazy as _ + + +class ApiUser(models.Model): + user_ptr = models.OneToOneField( + User, primary_key=True, related_name="apiuser", on_delete=models.CASCADE + ) + ip = models.GenericIPAddressField(verbose_name=_("IP")) + + class Meta: + verbose_name = _("Api - User") + verbose_name_plural = _("Api - Users") + + def __str__(self): + return self.user_ptr.username + + +class ApiSearchModel(models.Model): + user = models.ForeignKey(ApiUser, on_delete=models.CASCADE) + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + limit_query = models.TextField( + verbose_name=_("Limit query"), blank=True, null=True, + help_text=_("Search query add to each request") + ) + + class Meta: + verbose_name = _("Api - Search model") + verbose_name_plural = _("Api - Search models") diff --git a/ishtar_common/rest.py b/ishtar_common/rest.py new file mode 100644 index 000000000..9354a943d --- /dev/null +++ b/ishtar_common/rest.py @@ -0,0 +1,31 @@ +from rest_framework import authentication, permissions +from rest_framework.response import Response +from rest_framework.views import APIView + +from ishtar_common.models import ApiSearchModel + + +class IpModelPermission(permissions.BasePermission): + def has_permission(self, request, view): + if not request.user or not getattr(request.user, "apiuser", None): + return False + ip_addr = request.META['REMOTE_ADDR'] + q = ApiSearchModel.objects.filter( + user=request.user.apiuser, + user__ip=ip_addr, + content_type__app_label=view.model._meta.app_label, + content_type__model=view.model._meta.model_name) + return bool(q.count()) + + +class SearchAPIView(APIView): + model = None + authentication_classes = (authentication.TokenAuthentication,) + permission_classes = (permissions.IsAuthenticated, IpModelPermission) + + def __init__(self, **kwargs): + assert self.model is not None + super(SearchAPIView, self).__init__(**kwargs) + + def get(self, request, format=None): + return Response({}) |