Deal with password scheme deprecation (crypt as of now)

Also display notifications in new admin.

see #3350
This commit is contained in:
Antoine Nguyen
2024-11-29 14:15:46 +01:00
parent 8d1c1fcbcb
commit 92b93fa6bf
17 changed files with 241 additions and 48 deletions
+3
View File
@@ -4,4 +4,7 @@ export default {
getComponentsInformation() {
return repository.get('/admin/components/')
},
getNotifications() {
return repository.get('/admin/notifications/')
},
}
+23 -19
View File
@@ -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>
+18
View File
@@ -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
View File
@@ -1,5 +1,6 @@
import { createPinia } from 'pinia'
export * from './global.store'
export * from './auth.store'
export * from './bus.store'
export * from './domains.store'
+73 -5
View File
@@ -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 = [
+3 -1
View File
@@ -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
+9
View File
@@ -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()
+5
View File
@@ -32,4 +32,9 @@ urlpatterns += [
views.ComponentsInformationAPIView.as_view(),
name="components_information",
),
path(
"admin/notifications/",
views.NotificationsAPIView.as_view(),
name="notifications",
),
]
+20
View File
@@ -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)
+4 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+8 -3
View File
@@ -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 -1
View File
@@ -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,
},
),
}
+1 -1
View File
@@ -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 %}