mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
Deal with password scheme deprecation (crypt as of now)
Also display notifications in new admin. see #3350
This commit is contained in:
@@ -4,4 +4,7 @@ export default {
|
||||
getComponentsInformation() {
|
||||
return repository.get('/admin/components/')
|
||||
},
|
||||
getNotifications() {
|
||||
return repository.get('/admin/notifications/')
|
||||
},
|
||||
}
|
||||
|
||||
@@ -22,11 +22,10 @@
|
||||
</div>
|
||||
|
||||
<v-list nav>
|
||||
<template v-for="item in menuItems">
|
||||
<template v-for="item in menuItems" :key="item.text">
|
||||
<template v-if="displayMenuItem(item)">
|
||||
<v-list-item
|
||||
v-if="!item.children"
|
||||
:key="item.text"
|
||||
:value="item"
|
||||
:to="item.to"
|
||||
link
|
||||
@@ -34,8 +33,11 @@
|
||||
:title="item.text"
|
||||
:prepend-icon="item.icon"
|
||||
>
|
||||
<template v-if="item.withBell && displayInformationBell" #append>
|
||||
<v-icon color="red" icon="mdi-bell-alert-outline" />
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-group v-else :key="item.text" :value="item.text">
|
||||
<v-list-group v-else :value="item.text">
|
||||
<template #activator="{ props }">
|
||||
<v-list-item
|
||||
v-bind="props"
|
||||
@@ -108,16 +110,15 @@
|
||||
</v-navigation-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="js">
|
||||
<script setup>
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import { getActivePinia } from 'pinia'
|
||||
|
||||
import { useGlobalStore, useAuthStore, useParametersStore } from '@/stores'
|
||||
import parametersApi from '@/api/parameters'
|
||||
import { useAuthStore, useParametersStore } from '@/stores'
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
const globalStore = useGlobalStore()
|
||||
const authStore = useAuthStore()
|
||||
const parametersStore = useParametersStore()
|
||||
const route = useRoute()
|
||||
@@ -132,7 +133,6 @@ const isAuthenticated = computed(() => authStore.isAuthenticated)
|
||||
const backgroundColor = computed(() =>
|
||||
route.meta.layout === 'account' ? 'bg-grey-darken-1' : 'bg-primary-darken-1'
|
||||
)
|
||||
|
||||
const userInitials = computed(() => {
|
||||
let initials = null
|
||||
if (authUser.value.first_name) {
|
||||
@@ -170,8 +170,13 @@ const mainColor = computed(() => {
|
||||
|
||||
const imapMigration = computed(() => parametersStore.imapMigrationEnabled)
|
||||
|
||||
const settings = []
|
||||
const displayInformationBell = computed(
|
||||
() =>
|
||||
globalStore.notifications.length !== undefined &&
|
||||
globalStore.notifications.length !== 0
|
||||
)
|
||||
|
||||
const settings = []
|
||||
const mainMenuItems = [
|
||||
{
|
||||
text: $gettext('Dashboard'),
|
||||
@@ -246,6 +251,7 @@ const mainMenuItems = [
|
||||
text: $gettext('Information'),
|
||||
roles: ['SuperAdmins'],
|
||||
to: { name: 'Information' },
|
||||
withBell: true,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -318,15 +324,13 @@ onMounted(() => {
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
if (
|
||||
authUser.value.role === 'SuperAdmins' &&
|
||||
parametersStore.imapMigrationEnabled === null
|
||||
) {
|
||||
parametersApi.getApplication('imap_migration').then((response) => {
|
||||
parametersStore.imapMigrationEnabled =
|
||||
response.data.params.enabled_imapmigration
|
||||
})
|
||||
globalStore.fetchNotifications()
|
||||
if (parametersStore.imapMigrationEnabled === null) {
|
||||
parametersApi.getApplication('imap_migration').then((response) => {
|
||||
parametersStore.imapMigrationEnabled =
|
||||
response.data.params.enabled_imapmigration
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import adminApi from '@/api/admin'
|
||||
|
||||
export const useGlobalStore = defineStore('global', () => {
|
||||
const notifications = ref([])
|
||||
|
||||
async function fetchNotifications() {
|
||||
const response = await adminApi.getNotifications()
|
||||
notifications.value = response.data
|
||||
}
|
||||
|
||||
function getNotificationById(id) {
|
||||
return notifications.value.find((item) => item.id === id)
|
||||
}
|
||||
|
||||
return { notifications, fetchNotifications, getNotificationById }
|
||||
})
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
export * from './global.store'
|
||||
export * from './auth.store'
|
||||
export * from './bus.store'
|
||||
export * from './domains.store'
|
||||
|
||||
@@ -3,6 +3,72 @@
|
||||
<v-toolbar flat>
|
||||
<v-toolbar-title>{{ $gettext('Information') }}</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
<v-card v-if="globalStore.notifications.length" class="my-6">
|
||||
<v-card-title>
|
||||
{{ $gettext('Important messages') }}
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-alert
|
||||
v-for="notification in globalStore.notifications"
|
||||
:key="notification.id"
|
||||
:type="notification.level"
|
||||
:title="notification.text"
|
||||
border="start"
|
||||
variant="tonal"
|
||||
>
|
||||
<template v-if="notification.id === 'deprecatedpasswordscheme'">
|
||||
{{
|
||||
$gettext(
|
||||
'The password scheme you are using has been deprecated and will be removed in the next minor version. The procedure to upgrade to a stronger scheme is as follows:'
|
||||
)
|
||||
}}
|
||||
<ol>
|
||||
<li
|
||||
v-html="
|
||||
$gettext(
|
||||
'Go to <strong>Settings > General</strong> section',
|
||||
true
|
||||
)
|
||||
"
|
||||
></li>
|
||||
<li
|
||||
v-html="
|
||||
$gettext(
|
||||
'Change the value of <strong>Default password scheme</strong>',
|
||||
true
|
||||
)
|
||||
"
|
||||
></li>
|
||||
<li
|
||||
v-html="
|
||||
$gettext(
|
||||
'Make sure <strong>Update password scheme at login</strong> option is enabled',
|
||||
true
|
||||
)
|
||||
"
|
||||
></li>
|
||||
<li v-html="$gettext('Save your changes')"></li>
|
||||
<li>
|
||||
{{
|
||||
$gettext(
|
||||
'Logout / Login with your current account so its password gets updated'
|
||||
)
|
||||
}}
|
||||
</li>
|
||||
<li
|
||||
v-html="
|
||||
$gettext(
|
||||
'Inform <strong>ALL</strong> your users that they must login to Modoboa to complete the operation',
|
||||
true
|
||||
)
|
||||
"
|
||||
></li>
|
||||
</ol>
|
||||
</template>
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-card class="mt-6">
|
||||
<v-card-title>
|
||||
{{ $gettext('Installed components') }}
|
||||
@@ -10,10 +76,10 @@
|
||||
<v-card-text>
|
||||
<v-alert
|
||||
v-if="updatesAvailable"
|
||||
variant="outlined"
|
||||
variant="tonal"
|
||||
type="success"
|
||||
text
|
||||
border="left"
|
||||
border="start"
|
||||
>
|
||||
<div tag="p">
|
||||
{{ $gettext('One or more updates are available') }}
|
||||
@@ -47,12 +113,14 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="js">
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import adminApi from '@/api/admin'
|
||||
<script setup>
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import { useGlobalStore } from '@/stores'
|
||||
import adminApi from '@/api/admin'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
const globalStore = useGlobalStore()
|
||||
|
||||
const components = ref([])
|
||||
const headers = [
|
||||
|
||||
@@ -94,10 +94,11 @@ import parametersApi from '@/api/parameters'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useParametersStore } from '@/stores'
|
||||
import { useGlobalStore, useParametersStore } from '@/stores'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
const busStore = useBusStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const route = useRoute()
|
||||
const parametersStore = useParametersStore()
|
||||
|
||||
@@ -152,6 +153,7 @@ async function save() {
|
||||
busStore.displayNotification({
|
||||
msg: $gettext('Parameters updated'),
|
||||
})
|
||||
globalStore.fetchNotifications()
|
||||
if (route.params.app === 'imap_migration') {
|
||||
parametersStore.imapMigrationEnabled =
|
||||
parameters.value.enabled_imapmigration
|
||||
|
||||
@@ -522,3 +522,12 @@ class ModoboaComponentSerializer(serializers.Serializer):
|
||||
description = serializers.CharField()
|
||||
update = serializers.BooleanField(default=False)
|
||||
changelog_url = serializers.URLField(required=False)
|
||||
|
||||
|
||||
class NotificationSerializer(serializers.Serializer):
|
||||
"""Serializer used to render a notification."""
|
||||
|
||||
id = serializers.CharField()
|
||||
url = serializers.CharField(required=False)
|
||||
text = serializers.CharField()
|
||||
level = serializers.CharField()
|
||||
|
||||
@@ -32,4 +32,9 @@ urlpatterns += [
|
||||
views.ComponentsInformationAPIView.as_view(),
|
||||
name="components_information",
|
||||
),
|
||||
path(
|
||||
"admin/notifications/",
|
||||
views.NotificationsAPIView.as_view(),
|
||||
name="notifications",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Core API v2 views."""
|
||||
|
||||
from functools import reduce
|
||||
import logging
|
||||
from smtplib import SMTPException
|
||||
|
||||
@@ -9,6 +10,7 @@ from drf_spectacular.utils import extend_schema
|
||||
from rest_framework import permissions, response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from modoboa.core import signals
|
||||
from modoboa.core.utils import check_for_updates
|
||||
from modoboa.lib.permissions import IsSuperUser
|
||||
from modoboa.lib.throttle import (
|
||||
@@ -141,3 +143,21 @@ class ComponentsInformationAPIView(APIView):
|
||||
status, extensions = check_for_updates()
|
||||
serializer = serializers.ModoboaComponentSerializer(extensions, many=True)
|
||||
return response.Response(serializer.data)
|
||||
|
||||
|
||||
class NotificationsAPIView(APIView):
|
||||
"""Return list of active notifications."""
|
||||
|
||||
permission_classes = [permissions.IsAuthenticated, IsSuperUser]
|
||||
throttle_classes = [UserLesserDdosUser]
|
||||
|
||||
@extend_schema(responses=serializers.NotificationSerializer(many=True))
|
||||
def get(self, request, *args, **kwargs):
|
||||
notifications = signals.get_top_notifications.send(
|
||||
sender="top_notifications", include_all=False
|
||||
)
|
||||
notifications = reduce(
|
||||
lambda a, b: a + b, [notif[1] for notif in notifications]
|
||||
)
|
||||
serializer = serializers.NotificationSerializer(notifications, many=True)
|
||||
return response.Response(serializer.data)
|
||||
|
||||
@@ -2,9 +2,9 @@ from django.core.checks import register, Info, Warning
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from modoboa.core.models import User
|
||||
from modoboa.core.password_hashers import get_configured_password_hasher
|
||||
from modoboa.core.utils import generate_rsa_private_key
|
||||
from modoboa.parameters.tools import get_global_parameter
|
||||
from modoboa.core.password_hashers import get_password_hasher
|
||||
|
||||
W001 = Warning(
|
||||
_(
|
||||
@@ -49,8 +49,7 @@ def check_rsa_private_key_exists(app_configs, **kwargs):
|
||||
@register()
|
||||
def check_password_hasher(app_configs, **kwargs):
|
||||
msgs = []
|
||||
scheme_in_use = get_global_parameter("password_scheme", app="core")
|
||||
hasher = get_password_hasher(scheme_in_use)
|
||||
if hasher.deprecated:
|
||||
hasher = get_configured_password_hasher()
|
||||
if hasher.deprecated or User.objects.is_password_scheme_in_use(hasher):
|
||||
msgs.append(W002)
|
||||
return msgs
|
||||
|
||||
+26
-11
@@ -12,6 +12,7 @@ from django.urls import reverse
|
||||
from django.utils.encoding import smart_str
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from modoboa.core.password_hashers import get_configured_password_hasher
|
||||
from modoboa.lib import exceptions, permissions
|
||||
from modoboa.lib.signals import get_request
|
||||
from . import models, signals as core_signals, utils
|
||||
@@ -113,19 +114,33 @@ def update_permissions(sender, instance, **kwargs):
|
||||
|
||||
|
||||
@receiver(core_signals.get_top_notifications)
|
||||
def check_for_new_versions(sender, include_all, **kwargs):
|
||||
def check_for_new_versions(sender, include_all: bool, **kwargs) -> list:
|
||||
"""Check if new versions are available."""
|
||||
request = get_request()
|
||||
if not request.user.is_superuser:
|
||||
return []
|
||||
result = []
|
||||
status, extensions = utils.check_for_updates()
|
||||
if not status:
|
||||
return [{"id": "newversionavailable"}] if include_all else []
|
||||
return [
|
||||
{
|
||||
"id": "newversionavailable",
|
||||
"url": reverse("core:index") + "#info/",
|
||||
"text": _("One or more updates are available"),
|
||||
"level": "info",
|
||||
}
|
||||
]
|
||||
if status:
|
||||
result += [
|
||||
{
|
||||
"id": "newversionavailable",
|
||||
"url": reverse("core:index") + "#info/",
|
||||
"text": _("One or more updates are available"),
|
||||
"level": "info",
|
||||
}
|
||||
]
|
||||
elif include_all:
|
||||
result += [{"id": "newversionavailable"}]
|
||||
hasher = get_configured_password_hasher()
|
||||
if hasher.deprecated or models.User.objects.is_password_scheme_in_use(hasher):
|
||||
result += [
|
||||
{
|
||||
"id": "deprecatedpasswordscheme",
|
||||
"url": reverse("core:index") + "#info/",
|
||||
"text": _("You are using a deprecated password scheme (%s)")
|
||||
% hasher.name,
|
||||
"level": "warning",
|
||||
}
|
||||
]
|
||||
return result
|
||||
|
||||
+16
-2
@@ -12,14 +12,14 @@ from django.utils.encoding import force_str, smart_bytes, smart_str
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext as _, gettext_lazy
|
||||
|
||||
from django.contrib.auth.models import AbstractUser, Group
|
||||
from django.contrib.auth.models import AbstractUser, BaseUserManager, Group
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from phonenumber_field.modelfields import PhoneNumberField
|
||||
from reversion import revisions as reversion
|
||||
|
||||
from modoboa.core.password_hashers import get_password_hasher
|
||||
from modoboa.core.password_hashers import get_password_hasher, PasswordHasher
|
||||
from modoboa.lib.exceptions import (
|
||||
BadRequest,
|
||||
Conflict,
|
||||
@@ -41,6 +41,18 @@ def get_default_language() -> str:
|
||||
return settings.LANGUAGE_CODE
|
||||
|
||||
|
||||
class UserManager(BaseUserManager):
|
||||
"""Custom manager for User."""
|
||||
|
||||
def is_password_scheme_in_use(self, password_hasher: PasswordHasher) -> bool:
|
||||
"""Check if given password scheme is still in use."""
|
||||
return (
|
||||
self.get_queryset()
|
||||
.filter(password__startswith=password_hasher.scheme)
|
||||
.exists()
|
||||
)
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
"""Custom User model.
|
||||
|
||||
@@ -92,6 +104,8 @@ class User(AbstractUser):
|
||||
|
||||
password_expr = re.compile(r"\{([\w\-]+)\}(.+)")
|
||||
|
||||
objects = UserManager()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Load parameter manager."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
"""
|
||||
Password hashers for Modoboa.
|
||||
"""
|
||||
"""Password hashers for Modoboa."""
|
||||
|
||||
from modoboa.core.password_hashers.advanced import ( # NOQA:F401
|
||||
BLFCRYPTHasher,
|
||||
@@ -17,6 +15,7 @@ from modoboa.core.password_hashers.base import ( # NOQA:F401
|
||||
SHA256Hasher,
|
||||
PasswordHasher,
|
||||
)
|
||||
from modoboa.parameters import tools as param_tools
|
||||
|
||||
|
||||
def get_password_hasher(scheme: str) -> PasswordHasher:
|
||||
@@ -34,3 +33,9 @@ def get_password_hasher(scheme: str) -> PasswordHasher:
|
||||
except KeyError:
|
||||
hasher = PLAINHasher
|
||||
return hasher
|
||||
|
||||
|
||||
def get_configured_password_hasher() -> PasswordHasher:
|
||||
"""Retrieve the password hasher class currently configured."""
|
||||
scheme = param_tools.get_global_parameter("password_scheme")
|
||||
return get_password_hasher(scheme)
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
{% load i18n core_tags lib_tags %}
|
||||
<h2>{% trans "Information" %} <small>{% trans "Various information about Modoboa" %}</small></h2><hr>
|
||||
{% if password_hasher.deprecated or password_hasher_in_use %}
|
||||
<div class="alert alert-block alert-warning">
|
||||
<h4>{% trans "You are using a deprecated password scheme!" %}</h4>
|
||||
<p>{% blocktranslate trimmed %}
|
||||
The password scheme <strong>{{ scheme }}</strong> has been
|
||||
deprecated and will be removed in the next minor version. The procedure to upgrade to a stronger scheme is as follows:
|
||||
<ul>
|
||||
<li>Go to <strong>Parameters > General</strong> section</li>
|
||||
<li>Change the value of <strong>Default password scheme</strong></li>
|
||||
<li>Make sure <strong>Update password scheme at login</strong> option is enabled</li>
|
||||
<li>Save your changes</li>
|
||||
<li>Logout / Login with your current account so its password gets updated</li>
|
||||
<li>Inform <strong>ALL</strong> your users that they must login to Modoboa to complete the operation</li>
|
||||
</ul>
|
||||
{% endblocktranslate %}</p>
|
||||
<p>{% blocktranslate trimmed %}
|
||||
You must apply this procedure <strong>BEFORE</strong> you install a newest
|
||||
version of Modoboa, otherwise <strong>you will be unable to connect to
|
||||
the web interface anymore</strong>.
|
||||
{% endblocktranslate %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if update_avail %}
|
||||
<div class="alert alert-block alert-success">
|
||||
<h4>{% trans "One or more updates are available!" %}</h4>
|
||||
|
||||
@@ -7,7 +7,8 @@ from django.shortcuts import render
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from modoboa.core.models import Log
|
||||
from modoboa.core.models import Log, User
|
||||
from modoboa.core.password_hashers import get_configured_password_hasher
|
||||
from modoboa.core.utils import check_for_updates
|
||||
from modoboa.lib.listing import get_listing_page, get_sort_order
|
||||
from modoboa.lib.web_utils import render_to_json_response
|
||||
@@ -60,6 +61,8 @@ def parameters(request, tplname="core/parameters.html"):
|
||||
@user_passes_test(lambda u: u.is_superuser)
|
||||
def information(request, tplname="core/information.html"):
|
||||
status, extensions = check_for_updates()
|
||||
hasher = get_configured_password_hasher()
|
||||
password_hasher_in_use = User.objects.is_password_scheme_in_use(hasher)
|
||||
return render_to_json_response(
|
||||
{
|
||||
"content": render_to_string(
|
||||
@@ -67,6 +70,9 @@ def information(request, tplname="core/information.html"):
|
||||
{
|
||||
"update_avail": status,
|
||||
"extensions": extensions,
|
||||
"password_hasher": hasher,
|
||||
"password_hasher_in_use": password_hasher_in_use,
|
||||
"scheme": hasher.name,
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ class NotDefined(exceptions.ModoboaException):
|
||||
return "Parameter {} not defined for app {}".format(self.name, self.app)
|
||||
|
||||
|
||||
class Registry(object):
|
||||
class Registry:
|
||||
"""A registry for parameters."""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
<a id="{{ notification.id }}" href="{{ notification.url }}">
|
||||
{% if notification.counter %}
|
||||
<span class="label label-{{ notification.level }}"> {{ notification.counter }}</span> {{ notification.text|safe }}
|
||||
{% elif notification.level == 'warning' %}
|
||||
<span class="fa fa-warning"></span> {{ notification.text|safe }}
|
||||
{% else %}
|
||||
<span class="fa fa-info-circle"></span> {{ notification.text|safe }}
|
||||
{% endif %}
|
||||
|
||||
Reference in New Issue
Block a user