privacy: Add new system app for popularity-contest

- Keep the description about app generic

- Remove enable/disable option

- Create a booleanfield to turn on/off popcon

- Don't re-enable popcon during an update

Tests:

- When enabling/disabling the option, the `"PARTICIPATE"` value in
`/etc/popularity-contest.conf` is changed to yes/no as expected. For reference
see `/var/lib/dpkg/info/popularity-contest.templates`

- When popcon option is enabled, running sudo sh -x
/etc/cron.daily/popularity-context shows that execution was successful and data
was submitted. Remove files /var/log/popularity-contest* and
/var/lib/popularity-contest/lastsub if necessary. Gpg is used and encrypted data
is what was submitted.

- When popcon option is disabled, running sudo sh -x
/etc/cron.daily/popularity-context shows that execution stopped because the
option is disabled.

Signed-off-by: nbenedek <contact@nbenedek.me>
[sunil: Add a notification to tell users about privacy app]
[sunil: Correct the URL to /sys]
[sunil: Minor code styling changes and updates to description, icon]
[sunil: Ensure that popcon works with encryption]
[sunil: Write configuration to a separate file]
[sunil: Use Shellvars lens instead of Php lns]
[sunil: Add functional tests]
[sunil: Backup/restore the configuration file]
Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: Sunil Mohan Adapa <sunil@medhas.org>
This commit is contained in:
nbenedek 2022-09-17 10:30:48 +02:00 committed by Sunil Mohan Adapa
parent 5dd2751514
commit 7e2ebcb743
No known key found for this signature in database
GPG Key ID: 43EA1CFF0AA7C5F2
11 changed files with 271 additions and 2 deletions

View File

@ -0,0 +1,79 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""FreedomBox app to the Privacy app."""
import augeas
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext_noop
from plinth import app as app_module
from plinth import menu
from plinth.modules.backups.components import BackupRestore
from plinth.package import Packages
from . import manifest, privileged
_description = [_('Manage system-wide privacy settings.')]
class PrivacyApp(app_module.App):
"""FreedomBox app for Privacy."""
app_id = 'privacy'
_version = 1
can_be_disabled = False
def __init__(self):
"""Create components for the app."""
super().__init__()
info = app_module.Info(app_id=self.app_id, version=self._version,
is_essential=True, name=_('Privacy'),
icon='fa-eye-slash', description=_description,
manual_page=None)
self.add(info)
menu_item = menu.Menu('menu-privacy', info.name,
info.short_description, info.icon,
'privacy:index', parent_url_name='system')
self.add(menu_item)
packages = Packages('packages-privacy', ['popularity-contest', 'gpg'])
self.add(packages)
backup_restore = BackupRestore('backup-restore-privacy',
**manifest.backup)
self.add(backup_restore)
def setup(self, old_version):
"""Install and configure the app."""
super().setup(old_version)
privileged.setup()
if old_version == 0:
privileged.set_configuration(enable_popcon=True)
_show_privacy_notification()
def _show_privacy_notification():
"""Show a notification asking user to review privacy settings."""
from plinth.notification import Notification
message = gettext_noop(
'Please update privacy settings to match your preferences.')
data = {
'app_name': 'translate:' + gettext_noop('Privacy'),
'app_icon': 'fa-eye-slash'
}
title = gettext_noop('Review privacy setting')
actions_ = [{
'type': 'link',
'class': 'primary',
'text': gettext_noop('Go to {app_name}'),
'url': 'privacy:index'
}, {
'type': 'dismiss'
}]
Notification.update_or_create(id='privacy-review', app_id='privacy',
severity='info', title=title,
message=message, actions=actions_, data=data,
group='admin')

View File

@ -0,0 +1 @@
plinth.modules.privacy

View File

@ -0,0 +1,24 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""FreedomBox privacy app."""
from django import forms
from django.utils.translation import gettext_lazy as _
from plinth import cfg
from plinth.utils import format_lazy
class PrivacyForm(forms.Form):
"""Privacy configuration form."""
enable_popcon = forms.BooleanField(
label=_('Periodically submit a list of apps used (suggested)'),
required=False, help_text=format_lazy(
_('Help Debian/{box_name} developers by participating in the '
'Popularity Contest package survey program. When enabled, a '
'list of apps used on this system will be anonymously submitted '
'to Debian every week. Statistics for the data collected are '
'publicly available at <a href="https://popcon.debian.org/" '
'target="_blank">popcon.debian.org</a>. Submission happens over '
'the Tor network for additional anonymity if Tor app is enabled.'
), box_name=_(cfg.box_name)))

View File

