From 2bd33ed4281fa11121bebc6fb7a1c91dc11d5e75 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Thu, 23 Apr 2026 22:05:41 -0700 Subject: [PATCH] radicale: Fix issue with parsing new configuration file The latest version of radicale calendar server's configuration file does not parse with augeas. This is because it contains the following entry in [headers] section: Content-Security-Policy = default-src 'self'; object-src 'none' The semicolon is treated as comment by the lens which is not correct. Fix this by overriding comment_re in the lens. Tests: - Updated test case works when using augparse. - With the patch, latest upstream configuration file parses without errors. - Functional tests work for radicale in testing distribution. Without patch radicale fails to install. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/modules/radicale/__init__.py | 30 +------ .../data/usr/share/augeas/lenses/radicale.aug | 45 +++++++++++ .../augeas/lenses/tests/test_radicale.aug | 81 +++++++++++++++++++ plinth/modules/radicale/privileged.py | 14 +++- plinth/modules/radicale/views.py | 6 +- 5 files changed, 143 insertions(+), 33 deletions(-) create mode 100644 plinth/modules/radicale/data/usr/share/augeas/lenses/radicale.aug create mode 100644 plinth/modules/radicale/data/usr/share/augeas/lenses/tests/test_radicale.aug diff --git a/plinth/modules/radicale/__init__.py b/plinth/modules/radicale/__init__.py index 3910d0f08..7eab78db5 100644 --- a/plinth/modules/radicale/__init__.py +++ b/plinth/modules/radicale/__init__.py @@ -5,7 +5,6 @@ FreedomBox app for radicale. import logging -import augeas from django.utils.translation import gettext_lazy as _ from plinth import app as app_module @@ -37,8 +36,6 @@ _description = [ logger = logging.getLogger(__name__) -CONFIG_FILE = '/etc/radicale/config' - class RadicaleApp(app_module.App): """FreedomBox app for Radicale.""" @@ -129,7 +126,7 @@ class RadicaleApp(app_module.App): if Version(package['new_version']) > Version('4~'): return False - rights = get_rights_value() + rights = privileged.get_rights_value() install(['radicale'], force_configuration='new') privileged.setup() privileged.configure(rights) @@ -140,28 +137,3 @@ class RadicaleApp(app_module.App): """De-configure and uninstall the app.""" super().uninstall() privileged.uninstall() - - -def load_augeas(): - """Prepares the augeas.""" - aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD + - augeas.Augeas.NO_MODL_AUTOLOAD) - - # INI file lens - aug.set('/augeas/load/Puppet/lens', 'Puppet.lns') - aug.set('/augeas/load/Puppet/incl[last() + 1]', CONFIG_FILE) - - aug.load() - return aug - - -def get_rights_value(): - """Returns the current Rights value.""" - aug = load_augeas() - value = aug.get('/files' + CONFIG_FILE + '/rights/type') - - if value == 'from_file': - # Default rights file is equivalent to owner_only. - value = 'owner_only' - - return value diff --git a/plinth/modules/radicale/data/usr/share/augeas/lenses/radicale.aug b/plinth/modules/radicale/data/usr/share/augeas/lenses/radicale.aug new file mode 100644 index 000000000..cad3e6f4d --- /dev/null +++ b/plinth/modules/radicale/data/usr/share/augeas/lenses/radicale.aug @@ -0,0 +1,45 @@ +(* Radicale module for Augeas + Based on Puppet lens. + + Manage config file for http://radicale.org/ + /etc/radicale/config is a standard INI File. +*) + + +module Radicale = + autoload xfm + +(************************************************************************ + * INI File settings + * + * /etc/radicale/config only supports "#" as commentary and "=" as separator + *************************************************************************) +let comment = IniFile.comment "#" "#" +let comment_re = /[#]/ +let sep = IniFile.sep "=" "=" + + +(************************************************************************ + * ENTRY + * /etc/radicale/config uses standard INI File entries + *************************************************************************) +let entry = IniFile.entry_generic (Util.indent . key IniFile.entry_re) sep comment_re comment + + +(************************************************************************ + * RECORD + * /etc/radicale/config uses standard INI File records + *************************************************************************) +let title = IniFile.indented_title IniFile.record_re +let record = IniFile.record title entry + + +(************************************************************************ + * LENS & FILTER + * /etc/radicale/config uses standard INI File records + *************************************************************************) +let lns = IniFile.lns record comment + +let filter = (incl "/etc/radicale/config") + +let xfm = transform lns filter diff --git a/plinth/modules/radicale/data/usr/share/augeas/lenses/tests/test_radicale.aug b/plinth/modules/radicale/data/usr/share/augeas/lenses/tests/test_radicale.aug new file mode 100644 index 000000000..085c60152 --- /dev/null +++ b/plinth/modules/radicale/data/usr/share/augeas/lenses/tests/test_radicale.aug @@ -0,0 +1,81 @@ +module Test_radicale = + + let conf = " +[server] + +[encoding] + +[well-known] + +[auth] + +[git] + +[rights] + +[storage] + +[logging] + +[headers] +Content-Security-Policy = default-src 'self'; object-src 'none' + +" + + test Radicale.lns get conf = + {} + { "server" + {} } + { "encoding" + {} } + { "well-known" + {} } + { "auth" + {} } + { "git" + {} } + { "rights" + {} } + { "storage" + {} } + { "logging" + {} } + { "headers" + { "Content-Security-Policy" = "default-src 'self'; object-src 'none'" } + {} } + + test Radicale.lns put conf after + set "server/hosts" "127.0.0.1:5232, [::1]:5232"; + set "server/base_prefix" "/radicale/"; + set "well-known/caldav" "/radicale/%(user)s/caldav/"; + set "well-known/cardav" "/radicale/%(user)s/carddav/"; + set "auth/type" "remote_user"; + set "rights/type" "owner_only"; + set "headers/Content-Security-Policy" "default-src 'self'; object-src 'none'" + = " +[server] + +hosts=127.0.0.1:5232, [::1]:5232 +base_prefix=/radicale/ +[encoding] + +[well-known] + +caldav=/radicale/%(user)s/caldav/ +cardav=/radicale/%(user)s/carddav/ +[auth] + +type=remote_user +[git] + +[rights] + +type=owner_only +[storage] + +[logging] + +[headers] +Content-Security-Policy = default-src 'self'; object-src 'none' + +" diff --git a/plinth/modules/radicale/privileged.py b/plinth/modules/radicale/privileged.py index 546150125..5799bac67 100644 --- a/plinth/modules/radicale/privileged.py +++ b/plinth/modules/radicale/privileged.py @@ -56,12 +56,24 @@ def load_augeas(): aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD + augeas.Augeas.NO_MODL_AUTOLOAD) # INI file lens - aug.transform('Puppet', CONFIG_FILE) + aug.transform('Radicale', CONFIG_FILE) aug.set('/augeas/context', '/files' + CONFIG_FILE) aug.load() return aug +def get_rights_value(): + """Returns the current Rights value.""" + aug = load_augeas() + value = aug.get('rights/type') + + if value == 'from_file': + # Default rights file is equivalent to owner_only. + value = 'owner_only' + + return value + + @privileged def uninstall(): """Remove all radicale collections.""" diff --git a/plinth/modules/radicale/views.py b/plinth/modules/radicale/views.py index e7d73b7a6..2aa36cedc 100644 --- a/plinth/modules/radicale/views.py +++ b/plinth/modules/radicale/views.py @@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _ from plinth.views import AppView -from . import get_rights_value, privileged +from . import privileged from .forms import RadicaleForm @@ -20,13 +20,13 @@ class RadicaleAppView(AppView): def get_initial(self): """Return the values to fill in the form.""" initial = super().get_initial() - initial['access_rights'] = get_rights_value() + initial['access_rights'] = privileged.get_rights_value() return initial def form_valid(self, form): """Change the access control of Radicale service.""" data = form.cleaned_data - if get_rights_value() != data['access_rights']: + if privileged.get_rights_value() != data['access_rights']: privileged.configure(data['access_rights']) messages.success(self.request, _('Access rights configuration updated'))