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 <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Sunil Mohan Adapa 2026-04-23 22:05:41 -07:00 committed by James Valleroy
parent ae50ceaeb0
commit 2bd33ed428
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
5 changed files with 143 additions and 33 deletions

View File

@ -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

View File

@ -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

View File

@ -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'
"

View File

@ -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."""

View File

@ -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'))