Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mvj 234 gdpr api #774

Merged
merged 4 commits into from
Nov 19, 2024
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
34 changes: 34 additions & 0 deletions forms/migrations/0026_alter_answer_user_alter_attachment_user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Generated by Django 4.2.16 on 2024-11-15 14:00

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("forms", "0025_alter_answeropeningrecord_openers"),
]

operations = [
migrations.AlterField(
model_name="answer",
name="user",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
migrations.AlterField(
model_name="attachment",
name="user",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
]
14 changes: 11 additions & 3 deletions forms/models/form.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from django.db.models.fields.json import JSONField
from django.utils.translation import gettext_lazy as _
from enumfields import EnumField
from helsinki_gdpr.models import SerializableMixin

from users.models import User

Expand Down Expand Up @@ -161,7 +162,7 @@ class Answer(models.Model):
"""

form = models.ForeignKey(Form, on_delete=models.PROTECT)
user = models.ForeignKey(User, on_delete=models.CASCADE)
user = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)

created_at = models.DateTimeField(auto_now=True)
ready = models.BooleanField(default=False)
Expand Down Expand Up @@ -206,15 +207,22 @@ def get_attachment_file_upload_to(instance, filename):
)


class Attachment(models.Model):
class Attachment(SerializableMixin, models.Model):
name = models.CharField(max_length=255)
attachment = models.FileField(upload_to=get_attachment_file_upload_to)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Time created"))
path = models.TextField(null=True, blank=True)

answer = models.ForeignKey(Answer, on_delete=models.CASCADE, null=True, blank=True)
field = models.ForeignKey(Field, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
user = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)

# GDPR API
serialize_fields = (
{"name": "name"},
{"name": "attachment"},
{"name": "created_at"},
)

recursive_get_related_skip_relations = [
"user",
Expand Down
Empty file added gdpr/__init__.py
Empty file.
22 changes: 22 additions & 0 deletions gdpr/tests/test_gdpr_permission.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from helusers.authz import UserAuthorization
from requests import Request

from gdpr.views import AmrPermission, MvjGDPRAPIView


def test_amr_permission_has_permission():
request = Request()
request.auth = UserAuthorization(user=None, api_token_payload={"amr": ["suomi_fi"]})
view = MvjGDPRAPIView()
permission = AmrPermission()
assert permission.has_permission(request, view) is True


def test_amr_permission_has_not_permission():
request = Request()
request.auth = UserAuthorization(
user=None, api_token_payload={"amr": ["helsinki_tunnus"]}
)
view = MvjGDPRAPIView()
permission = AmrPermission()
assert permission.has_permission(request, view) is False
22 changes: 22 additions & 0 deletions gdpr/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from helsinki_gdpr.views import DryRunException

from users.models import User


def get_user(user: User) -> User:
juho-kettunen-nc marked this conversation as resolved.
Show resolved Hide resolved
"""
Get the user provider, as User is the GDPR API root model. GDPR API implementation
by default attempts to get it from the root models user attribute.
This function is used by defining it as the value for setting `GDPR_API_USER_PROVIDER`.
"""
return user


def delete_user_data(user: User, dry_run: bool) -> None:
juho-kettunen-nc marked this conversation as resolved.
Show resolved Hide resolved
"""
Delete user data.
"""
if dry_run:
raise DryRunException("Dry run. Rollback delete transaction.")
else:
user.delete()
17 changes: 17 additions & 0 deletions gdpr/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from helsinki_gdpr.views import GDPRAPIView
from rest_framework.permissions import BasePermission
from rest_framework.request import Request


class AmrPermission(BasePermission):
"""Authentication Method Reference (amr)"""

def has_permission(self, request: Request, _view: "MvjGDPRAPIView") -> bool:
amrs: list[str] = request.auth.data.get("amr", [])
allowed_amr = "suomi_fi"
has_allowed_amr = allowed_amr in amrs
return has_allowed_amr


class MvjGDPRAPIView(GDPRAPIView):
permission_classes = GDPRAPIView.permission_classes + [AmrPermission]
16 changes: 16 additions & 0 deletions mvj/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ def get_git_revision_hash():
LASKE_PAYMENTS_DIRECTORY=(str, ""),
LASKE_PAYMENTS_KEY_TYPE=(str, ""),
LASKE_PAYMENTS_KEY=(bytes, ""),
GDPR_API_URL_PATTERN=(str, "v1/profiles/<uuid:uuid>"),
GDPR_API_MODEL=(str, "users.User"),
GDPR_API_MODEL_LOOKUP=(str, "uuid"),
GDPR_API_QUERY_SCOPE=(str, ""),
GDPR_API_DELETE_SCOPE=(str, ""),
GDPR_API_USER_PROVIDER=(str, "gdpr.utils.get_user"),
GDPR_API_DELETER=(str, "gdpr.utils.delete_user_data"),
)

env_file = project_root(".env")
Expand Down Expand Up @@ -325,6 +332,15 @@ def get_git_revision_hash():
"REQUIRE_API_SCOPE_FOR_AUTHENTICATION": env.bool("TOKEN_AUTH_REQUIRE_SCOPE_PREFIX"),
}

# https://github.com/City-of-Helsinki/helsinki-profile-gdpr-api
GDPR_API_URL_PATTERN = env("GDPR_API_URL_PATTERN")
GDPR_API_MODEL = env("GDPR_API_MODEL")
GDPR_API_MODEL_LOOKUP = env("GDPR_API_MODEL_LOOKUP")
GDPR_API_QUERY_SCOPE = env("GDPR_API_QUERY_SCOPE")
GDPR_API_DELETE_SCOPE = env("GDPR_API_DELETE_SCOPE")
GDPR_API_USER_PROVIDER = env("GDPR_API_USER_PROVIDER")
GDPR_API_DELETER = env("GDPR_API_DELETER")

LASKE_VALUES = {
"distribution_channel": "10",
"division": "10",
Expand Down
35 changes: 27 additions & 8 deletions mvj/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
MeetingMemoViewset,
TargetStatusViewset,
)
from gdpr.views import MvjGDPRAPIView
from leasing.api_functions import CalculateIncreaseWith360DayCalendar
from leasing.report.viewset import ReportViewSet
from leasing.views import CloudiaProxy, VirreProxy, ktj_proxy
Expand Down Expand Up @@ -364,28 +365,46 @@
CalculateIncreaseWith360DayCalendar.as_view(),
),
]

gdpr_urls = [
path(
getattr(
settings,
"GDPR_API_URL_PATTERN",
"v1/profiles/<uuid:pk>",
),
MvjGDPRAPIView.as_view(),
name="gdpr_v1",
)
]
additional_pub_api_paths = [
path(
"direct_reservation_to_favourite/<str:uuid>/",
DirectReservationToFavourite.as_view(),
name="pub_direct_reservation_to_favourite",
),
path("plot_search_ui/", PlotSearchUIDataView.as_view(), name="pub_plot_search_ui"),
# Enables oidc backchannel logout, requires setting `HELUSERS_BACK_CHANNEL_LOGOUT_ENABLED = True`
# to be useful
path("helauth/", include("helusers.urls")),
# GDPR API
path(
"gdpr-api/",
include(
(
gdpr_urls,
"gdpr", # Namespace
),
),
),
]

api_urls = router.urls + additional_api_paths

# Path: v1/pub/
pub_api_urls = [
path(
"pub/",
include(
pub_router.urls
+ additional_pub_api_paths
# Enables oidc backchannel logout, requires setting `HELUSERS_BACK_CHANNEL_LOGOUT_ENABLED = True`
# to be useful
+ [path("helauth/", include("helusers.urls"))]
),
include(pub_router.urls + additional_pub_api_paths),
)
]
credit_integration_urls = [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Generated by Django 4.2.16 on 2024-11-15 14:00

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("plotsearch", "0036_areasearch_service_unit"),
]

operations = [
migrations.AlterModelOptions(
name="areasearchattachment",
options={},
),
migrations.AlterField(
model_name="areasearch",
name="user",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
migrations.AlterField(
model_name="areasearchattachment",
name="user",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AlterField(
model_name="favourite",
name="user",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
]
24 changes: 19 additions & 5 deletions plotsearch/models/plot_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from django.utils.translation import gettext_lazy as _
from django.utils.translation import pgettext_lazy
from enumfields import EnumField
from helsinki_gdpr.models import SerializableMixin
from rest_framework.serializers import ValidationError
from safedelete.models import SOFT_DELETE, SafeDeleteModel

Expand Down Expand Up @@ -422,7 +423,7 @@ class MeetingMemo(models.Model):


class Favourite(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
user = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)

created_at = models.DateTimeField(auto_now_add=True, db_index=True)
modified_at = models.DateTimeField(auto_now=True)
Expand Down Expand Up @@ -482,7 +483,7 @@ class AreaSearchStatus(models.Model):
]


class AreaSearch(models.Model):
class AreaSearch(SerializableMixin, models.Model):
# In Finnish: aluehaku

geometry = gmodels.MultiPolygonField(
Expand Down Expand Up @@ -531,7 +532,7 @@ def lessor_name(self):
answer = models.OneToOneField(
Answer, on_delete=models.CASCADE, null=True, related_name="area_search"
)
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)

# In Finnish: Käsittelijä
preparer = models.ForeignKey(
Expand Down Expand Up @@ -566,6 +567,14 @@ def lessor_name(self):
blank=True,
)

# GDPR API
serialize_fields = (
{"name": "address"},
{"name": "received_date"},
# plotsearch.AreaSearchAttachment
juho-kettunen-nc marked this conversation as resolved.
Show resolved Hide resolved
{"name": "area_search_attachments"},
)

recursive_get_related_skip_relations = [
"related_plot_applications",
"areasearchattachment",
Expand Down Expand Up @@ -598,13 +607,15 @@ def get_area_search_attachment_upload_to(instance, filename):
)


class AreaSearchAttachment(NameModel):
class AreaSearchAttachment(SerializableMixin, NameModel):
attachment = models.FileField(
upload_to=get_area_search_attachment_upload_to, null=True, blank=True
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Time created"))

user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="+")
user = models.ForeignKey(
User, null=True, on_delete=models.SET_NULL, related_name="+"
)

# In Finnish: Aluehaut
area_search = models.ForeignKey(
Expand All @@ -615,6 +626,9 @@ class AreaSearchAttachment(NameModel):
blank=True,
)

# GDPR API
serialize_fields = ({"name": "attachment"}, {"name": "created_at"})

recursive_get_related_skip_relations = [
"user",
]
Expand Down
1 change: 1 addition & 0 deletions requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ djangorestframework~=3.15.2
djangorestframework-gis~=1.1.0
drf-yasg[validation]~=1.21.8
docxtpl~=0.18.0
helsinki-profile-gdpr-api~=0.2.0
oracledb~=2.1.0
psycopg[binary]~=3.2.3
pysftp~=0.2.9
Expand Down
Loading