mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
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:
@@ -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
|
||||
})
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
),
|
||||
]
|
||||
),
|
||||
},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user