Merge pull request #3737 from modoboa/feature/alias-target-acl

Added way to allow/block alias target at creation/modification.
This commit is contained in:
Antoine Nguyen
2025-09-26 16:05:20 +02:00
committed by GitHub
8 changed files with 145 additions and 8 deletions
+2 -1
View File
@@ -42,8 +42,9 @@ _axios.interceptors.response.use(
!error.config.ignoreErrors
) {
const busStore = useBusStore()
const msg = error.response.data?.error || error.response.data
busStore.displayNotification({
msg: error.response.data,
msg,
type: 'error',
})
}
@@ -28,7 +28,7 @@
</v-card>
</template>
<script setup lang="js">
<script setup>
import { ref } from 'vue'
import { useGettext } from 'vue3-gettext'
import accountApi from '@/api/account'
@@ -40,15 +40,17 @@ const busStore = useBusStore()
const form = ref({})
const loading = ref(false)
function submit() {
async function submit() {
loading.value = true
accountApi.setForward(form.value).then(() => {
loading.value = false
try {
await accountApi.setForward(form.value)
busStore.displayNotification({
type: 'success',
msg: $gettext('Forward updated'),
})
})
} finally {
loading.value = false
}
}
accountApi.getForward().then((resp) => {
@@ -169,7 +169,7 @@ function display(element) {
element.display.split('&').forEach((rule) => {
let [field, value] = rule.split('=')
if (value === 'true' || value === 'false') {
value = Boolean(value)
value = value === 'true'
}
result &= parameters.value[field] === value
})
+5
View File
@@ -244,6 +244,11 @@ class AdminGlobalParametersSerializer(serializers.Serializer):
auto_create_domain_and_mailbox = serializers.BooleanField(default=True)
create_alias_on_mbox_rename = serializers.BooleanField(default=False)
# Aliases settings
alias_can_target_any_domain = serializers.BooleanField(default=True)
alias_target_block_list = serializers.CharField(required=False, allow_null=True)
alias_target_allow_list = serializers.CharField(required=False, allow_null=True)
def validate_default_domain_quota(self, value):
"""Ensure quota is a positive integer."""
if value < 0:
+39
View File
@@ -239,6 +239,45 @@ GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict(
),
},
),
(
"aliases",
{
"label": gettext_lazy("Aliases"),
"params": collections.OrderedDict(
[
(
"alias_can_target_any_domain",
{
"label": gettext_lazy("Alias can target any domain"),
"help_text": gettext_lazy(
"Allow aliases to target any domain, not just the local domains."
),
},
),
(
"alias_target_block_list",
{
"label": gettext_lazy("Alias target block list"),
"help_text": gettext_lazy(
"A list of domains that aliases cannot target (comma separated)."
),
"display": "alias_can_target_any_domain=true",
},
),
(
"alias_target_allow_list",
{
"label": gettext_lazy("Alias target allow list"),
"help_text": gettext_lazy(
"A list of domains that aliases can target (comma separated)."
),
"display": "alias_can_target_any_domain=false",
},
),
]
),
},
),
]
)
+22
View File
@@ -21,6 +21,7 @@ from modoboa.lib.exceptions import (
ModoboaException,
AliasExists,
)
from modoboa.parameters import tools as param_tools
from .. import signals
from .base import AdminObject
from .domain import Domain
@@ -90,6 +91,25 @@ class AliasManager(models.Manager):
)
def convert_to_list(raw_value: str) -> list[str]:
"""Convert input string to list of string."""
return [item.strip() for item in raw_value.split(",")]
def is_alias_target_allowed(domain: str) -> bool:
"""Check if alias target is allowed or not."""
params = dict(param_tools.get_global_parameters(app="admin"))
if params.get("alias_can_target_any_domain"):
blocked_domains = params.get("alias_target_block_list")
if blocked_domains and domain in convert_to_list(blocked_domains):
return False
else:
allowed_domains = params.get("alias_target_allow_list")
if allowed_domains and domain not in convert_to_list(allowed_domains):
return False
return True
class Alias(AdminObject):
"""Mailbox alias."""
@@ -182,6 +202,8 @@ class Alias(AdminObject):
if domname is None:
raise BadRequest("{} {}".format(_("Invalid address"), address))
domain = Domain.objects.filter(name=domname).first()
if not domain and not is_alias_target_allowed(domname):
raise BadRequest(f"{_('Target domain not allowed')}: {domname}")
kwargs = {"address": address, "alias": self}
if (domain is not None) and (
any(
+68
View File
@@ -0,0 +1,68 @@
"""Tests for alias target allow/block settings."""
from django.urls import reverse
from modoboa.admin import factories, models
from modoboa.lib.tests import ModoAPITestCase
class AliasTargetSettingsTestCase(ModoAPITestCase):
@classmethod
def setUpTestData(cls): # NOQA:N802
super().setUpTestData()
factories.populate_database()
def test_block_external_domain_when_any_allowed_with_block_list(self):
# alias_can_target_any_domain = True and block external domain
self.set_global_parameter("alias_can_target_any_domain", True, app="admin")
self.set_global_parameter("alias_target_block_list", "bad.com", app="admin")
values = {
"address": "list@test.com",
"recipients": ["user@bad.com"],
"enabled": True,
}
resp = self.client.post(reverse("v2:alias-list"), values, format="json")
# Creation should fail
self.assertEqual(resp.status_code, 400)
self.assertEqual(resp.json()["error"], "Target domain not allowed: bad.com")
def test_allow_only_list_when_any_disallowed(self):
# alias_can_target_any_domain = False and allow list contains domain
self.set_global_parameter("alias_can_target_any_domain", False, app="admin")
self.set_global_parameter("alias_target_allow_list", "ok.com", app="admin")
# Check if local domain is still allowed
values = {
"address": "list2@test.com",
"recipients": ["user@test.com"],
"enabled": True,
}
resp = self.client.post(reverse("v2:alias-list"), values, format="json")
self.assertEqual(resp.status_code, 201)
values = {
"address": "list3@test.com",
"recipients": ["user@ok.com"],
"enabled": True,
}
resp = self.client.post(reverse("v2:alias-list"), values, format="json")
self.assertEqual(resp.status_code, 201)
alias = models.Alias.objects.get(address="list3@test.com", internal=False)
# Allowed external domain should remain external
self.assertTrue(
alias.aliasrecipient_set.filter(
address="user@ok.com", r_mailbox__isnull=True, r_alias__isnull=True
).exists()
)
# Now try a domain not in allow list, should stay external as well
values = {
"address": "list4@test.com",
"recipients": ["user@bad.com"],
"enabled": True,
}
resp = self.client.post(reverse("v2:alias-list"), values, format="json")
self.assertEqual(resp.status_code, 400)
self.assertEqual(resp.json()["error"], "Target domain not allowed: bad.com")
+1 -1
View File
@@ -43,7 +43,7 @@ dependencies = [
"django-oauth-toolkit",
"django-cors-headers",
"passlib~=1.7.4",
"bcrypt", # Requires libffi-dev and python-dev
"bcrypt==4.3.0", # Requires libffi-dev and python-dev
"asgiref",
"dnspython==2.8.0",
"feedparser==6.0.12",