🔥(sops) remove obsolete sops file

remove obsolete sops file
This commit is contained in:
Marie PUPO JEAMMET
2025-10-21 18:23:29 +02:00
committed by Marie
parent 53d0336755
commit 23561cd0e0
10 changed files with 171 additions and 50 deletions

View File

@@ -1,11 +0,0 @@
creation_rules:
- path_regex: ./*
key_groups:
- age:
- age15fyxdwmg5mvldtqqus87xspuws2u0cpvwheehrtvkexj4tnsqqysw6re2x # jacques
- age16hnlml8yv4ynwy0seer57g8qww075crd0g7nsundz3pj4wk7m3vqftszg7 # github-repo
- age1plkp8td6zzfcavjusmsfrlk54t9vn8jjxm8zaz7cmnr7kzl2nfnsd54hwg # Anthony Le-Courric
- age12g6f5fse25tgrwweleh4jls3qs52hey2edh759smulwmk5lnzadslu2cp3 # Antoine Lebaud
- age1hnhuzj96ktkhpyygvmz0x9h8mfvssz7ss6emmukags644mdhf4msajk93r # Samuel Paccoud
- age1tl80n23wq6zxegupwn70ew0yp225ua5v4dk800x7g2w6pvlxz46qk592pa # Marie Pupo Jeammet
- age1rjchule5sncn8r8gfph07muee6vzx4wqfrtldt5jjzke4vlfxy2qqplfvc # Quentin Bey

View File

@@ -8,9 +8,12 @@ and this project adheres to
## [Unreleased]
- ✨(aliases) can create, list and delete aliases #974
## [1.20.0] - 2025-10-22
- 🐛(mailbox) fix case-sensitive duplicates on display names
- (models) impose uniqueness on display name, to match ox's constraint
- 🐛(dimail) catch duplicate displayname error
- ✨(mailbox) synchronize password of newly created mailbox with Dimail's
## [1.19.1] - 2025-09-19

View File

@@ -327,18 +327,15 @@ class AliasSerializer(serializers.ModelSerializer):
"""
Override create function to fire a request to dimail on alias creation.
"""
alias = super().create(validated_data)
if validated_data["domain"].status == enums.MailDomainStatusChoices.ENABLED:
alias = models.Alias(**validated_data)
if alias.domain.status == enums.MailDomainStatusChoices.ENABLED:
client = DimailAPIClient()
# send new alias request to dimail
try:
client.create_alias(alias, self.context["request"].user.sub)
except django_exceptions.ValidationError as exc:
alias.delete()
raise exc
client = DimailAPIClient()
client.create_alias(alias, self.context["request"].user.sub)
return super().create(validated_data)
return alias
return None
def validate_local_part(self, value):
"""Validate this local part does not match a mailbox."""

View File

@@ -40,7 +40,7 @@ class MailDomainViewSet(
Fetch domain status and expected config from dimail.
"""
permission_classes = [permissions.AccessPermission]
permission_classes = [permissions.DomainResourcePermission]
serializer_class = serializers.MailDomainSerializer
filter_backends = [filters.OrderingFilter]
ordering_fields = ["created_at", "name"]
@@ -112,7 +112,7 @@ class MailDomainAccessViewSet(
Delete targeted domain access
"""
permission_classes = [permissions.MailDomainAccessRolePermission]
permission_classes = [permissions.DomainResourcePermission]
serializer_class = serializers.MailDomainAccessSerializer
filter_backends = [filters.OrderingFilter]
ordering_fields = ["role", "user__email", "user__name"]
@@ -355,7 +355,7 @@ class MailDomainInvitationViewset(
"""
lookup_field = "id"
permission_classes = [permissions.AccessPermission]
permission_classes = [permissions.DomainResourcePermission]
queryset = (
models.MailDomainInvitation.objects.all()
.select_related("domain")
@@ -395,13 +395,16 @@ class MailDomainInvitationViewset(
class AliasViewSet(
viewsets.GenericViewSet,
mixins.CreateModelMixin,
mixins.ListModelMixin,
mixins.DestroyModelMixin,
viewsets.GenericViewSet,
):
"""API ViewSet for aliases.
GET /api/<version>/mail-domains/<domain_slug>/aliases/
Return list of aliases related to that domain
POST /api/<version>/mail-domains/<domain_slug>/aliases/ with expected data:
- local_part: str
- destination: str
@@ -411,7 +414,7 @@ class AliasViewSet(
Delete targeted alias
"""
lookup_field = "id"
lookup_field = "local_part"
permission_classes = [permissions.DomainPermission]
serializer_class = serializers.AliasSerializer
queryset = (
@@ -448,6 +451,18 @@ class AliasViewSet(
return queryset
def get_permissions(self):
"""Add a specific permission for domain viewers to delete their aliases."""
if self.action in ["destroy"]:
permission_classes = [
permissions.DomainResourcePermission
| permissions.IsAliasDestinationPermission
]
else:
return super().get_permissions()
return [permission() for permission in permission_classes]
def perform_create(self, serializer):
"""Create new mailbox."""
domain_slug = self.kwargs.get("domain_slug", "")

View File

@@ -7,7 +7,7 @@ from core.api import permissions as core_permissions
from mailbox_manager import models
class AccessPermission(core_permissions.IsAuthenticated):
class DomainResourcePermission(core_permissions.IsAuthenticated):
"""Permission class for access objects."""
def has_object_permission(self, request, view, obj):
@@ -16,7 +16,7 @@ class AccessPermission(core_permissions.IsAuthenticated):
return abilities.get(request.method.lower(), False)
class DomainPermission(AccessPermission):
class DomainPermission(DomainResourcePermission):
"""Permission class to manage mailboxes and aliases for a mail domain"""
def has_permission(self, request, view):
@@ -40,10 +40,15 @@ class IsMailboxOwnerPermission(permissions.BasePermission):
return obj.get_email() == request.user.email
class MailDomainAccessRolePermission(core_permissions.IsAuthenticated):
"""Permission class to manage mailboxes for a mail domain"""
class IsAliasDestinationPermission(core_permissions.IsAuthenticated):
"""Can delete an alias if the alias points to their own email address."""
def has_permission(self, request, view):
"""This permission is specifically about updates"""
domain = models.MailDomain.objects.get(slug=view.kwargs.get("domain_slug", ""))
abilities = domain.get_abilities(request.user)
return abilities["get"]
def has_object_permission(self, request, view, obj):
"""Check permission for a given object."""
abilities = obj.get_abilities(request.user)
return abilities.get(request.method.lower(), False)
"""If the user is trying to update their own mailbox."""
return obj.destination == request.user.email

View File

@@ -8,7 +8,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mailbox_manager', '0025_alter_mailbox_secondary_email'),
('mailbox_manager', '0026_alter_mailbox_unique_together_and_more'),
]
operations = [

View File

@@ -476,3 +476,26 @@ class Alias(BaseModel):
def __str__(self):
return f"{self.local_part} to {self.destination}"
def get_abilities(self, user):
"""Compute and return abilities for a given user. Admin and owners can
edit aliases, but also viewer if the alias points to their email."""
try:
role = user.mail_domain_accesses.get(domain=self.domain).role
except (MailDomainAccess.DoesNotExist, IndexError):
role = None
is_owner_or_admin = role in [
MailDomainRoleChoices.OWNER,
MailDomainRoleChoices.ADMIN,
]
is_self = self.destination == user.email
return {
"get": bool(role),
"post": is_owner_or_admin,
"patch": False,
"put": False,
"delete": is_owner_or_admin or is_self,
}

View File

@@ -1,7 +1,8 @@
"""
Tests for mailbox Aliases API endpoint in People's app mailbox_manager.
Tests for aliases API endpoint.
Focus on "create" action.
"""
# pylint: disable=W0613
import json
import re
@@ -14,7 +15,6 @@ from rest_framework.test import APIClient
from core import factories as core_factories
from mailbox_manager import enums, factories, models
from mailbox_manager.tests.fixtures.dimail import TOKEN_OK
pytestmark = pytest.mark.django_db
@@ -80,7 +80,7 @@ def test_api_aliases_create__duplicate_forbidden():
assert models.Alias.objects.filter(domain=access.domain).count() == 1
def test_api_aliases_create__existing_mailbox_bad_request():
def test_api_aliases_create__existing_alias_bad_request():
"""Cannot create alias if local_part is already used by a mailbox."""
access = factories.MailDomainAccessFactory(
role="owner", domain=factories.MailDomainEnabledFactory()
@@ -94,6 +94,46 @@ def test_api_aliases_create__existing_mailbox_bad_request():
{"local_part": mailbox.local_part, "destination": "someone@outsidedomain.com"},
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.json() == {
"local_part": [f'Local part "{mailbox.local_part}" already used for a mailbox.']
}
assert not models.Alias.objects.exists()
@responses.activate
def test_api_aliases_create__async_alias_bad_request(dimail_token_ok):
"""
If People fall out of sync with dimail, return a clear error if alias cannot be created
because it already exists on dimail.
"""
access = factories.MailDomainAccessFactory(
role="owner", domain=factories.MailDomainEnabledFactory()
)
client = APIClient()
client.force_login(access.user)
# Mock dimail response
responses.add(
responses.POST,
re.compile(r".*/aliases/"),
body=json.dumps({"detail": "Alias already exists"}),
status=status.HTTP_409_CONFLICT,
content_type="application/json",
)
response = client.post(
f"/api/v1.0/mail-domains/{access.domain.slug}/aliases/",
{
"local_part": "already_existing_alias",
"destination": "someone@outsidedomain.com",
},
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.json() == {
"NON_FIELD_ERRORS": [
"Alias already exists. Your domain is out of sync, please contact our support."
]
}
assert not models.Alias.objects.exists()
@@ -102,20 +142,14 @@ def test_api_aliases_create__existing_mailbox_bad_request():
"role",
[enums.MailDomainRoleChoices.OWNER, enums.MailDomainRoleChoices.ADMIN],
)
def test_api_aliases_create__admins_ok(role):
def test_api_aliases_create__admins_ok(role, dimail_token_ok):
"""Domain admins should be able to create aliases."""
access = factories.MailDomainAccessFactory(role=role)
client = APIClient()
client.force_login(access.user)
# Prepare responses
responses.add(
responses.GET,
re.compile(r".*/token/"),
body=TOKEN_OK,
status=status.HTTP_200_OK,
content_type="application/json",
)
# token response in fixtures
responses.add(
responses.POST,
re.compile(rf".*/domains/{access.domain.name}/aliases/"),

View File

@@ -45,21 +45,63 @@ def test_api_aliases_delete__no_access_forbidden():
def test_api_aliases_delete__viewer_forbidden():
"""
Authenticated users should not be allowed to delete a mail domain access for a
Authenticated users should not be allowed to delete aliases for a
mail domain in which they are a simple viewer.
"""
authenticated_user = core_factories.UserFactory()
mail_domain = factories.MailDomainFactory(
users=[(authenticated_user, enums.MailDomainRoleChoices.VIEWER)]
)
access = factories.MailDomainAccessFactory(domain=mail_domain)
alias = factories.AliasFactory(domain=mail_domain)
client = APIClient()
client.force_login(authenticated_user)
response = client.delete(
f"/api/v1.0/mail-domains/{mail_domain.slug}/accesses/{access.id!s}/",
f"/api/v1.0/mail-domains/{mail_domain.slug}/aliases/{alias.local_part}/",
)
assert response.status_code == status.HTTP_403_FORBIDDEN
assert models.MailDomainAccess.objects.count() == 2
assert models.MailDomainAccess.objects.filter(user=access.user).exists()
assert models.Alias.objects.count() == 1
def test_api_aliases_delete__viewer_can_delete_self_alias():
"""
Authenticated users should be allowed to delete aliases when linking
to their own mailbox.
"""
authenticated_user = core_factories.UserFactory()
mail_domain = factories.MailDomainFactory(
users=[(authenticated_user, enums.MailDomainRoleChoices.VIEWER)]
)
alias = factories.AliasFactory(
domain=mail_domain, destination=authenticated_user.email
)
client = APIClient()
client.force_login(authenticated_user)
response = client.delete(
f"/api/v1.0/mail-domains/{mail_domain.slug}/aliases/{alias.local_part}/",
)
assert response.status_code == status.HTTP_204_NO_CONTENT
assert not models.Alias.objects.exists()
def test_api_aliases_delete__administrators_allowed():
"""
Administrators of a mail domain should be allowed to delete accesses excepted owner accesses.
"""
authenticated_user = core_factories.UserFactory()
mail_domain = factories.MailDomainFactory(
users=[(authenticated_user, enums.MailDomainRoleChoices.ADMIN)]
)
alias = factories.AliasFactory(domain=mail_domain)
client = APIClient()
client.force_login(authenticated_user)
response = client.delete(
f"/api/v1.0/mail-domains/{mail_domain.slug}/aliases/{alias.local_part}/",
)
assert response.status_code == status.HTTP_204_NO_CONTENT
assert not models.Alias.objects.exists()

View File

@@ -711,4 +711,17 @@ class DimailAPIClient:
"Permission denied. Please check your MAIL_PROVISIONING_API_CREDENTIALS."
)
if response.status_code == status.HTTP_409_CONFLICT:
logger.error(
"[DIMAIL] Out of sync with dimail. Admin, please import aliases for domain %s",
str(alias.domain),
)
raise exceptions.ValidationError(
{
"NON_FIELD_ERRORS": [
"Alias already exists. Your domain is out of sync, please contact our support."
]
}
)
return self.raise_exception_for_unexpected_response(response)