From 7e2ebcb7437ebcaf1f883e2d4679f3916b69fe87 Mon Sep 17 00:00:00 2001 From: nbenedek Date: Sat, 17 Sep 2022 10:30:48 +0200 Subject: [PATCH] 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 [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 Reviewed-by: Sunil Mohan Adapa --- plinth/modules/privacy/__init__.py | 79 +++++++++++++++++++ .../data/etc/plinth/modules-enabled/privacy | 1 + plinth/modules/privacy/forms.py | 24 ++++++ plinth/modules/privacy/manifest.py | 6 ++ plinth/modules/privacy/privileged.py | 51 ++++++++++++ plinth/modules/privacy/tests/__init__.py | 0 .../modules/privacy/tests/test_functional.py | 59 ++++++++++++++ plinth/modules/privacy/urls.py | 10 +++ plinth/modules/privacy/views.py | 38 +++++++++ plinth/tests/functional/__init__.py | 4 +- pyproject.toml | 1 + 11 files changed, 271 insertions(+), 2 deletions(-) create mode 100644 plinth/modules/privacy/__init__.py create mode 100644 plinth/modules/privacy/data/etc/plinth/modules-enabled/privacy create mode 100644 plinth/modules/privacy/forms.py create mode 100644 plinth/modules/privacy/manifest.py create mode 100644 plinth/modules/privacy/privileged.py create mode 100644 plinth/modules/privacy/tests/__init__.py create mode 100644 plinth/modules/privacy/tests/test_functional.py create mode 100644 plinth/modules/privacy/urls.py create mode 100644 plinth/modules/privacy/views.py diff --git a/plinth/modules/privacy/__init__.py b/plinth/modules/privacy/__init__.py new file mode 100644 index 000000000..e0bdfacd8 --- /dev/null +++ b/plinth/modules/privacy/__init__.py @@ -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') diff --git a/plinth/modules/privacy/data/etc/plinth/modules-enabled/privacy b/plinth/modules/privacy/data/etc/plinth/modules-enabled/privacy new file mode 100644 index 000000000..a782d038c --- /dev/null +++ b/plinth/modules/privacy/data/etc/plinth/modules-enabled/privacy @@ -0,0 +1 @@ +plinth.modules.privacy diff --git a/plinth/modules/privacy/forms.py b/plinth/modules/privacy/forms.py new file mode 100644 index 000000000..c824b63a5 --- /dev/null +++ b/plinth/modules/privacy/forms.py @@ -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 popcon.debian.org. Submission happens over ' + 'the Tor network for additional anonymity if Tor app is enabled.' + ), box_name=_(cfg.box_name))) diff --git a/plinth/modules/privacy/manifest.py b/plinth/modules/privacy/manifest.py new file mode 100644 index 000000000..a10591d1c --- /dev/null +++ b/plinth/modules/privacy/manifest.py @@ -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)]}} diff --git a/plinth/modules/privacy/privileged.py b/plinth/modules/privacy/privileged.py new file mode 100644 index 000000000..e5a790276 --- /dev/null +++ b/plinth/modules/privacy/privileged.py @@ -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 diff --git a/plinth/modules/privacy/tests/__init__.py b/plinth/modules/privacy/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plinth/modules/privacy/tests/test_functional.py b/plinth/modules/privacy/tests/test_functional.py new file mode 100644 index 000000000..21b0ee1f4 --- /dev/null +++ b/plinth/modules/privacy/tests/test_functional.py @@ -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') diff --git a/plinth/modules/privacy/urls.py b/plinth/modules/privacy/urls.py new file mode 100644 index 000000000..d0c28ee5c --- /dev/null +++ b/plinth/modules/privacy/urls.py @@ -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'), +] diff --git a/plinth/modules/privacy/views.py b/plinth/modules/privacy/views.py new file mode 100644 index 000000000..a60e091e1 --- /dev/null +++ b/plinth/modules/privacy/views.py @@ -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) diff --git a/plinth/tests/functional/__init__.py b/plinth/tests/functional/__init__.py index 1af83bc06..508c4980d 100644 --- a/plinth/tests/functional/__init__.py +++ b/plinth/tests/functional/__init__.py @@ -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' ] diff --git a/pyproject.toml b/pyproject.toml index 5ef7e8f2f..0af5e8616 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,7 @@ markers = [ "openvpn", "pagekite", "performance", + "privacy", "privoxy", "quassel", "radicale",