diff --git a/swh/web/auth/mailmap.py b/swh/web/auth/mailmap.py new file mode 100644 --- /dev/null +++ b/swh/web/auth/mailmap.py @@ -0,0 +1,54 @@ +# Copyright (C) 2022 The Software Heritage developers +# See the AUTHORS file at the top-level directory of this distribution +# License: GNU Affero General Public License version 3, or any later version +# See top-level LICENSE file for more information + +from django.conf.urls import url +from django.http.response import HttpResponse, HttpResponseForbidden +from rest_framework import serializers +from rest_framework.decorators import api_view +from rest_framework.request import Request +from rest_framework.response import Response + +from swh.web.auth.models import UserMailmap +from swh.web.auth.utils import MAILMAP_PERMISSION + + +class UserMailmapSerializer(serializers.ModelSerializer): + class Meta: + model = UserMailmap + fields = "__all__" + + +@api_view(["POST"]) +def profile_add_mailmap(request: Request) -> HttpResponse: + if not request.user.has_perm(MAILMAP_PERMISSION): + return HttpResponseForbidden() + + UserMailmap.objects.create(user_id=str(request.user.id), **request.data) + mm = UserMailmap.objects.get( + user_id=str(request.user.id), from_email=request.data.get("from_email") + ) + return Response(UserMailmapSerializer(mm).data) + + +@api_view(["POST"]) +def profile_update_mailmap(request: Request) -> HttpResponse: + if not request.user.has_perm(MAILMAP_PERMISSION): + return HttpResponseForbidden() + + UserMailmap.objects.update(user_id=str(request.user.id), **request.data) + mm = UserMailmap.objects.get( + user_id=str(request.user.id), from_email=request.data.get("from_email") + ) + return Response(UserMailmapSerializer(mm).data) + + +urlpatterns = [ + url(r"^profile/mailmap/add$", profile_add_mailmap, name="profile-mailmap-add",), + url( + r"^profile/mailmap/update$", + profile_update_mailmap, + name="profile-mailmap-update", + ), +] diff --git a/swh/web/auth/migrations/0004_usermailmap.py b/swh/web/auth/migrations/0004_usermailmap.py new file mode 100644 --- /dev/null +++ b/swh/web/auth/migrations/0004_usermailmap.py @@ -0,0 +1,45 @@ +# Copyright (C) 2022 The Software Heritage developers +# See the AUTHORS file at the top-level directory of this distribution +# License: GNU Affero General Public License version 3, or any later version +# See top-level LICENSE file for more information + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("swh_web_auth", "0003_delete_oidcuser"), + ] + + operations = [ + migrations.CreateModel( + name="UserMailmap", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("user_id", models.CharField(max_length=50, null=True)), + ("from_email", models.TextField(unique=True, null=False)), + ("from_email_verified", models.BooleanField(default=False)), + ( + "from_email_verification_request_date", + models.DateTimeField(null=True), + ), + ("display_name", models.TextField(null=False)), + ("display_name_activated", models.BooleanField(default=False)), + ("to_email", models.TextField(null=True)), + ("to_email_verified", models.BooleanField(default=False)), + ("to_email_verification_request_date", models.DateTimeField(null=True)), + ("mailmap_last_processing_date", models.DateTimeField(null=True)), + ("last_update_date", models.DateTimeField(auto_now=True)), + ], + options={"db_table": "user_mailmap",}, + ), + ] diff --git a/swh/web/auth/models.py b/swh/web/auth/models.py --- a/swh/web/auth/models.py +++ b/swh/web/auth/models.py @@ -1,4 +1,4 @@ -# Copyright (C) 2020 The Software Heritage developers +# Copyright (C) 2020-2022 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information @@ -18,3 +18,46 @@ class Meta: app_label = "swh_web_auth" db_table = "oidc_user_offline_tokens" + + +class UserMailmap(models.Model): + """ + Model storing mailmap settings submitted by users. + """ + + user_id = models.CharField(max_length=50, null=True) + """Optional user id from Keycloak""" + + from_email = models.TextField(unique=True, null=False) + """Email address to find author in the archive""" + + from_email_verified = models.BooleanField(default=False) + """Indicates if the from email has been verified""" + + from_email_verification_request_date = models.DateTimeField(null=True) + """Last from email verification request date""" + + display_name = models.TextField(null=False) + """Display name to use for the author instead of the archived one""" + + display_name_activated = models.BooleanField(default=False) + """Indicates if the new display name should be used""" + + to_email = models.TextField(null=True) + """Optional new email to use in the display name instead of the archived one""" + + to_email_verified = models.BooleanField(default=False) + """Indicates if the to email has been verified""" + + to_email_verification_request_date = models.DateTimeField(null=True) + """Last to email verification request date""" + + mailmap_last_processing_date = models.DateTimeField(null=True) + """Last mailmap synchronisation date with swh-storage""" + + last_update_date = models.DateTimeField(auto_now=True) + """Last date that mailmap model was updated""" + + class Meta: + app_label = "swh_web_auth" + db_table = "user_mailmap" diff --git a/swh/web/auth/utils.py b/swh/web/auth/utils.py --- a/swh/web/auth/utils.py +++ b/swh/web/auth/utils.py @@ -1,4 +1,4 @@ -# Copyright (C) 2020-2021 The Software Heritage developers +# Copyright (C) 2020-2022 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information @@ -18,6 +18,7 @@ SWH_AMBASSADOR_PERMISSION = "swh.ambassador" API_SAVE_ORIGIN_PERMISSION = "swh.web.api.save_origin" ADMIN_LIST_DEPOSIT_PERMISSION = "swh.web.admin.list_deposits" +MAILMAP_PERMISSION = "swh.web.mailmap" def _get_fernet(password: bytes, salt: bytes) -> Fernet: diff --git a/swh/web/auth/views.py b/swh/web/auth/views.py --- a/swh/web/auth/views.py +++ b/swh/web/auth/views.py @@ -1,4 +1,4 @@ -# Copyright (C) 2020 The Software Heritage developers +# Copyright (C) 2020-2022 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information @@ -33,6 +33,8 @@ from swh.web.common.utils import reverse from swh.web.config import get_config +from .mailmap import urlpatterns as mailmap_urlpatterns + def oidc_generate_bearer_token(request: HttpRequest) -> HttpResponse: if not request.user.is_authenticated or not isinstance(request.user, OIDCUser): @@ -157,31 +159,35 @@ return render(request, "auth/profile.html") -urlpatterns = auth_urlpatterns + [ - url( - r"^oidc/generate-bearer-token/$", - oidc_generate_bearer_token, - name="oidc-generate-bearer-token", - ), - url( - r"^oidc/generate-bearer-token-complete/$", - oidc_generate_bearer_token_complete, - name="oidc-generate-bearer-token-complete", - ), - url( - r"^oidc/list-bearer-token/$", - oidc_list_bearer_tokens, - name="oidc-list-bearer-tokens", - ), - url( - r"^oidc/get-bearer-token/$", - oidc_get_bearer_token, - name="oidc-get-bearer-token", - ), - url( - r"^oidc/revoke-bearer-tokens/$", - oidc_revoke_bearer_tokens, - name="oidc-revoke-bearer-tokens", - ), - url(r"^oidc/profile/$", _oidc_profile_view, name="oidc-profile",), -] +urlpatterns = ( + auth_urlpatterns + + [ + url( + r"^oidc/generate-bearer-token/$", + oidc_generate_bearer_token, + name="oidc-generate-bearer-token", + ), + url( + r"^oidc/generate-bearer-token-complete/$", + oidc_generate_bearer_token_complete, + name="oidc-generate-bearer-token-complete", + ), + url( + r"^oidc/list-bearer-token/$", + oidc_list_bearer_tokens, + name="oidc-list-bearer-tokens", + ), + url( + r"^oidc/get-bearer-token/$", + oidc_get_bearer_token, + name="oidc-get-bearer-token", + ), + url( + r"^oidc/revoke-bearer-tokens/$", + oidc_revoke_bearer_tokens, + name="oidc-revoke-bearer-tokens", + ), + url(r"^oidc/profile/$", _oidc_profile_view, name="oidc-profile",), + ] + + mailmap_urlpatterns +) diff --git a/swh/web/tests/auth/test_mailmap.py b/swh/web/tests/auth/test_mailmap.py new file mode 100644 --- /dev/null +++ b/swh/web/tests/auth/test_mailmap.py @@ -0,0 +1,45 @@ +# Copyright (C) 2022 The Software Heritage developers +# See the AUTHORS file at the top-level directory of this distribution +# License: GNU Affero General Public License version 3, or any later version +# See top-level LICENSE file for more information + +import pytest + +from swh.web.auth.utils import MAILMAP_PERMISSION +from swh.web.common.utils import reverse +from swh.web.tests.utils import check_api_post_response, create_django_permission + + +@pytest.fixture +def mailmap_user(regular_user): + regular_user.user_permissions.add(create_django_permission(MAILMAP_PERMISSION)) + return regular_user + + +@pytest.mark.django_db(transaction=True) +@pytest.mark.parametrize("view_name", ["profile-mailmap-add", "profile-mailmap-update"]) +def test_mailmap_endpoints_anonymous_user(api_client, view_name): + url = reverse(view_name) + check_api_post_response(api_client, url, status_code=403) + + +@pytest.mark.django_db(transaction=True) +def test_mailmap_endpoints_user_with_permission(api_client, mailmap_user): + api_client.force_login(mailmap_user) + for view_name in ("profile-mailmap-add", "profile-mailmap-update"): + url = reverse(view_name) + check_api_post_response( + api_client, + url, + data={"from_email": "bar@example.org", "display_name": "bar"}, + status_code=200, + ) + + +@pytest.mark.django_db(transaction=True) +def test_mailmap_endpoints_error_response(api_client, mailmap_user): + api_client.force_login(mailmap_user) + for view_name in ("profile-mailmap-add", "profile-mailmap-update"): + url = reverse(view_name) + resp = check_api_post_response(api_client, url, status_code=500) + assert "exception" in resp.data