mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-04-29 10:10:19 +00:00
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:
parent
5dd2751514
commit
7e2ebcb743
79
plinth/modules/privacy/__init__.py
Normal file
79
plinth/modules/privacy/__init__.py
Normal 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')
|
||||||
@ -0,0 +1 @@
|
|||||||
|
plinth.modules.privacy
|
||||||
24
plinth/modules/privacy/forms.py
Normal file
24
plinth/modules/privacy/forms.py
Normal 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)))
|
||||||
6
plinth/modules/privacy/manifest.py
Normal file
6
plinth/modules/privacy/manifest.py
Normal 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)]}}
|
||||||
51
plinth/modules/privacy/privileged.py
Normal file
51
plinth/modules/privacy/privileged.py
Normal 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
|
||||||
0
plinth/modules/privacy/tests/__init__.py
Normal file
0
plinth/modules/privacy/tests/__init__.py
Normal file
59
plinth/modules/privacy/tests/test_functional.py
Normal file
59
plinth/modules/privacy/tests/test_functional.py
Normal 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')
|
||||||
10
plinth/modules/privacy/urls.py
Normal file
10
plinth/modules/privacy/urls.py
Normal 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'),
|
||||||
|
]
|
||||||
38
plinth/modules/privacy/views.py
Normal file
38
plinth/modules/privacy/views.py
Normal 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)
|
||||||
@ -50,8 +50,8 @@ _site_url = {
|
|||||||
_sys_modules = [
|
_sys_modules = [
|
||||||
'avahi', 'backups', 'bind', 'cockpit', 'config', 'datetime', 'diagnostics',
|
'avahi', 'backups', 'bind', 'cockpit', 'config', 'datetime', 'diagnostics',
|
||||||
'dynamicdns', 'firewall', 'letsencrypt', 'names', 'networks', 'pagekite',
|
'dynamicdns', 'firewall', 'letsencrypt', 'names', 'networks', 'pagekite',
|
||||||
'performance', 'power', 'security', 'snapshot', 'ssh', 'storage',
|
'performance', 'power', 'privacy', 'security', 'snapshot', 'ssh',
|
||||||
'upgrades', 'users'
|
'storage', 'upgrades', 'users'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -46,6 +46,7 @@ markers = [
|
|||||||
"openvpn",
|
"openvpn",
|
||||||
"pagekite",
|
"pagekite",
|
||||||
"performance",
|
"performance",
|
||||||
|
"privacy",
|
||||||
"privoxy",
|
"privoxy",
|
||||||
"quassel",
|
"quassel",
|
||||||
"radicale",
|
"radicale",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user