@ -0,0 +1,6 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Application manifest for privacy app."""
from . import privileged
backup = {'config': {'files': [str(privileged.CONFIG_FILE)]}}

View File

@ -0,0 +1,51 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Configure Privacy App."""
import pathlib
from typing import Optional
import augeas
from plinth.actions import privileged
CONFIG_FILE = pathlib.Path('/etc/popularity-contest.d/freedombox.conf')
@privileged
def setup():
"""Create initial popcon configuration."""
CONFIG_FILE.parent.mkdir(exist_ok=True)
CONFIG_FILE.touch()
aug = _load_augeas()
aug.set('ENCRYPT', 'yes')
aug.save()
@privileged
def set_configuration(enable_popcon: Optional[bool] = None):
"""Update popcon configuration."""
aug = _load_augeas()
if enable_popcon:
aug.set('PARTICIPATE', 'yes')
else:
aug.set('PARTICIPATE', 'no')
aug.save()
def get_configuration() -> dict[str, bool]:
"""Return if popcon participation is enabled."""
aug = _load_augeas()
value = aug.get('PARTICIPATE')
return {'enable_popcon': (value == 'yes')}
def _load_augeas():
"""Initialize Augeas."""
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
augeas.Augeas.NO_MODL_AUTOLOAD)
aug.transform('Shellvars', str(CONFIG_FILE))
aug.set('/augeas/context', '/files' + str(CONFIG_FILE))
aug.load()
return aug

View File

View File

@ -0,0 +1,59 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Functional, browser based tests for privacy app."""
import pytest
from plinth.tests import functional
pytestmark = [pytest.mark.system, pytest.mark.privacy]
class TestPrivacyApp(functional.BaseAppTests):
"""Tests for privacy app."""
app_name = 'privacy'
has_service = False
has_web = False
disable_after_tests = False
@pytest.fixture(autouse=True)
def fixture_background(self, session_browser):
"""Login, install, and enable the app."""
functional.login(session_browser)
functional.nav_to_module(session_browser, self.app_name)
yield
def test_enable_disable(self, session_browser):
"""Skip test for enabling and disabling the app."""
pytest.skip('Can not be disabled')
@pytest.mark.backups
def test_enable_disable_popcon(self, session_browser):
"""Test that popcon can be enable/disabled."""
functional.change_checkbox_status(session_browser, self.app_name,
'id_enable_popcon', 'disabled')
functional.change_checkbox_status(session_browser, self.app_name,
'id_enable_popcon', 'enabled')
assert session_browser.find_by_id('id_enable_popcon').checked
functional.change_checkbox_status(session_browser, self.app_name,
'id_enable_popcon', 'disabled')
assert not session_browser.find_by_id('id_enable_popcon').checked
@pytest.mark.backups
def test_backup_restore(self, session_browser):
"""Test that backup and restore operations work on the app."""
functional.change_checkbox_status(session_browser, self.app_name,
'id_enable_popcon', 'disabled')
functional.backup_create(session_browser, self.app_name,
'test_' + self.app_name)
functional.nav_to_module(session_browser, self.app_name)
functional.change_checkbox_status(session_browser, self.app_name,
'id_enable_popcon', 'enabled')
functional.backup_restore(session_browser, self.app_name,
'test_' + self.app_name)
functional.nav_to_module(session_browser, self.app_name)
assert not session_browser.find_by_id('id_enable_popcon').checked
def test_uninstall(self, session_browser):
"""Skip test for uninstall."""
pytest.skip('Essential app')

View File

@ -0,0 +1,10 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""URLs for the Privacy module."""
from django.urls import re_path
from .views import PrivacyAppView
urlpatterns = [
re_path(r'^sys/privacy/$', PrivacyAppView.as_view(), name='index'),
]

View File

@ -0,0 +1,38 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Views for privacy app."""
from django.contrib import messages
from django.utils.translation import gettext as _
from plinth.modules.privacy.forms import PrivacyForm
from plinth.views import AppView
from . import privileged
class PrivacyAppView(AppView):
"""Serve configuration page."""
app_id = 'privacy'
form_class = PrivacyForm
def get_initial(self):
"""Return the values to fill in the form."""
initial = super().get_initial()
initial.update(privileged.get_configuration())
return initial
def form_valid(self, form):
"""Change the configurations of Minetest service."""
new_config = form.cleaned_data
old_config = form.initial
changes = {}
if old_config['enable_popcon'] != new_config['enable_popcon']:
changes['enable_popcon'] = new_config['enable_popcon']
if changes:
privileged.set_configuration(**changes)
messages.success(self.request, _('Configuration updated'))
return super().form_valid(form)

View File

@ -50,8 +50,8 @@ _site_url = {
_sys_modules = [
'avahi', 'backups', 'bind', 'cockpit', 'config', 'datetime', 'diagnostics',
'dynamicdns', 'firewall', 'letsencrypt', 'names', 'networks', 'pagekite',
'performance', 'power', 'security', 'snapshot', 'ssh', 'storage',
'upgrades', 'users'
'performance', 'power', 'privacy', 'security', 'snapshot', 'ssh',
'storage', 'upgrades', 'users'
]

View File

@ -46,6 +46,7 @@ markers = [
"openvpn",
"pagekite",
"performance",
"privacy",
"privoxy",
"quassel",
"radicale",