diff --git a/doc/dev/tutorial/components.rst b/doc/dev/tutorial/components.rst
index c22b3eaaf..5886fcf23 100644
--- a/doc/dev/tutorial/components.rst
+++ b/doc/dev/tutorial/components.rst
@@ -163,11 +163,19 @@ with the FreedomBox framework in ``__init.py__``.
.. code-block:: python3
- group = ('bit-torrent', 'Download files using BitTorrent applications')
+ from plinth.modules.users.components import UsersAndGroups
- def init():
+ class TransmissionApp(app_module.App):
...
- register_group(group)
+
+ def __init__(self):
+ ...
+
+ groups = { 'bit-torrent': _('Download files using BitTorrent applications') }
+ users_and_groups = UsersAndGroups('users-and-groups-transmission',
+ groups=groups)
+ self.add(users_and_groups)
+
Then in the Apache configuration snippet, we can mandate that only users of this
group (and, of course, admin users) should be allowed to access our app. In the
diff --git a/plinth/modules/deluge/__init__.py b/plinth/modules/deluge/__init__.py
index 7ca3add71..b70bc8fb7 100644
--- a/plinth/modules/deluge/__init__.py
+++ b/plinth/modules/deluge/__init__.py
@@ -11,7 +11,8 @@ from plinth import frontpage, menu
from plinth.daemon import Daemon
from plinth.modules.apache.components import Webserver
from plinth.modules.firewall.components import Firewall
-from plinth.modules.users import add_user_to_share_group, register_group
+from plinth.modules.users import add_user_to_share_group
+from plinth.modules.users.components import UsersAndGroups
from .manifest import backup, clients # noqa, pylint: disable=unused-import
@@ -27,12 +28,10 @@ _description = [
'change it immediately after enabling this service.')
]
-group = ('bit-torrent', _('Download files using BitTorrent applications'))
-
-reserved_usernames = ['debian-deluged']
-
app = None
+SYSTEM_USER = 'debian-deluged'
+
class DelugeApp(app_module.App):
"""FreedomBox app for Deluge."""
@@ -42,6 +41,11 @@ class DelugeApp(app_module.App):
def __init__(self):
"""Create components for the app."""
super().__init__()
+
+ groups = {
+ 'bit-torrent': _('Download files using BitTorrent applications')
+ }
+
info = app_module.Info(app_id=self.app_id, version=version,
name=_('Deluge'), icon_filename='deluge',
short_description=_('BitTorrent Web Client'),
@@ -59,7 +63,7 @@ class DelugeApp(app_module.App):
url='/deluge', icon=info.icon_filename,
clients=info.clients,
login_required=True,
- allowed_groups=[group[0]])
+ allowed_groups=list(groups))
self.add(shortcut)
firewall = Firewall('firewall-deluge', info.name,
@@ -78,12 +82,16 @@ class DelugeApp(app_module.App):
listen_ports=[(8112, 'tcp4')])
self.add(daemon_web)
+ users_and_groups = UsersAndGroups('users-and-groups-deluge',
+ reserved_usernames=[SYSTEM_USER],
+ groups=groups)
+ self.add(users_and_groups)
+
def init():
"""Initialize the Deluge module."""
global app
app = DelugeApp()
- register_group(group)
setup_helper = globals()['setup_helper']
if setup_helper.get_state() != 'needs-setup' and app.is_enabled():
@@ -94,5 +102,5 @@ def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(managed_packages)
helper.call('post', actions.superuser_run, 'deluge', ['setup'])
- add_user_to_share_group(reserved_usernames[0])
+ add_user_to_share_group(SYSTEM_USER)
helper.call('post', app.enable)
diff --git a/plinth/modules/deluge/forms.py b/plinth/modules/deluge/forms.py
index a969cb301..65e766bbb 100644
--- a/plinth/modules/deluge/forms.py
+++ b/plinth/modules/deluge/forms.py
@@ -5,18 +5,18 @@ Forms for Deluge app.
from django.utils.translation import ugettext_lazy as _
-from plinth.modules.deluge import reserved_usernames
from plinth.modules.storage.forms import (DirectorySelectForm,
DirectoryValidator)
+from . import SYSTEM_USER
+
class DelugeForm(DirectorySelectForm):
"""Deluge configuration form"""
def __init__(self, *args, **kw):
- validator = DirectoryValidator(username=reserved_usernames[0],
+ validator = DirectoryValidator(username=SYSTEM_USER,
check_creatable=True)
- super(DelugeForm, self).__init__(
- title=_('Download directory'),
- default='/var/lib/deluged/Downloads', validator=validator, *args,
- **kw)
+ super(DelugeForm, self).__init__(title=_('Download directory'),
+ default='/var/lib/deluged/Downloads',
+ validator=validator, *args, **kw)
diff --git a/plinth/modules/dynamicdns/__init__.py b/plinth/modules/dynamicdns/__init__.py
index f254f52a5..77a0d5823 100644
--- a/plinth/modules/dynamicdns/__init__.py
+++ b/plinth/modules/dynamicdns/__init__.py
@@ -11,6 +11,7 @@ from plinth import cfg, menu
from plinth.modules.names.components import DomainType
from plinth.signals import domain_added
from plinth.utils import format_lazy
+from plinth.modules.users.components import UsersAndGroups
from .manifest import backup # noqa, pylint: disable=unused-import
@@ -38,8 +39,6 @@ _description = [
'name, they will get a response with your current IP address.')
]
-reserved_usernames = ['ez-ipupd']
-
app = None
@@ -67,6 +66,10 @@ class DynamicDNSApp(app_module.App):
can_have_certificate=True)
self.add(domain_type)
+ users_and_groups = UsersAndGroups('users-and-groups-dynamicdns',
+ reserved_usernames=['ez-ipupd'])
+ self.add(users_and_groups)
+
def init():
"""Initialize the module."""
diff --git a/plinth/modules/ejabberd/__init__.py b/plinth/modules/ejabberd/__init__.py
index 94ee504cc..76e44f533 100644
--- a/plinth/modules/ejabberd/__init__.py
+++ b/plinth/modules/ejabberd/__init__.py
@@ -21,6 +21,7 @@ from plinth.modules.letsencrypt.components import LetsEncrypt
from plinth.signals import (domain_added, post_hostname_change,
pre_hostname_change)
from plinth.utils import format_lazy
+from plinth.modules.users.components import UsersAndGroups
from .manifest import backup, clients # noqa, pylint: disable=unused-import
@@ -45,8 +46,6 @@ _description = [
jsxc_url=reverse_lazy('jsxc:index'))
]
-reserved_usernames = ['ejabberd']
-
port_forwarding_info = [
('TCP', 5222),
('TCP', 5269),
@@ -110,6 +109,10 @@ class EjabberdApp(app_module.App):
(5269, 'tcp6'), (5443, 'tcp4'), (5443, 'tcp6')])
self.add(daemon)
+ users_and_groups = UsersAndGroups('users-and-groups-ejabberd',
+ reserved_usernames=['ejabberd'])
+ self.add(users_and_groups)
+
def init():
"""Initialize the ejabberd module"""
diff --git a/plinth/modules/gitweb/__init__.py b/plinth/modules/gitweb/__init__.py
index 484946658..c9f57d287 100644
--- a/plinth/modules/gitweb/__init__.py
+++ b/plinth/modules/gitweb/__init__.py
@@ -14,7 +14,7 @@ from plinth import frontpage, menu
from plinth.errors import ActionError
from plinth.modules.apache.components import Webserver
from plinth.modules.firewall.components import Firewall
-from plinth.modules.users import register_group
+from plinth.modules.users.components import UsersAndGroups
from .forms import is_repo_url
from .manifest import (GIT_REPO_PATH, # noqa, pylint: disable=unused-import
@@ -36,8 +36,6 @@ _description = [
'Git tutorial.')
]
-group = ('git-access', _('Read-write access to Git repositories'))
-
app = None
@@ -50,6 +48,8 @@ class GitwebApp(app_module.App):
"""Create components for the app."""
super().__init__()
+ groups = {'git-access': _('Read-write access to Git repositories')}
+
self.repos = []
info = app_module.Info(app_id=self.app_id, version=version,
@@ -69,7 +69,7 @@ class GitwebApp(app_module.App):
icon=info.icon_filename, url='/gitweb/',
clients=info.clients,
login_required=True,
- allowed_groups=[group[0]])
+ allowed_groups=list(groups))
self.add(shortcut)
firewall = Firewall('firewall-gitweb', info.name,
@@ -84,6 +84,10 @@ class GitwebApp(app_module.App):
'gitweb-freedombox-auth')
self.add(self.auth_webserver)
+ users_and_groups = UsersAndGroups('users-and-groups-gitweb',
+ groups=groups)
+ self.add(users_and_groups)
+
def set_shortcut_login_required(self, login_required):
"""Change the login_required property of shortcut."""
shortcut = self.remove('shortcut-gitweb')
@@ -163,7 +167,6 @@ def init():
"""Initialize the module."""
global app
app = GitwebApp()
- register_group(group)
setup_helper = globals()['setup_helper']
if setup_helper.get_state() != 'needs-setup':
diff --git a/plinth/modules/i2p/__init__.py b/plinth/modules/i2p/__init__.py
index edcbbe8af..cbdf49d14 100644
--- a/plinth/modules/i2p/__init__.py
+++ b/plinth/modules/i2p/__init__.py
@@ -12,7 +12,7 @@ from plinth.daemon import Daemon
from plinth.modules.apache.components import Webserver
from plinth.modules.firewall.components import Firewall
from plinth.modules.i2p.resources import FAVORITES
-from plinth.modules.users import register_group
+from plinth.modules.users.components import UsersAndGroups
from .manifest import backup, clients # noqa, pylint: disable=unused-import
@@ -35,8 +35,6 @@ _description = [
'configuration process.')
]
-group = ('i2p', _('Manage I2P application'))
-
port_forwarding_info = [
('TCP', 4444),
('TCP', 4445),
@@ -60,6 +58,9 @@ class I2PApp(app_module.App):
def __init__(self):
"""Create components for the app."""
super().__init__()
+
+ groups = {'i2p': _('Manage I2P application')}
+
info = app_module.Info(app_id=self.app_id, version=version,
name=_('I2P'), icon_filename='i2p',
short_description=_('Anonymity Network'),
@@ -77,7 +78,7 @@ class I2PApp(app_module.App):
icon=info.icon_filename, url='/i2p/',
clients=info.clients,
login_required=True,
- allowed_groups=[group[0]])
+ allowed_groups=list(groups))
self.add(shortcut)
firewall = Firewall('firewall-i2p-web', info.name,
@@ -97,12 +98,15 @@ class I2PApp(app_module.App):
listen_ports=[(7657, 'tcp6')])
self.add(daemon)
+ users_and_groups = UsersAndGroups('users-and-groups-i2p',
+ groups=groups)
+ self.add(users_and_groups)
+
def init():
"""Initialize the module."""
global app
app = I2PApp()
- register_group(group)
setup_helper = globals()['setup_helper']
if setup_helper.get_state() != 'needs-setup' and app.is_enabled():
diff --git a/plinth/modules/ikiwiki/__init__.py b/plinth/modules/ikiwiki/__init__.py
index b2e3c5494..4e8e738c3 100644
--- a/plinth/modules/ikiwiki/__init__.py
+++ b/plinth/modules/ikiwiki/__init__.py
@@ -11,7 +11,7 @@ from plinth import app as app_module
from plinth import cfg, frontpage, menu
from plinth.modules.apache.components import Webserver
from plinth.modules.firewall.components import Firewall
-from plinth.modules.users import register_group
+from plinth.modules.users.components import UsersAndGroups
from plinth.utils import format_lazy
from .manifest import backup, clients # noqa, pylint: disable=unused-import
@@ -36,8 +36,6 @@ _description = [
users_url=reverse_lazy('users:index'))
]
-group = ('wiki', _('View and edit wiki applications'))
-
app = None
@@ -49,6 +47,7 @@ class IkiwikiApp(app_module.App):
def __init__(self):
"""Create components for the app."""
super().__init__()
+
info = app_module.Info(app_id=self.app_id, version=version,
name=_('ikiwiki'), icon_filename='ikiwiki',
short_description=_('Wiki and Blog'),
@@ -71,6 +70,11 @@ class IkiwikiApp(app_module.App):
urls=['https://{host}/ikiwiki'])
self.add(webserver)
+ groups = {'wiki': _('View and edit wiki applications')}
+ users_and_groups = UsersAndGroups('users-and-groups-ikiwiki',
+ groups=groups)
+ self.add(users_and_groups)
+
def add_shortcut(self, site, title):
"""Add an ikiwiki shortcut to frontpage."""
shortcut = frontpage.Shortcut('shortcut-ikiwiki-' + site, title,
@@ -101,7 +105,6 @@ def init():
"""Initialize the ikiwiki module."""
global app
app = IkiwikiApp()
- register_group(group)
setup_helper = globals()['setup_helper']
if setup_helper.get_state() != 'needs-setup' and app.is_enabled():
diff --git a/plinth/modules/minetest/__init__.py b/plinth/modules/minetest/__init__.py
index 4fc1fd5ee..c8d7d373a 100644
--- a/plinth/modules/minetest/__init__.py
+++ b/plinth/modules/minetest/__init__.py
@@ -12,6 +12,7 @@ from plinth import cfg, frontpage, menu
from plinth.daemon import Daemon
from plinth.modules.firewall.components import Firewall
from plinth.utils import format_lazy
+from plinth.modules.users.components import UsersAndGroups
from .manifest import backup, clients # noqa, pylint: disable=unused-import
@@ -43,8 +44,6 @@ _description = [
port_forwarding_info = [('UDP', 30000)]
-reserved_usernames = ['Debian-minetest']
-
CONFIG_FILE = '/etc/minetest/minetest.conf'
AUG_PATH = '/files' + CONFIG_FILE + '/.anon'
@@ -87,6 +86,11 @@ class MinetestApp(app_module.App):
listen_ports=[(30000, 'udp4')])
self.add(daemon)
+ users_and_groups = UsersAndGroups(
+ 'users-and-groups-minetest',
+ reserved_usernames=['Debian-minetest'])
+ self.add(users_and_groups)
+
def init():
"""Initialize the module."""
diff --git a/plinth/modules/minidlna/__init__.py b/plinth/modules/minidlna/__init__.py
index b74d68f76..778b971b5 100644
--- a/plinth/modules/minidlna/__init__.py
+++ b/plinth/modules/minidlna/__init__.py
@@ -9,7 +9,7 @@ from plinth import actions, frontpage, menu
from plinth.daemon import Daemon
from plinth.modules.apache.components import Webserver
from plinth.modules.firewall.components import Firewall
-from plinth.modules.users import register_group
+from plinth.modules.users.components import UsersAndGroups
from .manifest import backup, clients # noqa
@@ -30,8 +30,6 @@ _description = [
'such as PS3 and Xbox 360) or applications such as totem and Kodi.')
]
-group = ('minidlna', _('Media streaming server'))
-
app = None
@@ -42,6 +40,9 @@ class MiniDLNAApp(app_module.App):
def __init__(self):
"""Initialize the app components"""
super().__init__()
+
+ groups = {'minidlna': _('Media streaming server')}
+
info = app_module.Info(app_id=self.app_id, version=version,
name='minidlna', icon_filename='minidlna',
short_description=_('Simple Media Server'),
@@ -60,16 +61,12 @@ class MiniDLNAApp(app_module.App):
is_external=False)
webserver = Webserver('webserver-minidlna', 'minidlna-freedombox',
urls=['http://localhost:8200/'])
- shortcut = frontpage.Shortcut(
- 'shortcut-minidlna',
- info.name,
- short_description=info.short_description,
- description=info.description,
- icon=info.icon_filename,
- url='/_minidlna/',
- login_required=True,
- allowed_groups=[group[0]],
- )
+ shortcut = frontpage.Shortcut('shortcut-minidlna', info.name,
+ short_description=info.short_description,
+ description=info.description,
+ icon=info.icon_filename,
+ url='/_minidlna/', login_required=True,
+ allowed_groups=list(groups))
daemon = Daemon('daemon-minidlna', managed_services[0])
self.add(menu_item)
@@ -78,12 +75,15 @@ class MiniDLNAApp(app_module.App):
self.add(shortcut)
self.add(daemon)
+ users_and_groups = UsersAndGroups('users-and-groups-minidlna',
+ groups=groups)
+ self.add(users_and_groups)
+
def init():
"""Initialize the module."""
global app
app = MiniDLNAApp()
- register_group(group)
setup_helper = globals()['setup_helper']
if setup_helper.get_state() != 'needs-setup' and app.is_enabled():
diff --git a/plinth/modules/mldonkey/__init__.py b/plinth/modules/mldonkey/__init__.py
index 7edd0de5b..d6572ef2f 100644
--- a/plinth/modules/mldonkey/__init__.py
+++ b/plinth/modules/mldonkey/__init__.py
@@ -11,7 +11,7 @@ from plinth import cfg, frontpage, menu
from plinth.daemon import Daemon
from plinth.modules.apache.components import Webserver
from plinth.modules.firewall.components import Firewall
-from plinth.modules.users import register_group
+from plinth.modules.users.components import UsersAndGroups
from plinth.utils import format_lazy
from .manifest import backup, clients # noqa, pylint: disable=unused-import
@@ -35,10 +35,6 @@ _description = [
'directory.'), box_name=cfg.box_name)
]
-reserved_usernames = ['mldonkey']
-
-group = ('ed2k', _('Download files using eDonkey applications'))
-
app = None
@@ -50,6 +46,9 @@ class MLDonkeyApp(app_module.App):
def __init__(self):
"""Create components for the app."""
super().__init__()
+
+ groups = {'ed2k': _('Download files using eDonkey applications')}
+
info = app_module.Info(
app_id=self.app_id, version=version, name=_('MLDonkey'),
icon_filename='mldonkey',
@@ -66,7 +65,7 @@ class MLDonkeyApp(app_module.App):
'shortcut-mldonkey', info.name,
short_description=info.short_description, icon=info.icon_filename,
url='/mldonkey/', login_required=True, clients=info.clients,
- allowed_groups=[group[0]])
+ allowed_groups=list(groups))
self.add(shortcuts)
firewall = Firewall('firewall-mldonkey', info.name,
@@ -81,12 +80,16 @@ class MLDonkeyApp(app_module.App):
listen_ports=[(4080, 'tcp4')])
self.add(daemon)
+ users_and_groups = UsersAndGroups('users-and-groups-mldonkey',
+ reserved_usernames=['mldonkey'],
+ groups=groups)
+ self.add(users_and_groups)
+
def init():
"""Initialize the MLDonkey module."""
global app
app = MLDonkeyApp()
- register_group(group)
setup_helper = globals()['setup_helper']
if setup_helper.get_state() != 'needs-setup' and app.is_enabled():
diff --git a/plinth/modules/monkeysphere/__init__.py b/plinth/modules/monkeysphere/__init__.py
index 05063519b..5a16a383e 100644
--- a/plinth/modules/monkeysphere/__init__.py
+++ b/plinth/modules/monkeysphere/__init__.py
@@ -7,6 +7,7 @@ from django.utils.translation import ugettext_lazy as _
from plinth import app as app_module
from plinth import menu
+from plinth.modules.users.components import UsersAndGroups
from .manifest import backup # noqa, pylint: disable=unused-import
@@ -33,8 +34,6 @@ _description = [
'website.')
]
-reserved_usernames = ['monkeysphere']
-
app = None
@@ -57,6 +56,10 @@ class MonkeysphereApp(app_module.App):
advanced=True)
self.add(menu_item)
+ users_and_groups = UsersAndGroups('users-and-groups-monkeysphere',
+ reserved_usernames=['monkeysphere'])
+ self.add(users_and_groups)
+
def init():
"""Initialize the monkeysphere module."""
diff --git a/plinth/modules/mumble/__init__.py b/plinth/modules/mumble/__init__.py
index dc0b3ad32..94c4896b7 100644
--- a/plinth/modules/mumble/__init__.py
+++ b/plinth/modules/mumble/__init__.py
@@ -10,6 +10,7 @@ from plinth import app as app_module
from plinth import frontpage, menu
from plinth.daemon import Daemon
from plinth.modules.firewall.components import Firewall
+from plinth.modules.users.components import UsersAndGroups
from .manifest import backup, clients # noqa, pylint: disable=unused-import
@@ -27,8 +28,6 @@ _description = [
'from your desktop and Android devices are available.')
]
-reserved_usernames = ['mumble-server']
-
port_forwarding_info = [
('TCP', 64738),
('UDP', 64738),
@@ -73,6 +72,10 @@ class MumbleApp(app_module.App):
(64738, 'udp6')])
self.add(daemon)
+ users_and_groups = UsersAndGroups('users-and-groups-mumble',
+ reserved_usernames=['mumble-server'])
+ self.add(users_and_groups)
+
def init():
"""Initialize the Mumble module."""
diff --git a/plinth/modules/privoxy/__init__.py b/plinth/modules/privoxy/__init__.py
index 40ee6c082..2848ad32b 100644
--- a/plinth/modules/privoxy/__init__.py
+++ b/plinth/modules/privoxy/__init__.py
@@ -13,6 +13,7 @@ from plinth.daemon import Daemon
from plinth.modules.apache.components import diagnose_url
from plinth.modules.firewall.components import Firewall
from plinth.utils import format_lazy
+from plinth.modules.users.components import UsersAndGroups
from .manifest import backup # noqa, pylint: disable=unused-import
@@ -39,8 +40,6 @@ _description = [
box_name=_(cfg.box_name)),
]
-reserved_usernames = ['privoxy']
-
app = None
@@ -78,6 +77,10 @@ class PrivoxyApp(app_module.App):
listen_ports=[(8118, 'tcp4'), (8118, 'tcp6')])
self.add(daemon)
+ users_and_groups = UsersAndGroups('users-and-groups-privoxy',
+ reserved_usernames=['privoxy'])
+ self.add(users_and_groups)
+
def diagnose(self):
"""Run diagnostics and return the results."""
results = super().diagnose()
diff --git a/plinth/modules/quassel/__init__.py b/plinth/modules/quassel/__init__.py
index 319e81cb7..90f1f4cfc 100644
--- a/plinth/modules/quassel/__init__.py
+++ b/plinth/modules/quassel/__init__.py
@@ -16,6 +16,7 @@ from plinth.modules import names
from plinth.modules.firewall.components import Firewall
from plinth.modules.letsencrypt.components import LetsEncrypt
from plinth.utils import format_lazy
+from plinth.modules.users.components import UsersAndGroups
from .manifest import backup, clients # noqa, pylint: disable=unused-import
@@ -43,8 +44,6 @@ _description = [
'are available.'),
]
-reserved_usernames = ['quasselcore']
-
port_forwarding_info = [('TCP', 4242)]
app = None
@@ -95,6 +94,10 @@ class QuasselApp(app_module.App):
listen_ports=[(4242, 'tcp4'), (4242, 'tcp6')])
self.add(daemon)
+ users_and_groups = UsersAndGroups('users-and-groups-quasselcore',
+ reserved_usernames=['quasselcore'])
+ self.add(users_and_groups)
+
def init():
"""Initialize the quassel module."""
diff --git a/plinth/modules/radicale/__init__.py b/plinth/modules/radicale/__init__.py
index 3ab67c2b3..e7d7d1a21 100644
--- a/plinth/modules/radicale/__init__.py
+++ b/plinth/modules/radicale/__init__.py
@@ -17,6 +17,7 @@ from plinth import cfg, frontpage, menu
from plinth.daemon import Daemon
from plinth.modules.apache.components import Uwsgi, Webserver
from plinth.modules.firewall.components import Firewall
+from plinth.modules.users.components import UsersAndGroups
from plinth.utils import format_lazy, Version
from .manifest import backup, clients # noqa, pylint: disable=unused-import
@@ -39,8 +40,6 @@ _description = [
'contacts, which must be done using a separate client.'),
]
-reserved_usernames = ['radicale']
-
logger = logging.getLogger(__name__)
CONFIG_FILE = '/etc/radicale/config'
@@ -91,6 +90,10 @@ class RadicaleApp(app_module.App):
daemon = RadicaleDaemon('daemon-radicale', managed_services[0])
self.add(daemon)
+ users_and_groups = UsersAndGroups('users-and-groups-radicale',
+ reserved_usernames=['radicale'])
+ self.add(users_and_groups)
+
class RadicaleWebserver(Webserver):
"""Webserver enable/disable behavior specific for radicale."""
diff --git a/plinth/modules/samba/__init__.py b/plinth/modules/samba/__init__.py
index 48cff4d64..e6f9376af 100644
--- a/plinth/modules/samba/__init__.py
+++ b/plinth/modules/samba/__init__.py
@@ -17,7 +17,7 @@ from plinth import app as app_module
from plinth import frontpage, menu
from plinth.daemon import Daemon
from plinth.modules.firewall.components import Firewall
-from plinth.modules.users import register_group
+from plinth.modules.users.components import UsersAndGroups
from plinth.utils import format_lazy
from .manifest import backup, clients # noqa, pylint: disable=unused-import
@@ -44,8 +44,6 @@ _description = [
'own private space.'),
]
-group = ('freedombox-share', _('Access to the private shares'))
-
app = None
@@ -57,6 +55,9 @@ class SambaApp(app_module.App):
def __init__(self):
"""Create components for the app."""
super().__init__()
+
+ groups = {'freedombox-share': _('Access to the private shares')}
+
info = app_module.Info(app_id=self.app_id, version=version,
name=_('Samba'), icon_filename='samba',
short_description=_('File Sharing'),
@@ -74,7 +75,7 @@ class SambaApp(app_module.App):
short_description=info.short_description, icon=info.icon_filename,
description=info.description,
configure_url=reverse_lazy('samba:index'), clients=info.clients,
- login_required=True, allowed_groups=[group[0]])
+ login_required=True, allowed_groups=list(groups))
self.add(shortcut)
firewall = Firewall('firewall-samba', info.name, ports=['samba'])
@@ -92,12 +93,15 @@ class SambaApp(app_module.App):
self.add(daemon_nmbd)
+ users_and_groups = UsersAndGroups('users-and-groups-samba',
+ groups=groups)
+ self.add(users_and_groups)
+
def init():
"""Initialize the module."""
global app
app = SambaApp()
- register_group(group)
setup_helper = globals()['setup_helper']
if setup_helper.get_state() != 'needs-setup' and app.is_enabled():
diff --git a/plinth/modules/searx/__init__.py b/plinth/modules/searx/__init__.py
index 4f0276d66..18ff6f50f 100644
--- a/plinth/modules/searx/__init__.py
+++ b/plinth/modules/searx/__init__.py
@@ -12,7 +12,7 @@ from plinth import app as app_module
from plinth import frontpage, menu
from plinth.modules.apache.components import Uwsgi, Webserver
from plinth.modules.firewall.components import Firewall
-from plinth.modules.users import register_group
+from plinth.modules.users.components import UsersAndGroups
from .manifest import (PUBLIC_ACCESS_SETTING_FILE, # noqa, pylint: disable=unused-import
backup, clients)
@@ -28,8 +28,6 @@ _description = [
'It stores no cookies by default.')
]
-group = ('web-search', _('Search the web'))
-
manual_page = 'Searx'
app = None
@@ -43,6 +41,9 @@ class SearxApp(app_module.App):
def __init__(self):
"""Create components for the app."""
super().__init__()
+
+ groups = {'web-search': _('Search the web')}
+
info = app_module.Info(app_id=self.app_id, version=version,
name=_('Searx'), icon_filename='searx',
short_description=_('Web Search'),
@@ -60,7 +61,7 @@ class SearxApp(app_module.App):
short_description=info.short_description, icon=info.icon_filename,
url='/searx/', clients=info.clients,
login_required=(not is_public_access_enabled()),
- allowed_groups=[group[0]])
+ allowed_groups=list(groups))
self.add(shortcut)
firewall = Firewall('firewall-searx', info.name,
@@ -78,6 +79,10 @@ class SearxApp(app_module.App):
uwsgi = Uwsgi('uwsgi-searx', 'searx')
self.add(uwsgi)
+ users_and_groups = UsersAndGroups('users-and-groups-searx',
+ groups=groups)
+ self.add(users_and_groups)
+
def set_shortcut_login_required(self, login_required):
"""Change the login_required property of shortcut."""
shortcut = self.remove('shortcut-searx')
@@ -101,7 +106,6 @@ def init():
"""Initialize the module."""
global app
app = SearxApp()
- register_group(group)
setup_helper = globals()['setup_helper']
if setup_helper.get_state() != 'needs-setup' and app.is_enabled():
diff --git a/plinth/modules/sharing/forms.py b/plinth/modules/sharing/forms.py
index fc74fa2b9..29b067e8c 100644
--- a/plinth/modules/sharing/forms.py
+++ b/plinth/modules/sharing/forms.py
@@ -8,7 +8,7 @@ from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from plinth.modules import sharing
-from plinth.modules.users.forms import get_group_choices
+from plinth.modules.users.components import UsersAndGroups
class AddShareForm(forms.Form):
@@ -29,6 +29,7 @@ class AddShareForm(forms.Form):
'Make files in this folder available to anyone with the link.'))
groups = forms.MultipleChoiceField(
+ choices=UsersAndGroups.get_group_choices,
widget=forms.CheckboxSelectMultiple, required=False,
label=_('User groups that can read the files in the share'),
help_text=_(
@@ -38,7 +39,6 @@ class AddShareForm(forms.Form):
def __init__(self, *args, **kwargs):
"""Initialize the form with extra request argument."""
super().__init__(*args, **kwargs)
- self.fields['groups'].choices = get_group_choices()
self.fields['name'].widget.attrs.update({'autofocus': 'autofocus'})
def clean_name(self):
diff --git a/plinth/modules/syncthing/__init__.py b/plinth/modules/syncthing/__init__.py
index ac43cc1a1..f1a8a01ec 100644
--- a/plinth/modules/syncthing/__init__.py
+++ b/plinth/modules/syncthing/__init__.py
@@ -11,7 +11,7 @@ from plinth import cfg, frontpage, menu
from plinth.daemon import Daemon
from plinth.modules.apache.components import Webserver
from plinth.modules.firewall.components import Firewall
-from plinth.modules.users import register_group
+from plinth.modules.users.components import UsersAndGroups
from plinth.utils import format_lazy
from .manifest import backup, clients # noqa, pylint: disable=unused-import
@@ -37,8 +37,6 @@ _description = [
'users belonging to the "admin" group.'), box_name=_(cfg.box_name)),
]
-group = ('syncthing', _('Administer Syncthing application'))
-
app = None
@@ -50,6 +48,9 @@ class SyncthingApp(app_module.App):
def __init__(self):
"""Create components for the app."""
super().__init__()
+
+ self.groups = {'syncthing': _('Administer Syncthing application')}
+
info = app_module.Info(app_id=self.app_id, version=version,
name=_('Syncthing'), icon_filename='syncthing',
short_description=_('File Synchronization'),
@@ -67,7 +68,7 @@ class SyncthingApp(app_module.App):
icon=info.icon_filename,
url='/syncthing/', clients=info.clients,
login_required=True,
- allowed_groups=[group[0]])
+ allowed_groups=list(self.groups))
self.add(shortcut)
firewall = Firewall('firewall-syncthing-web', info.name,
@@ -85,12 +86,15 @@ class SyncthingApp(app_module.App):
daemon = Daemon('daemon-syncthing', managed_services[0])
self.add(daemon)
+ users_and_groups = UsersAndGroups('users-and-groups-syncthing',
+ groups=self.groups)
+ self.add(users_and_groups)
+
def init():
"""Initialize the module."""
global app
app = SyncthingApp()
- register_group(group)
setup_helper = globals()['setup_helper']
if setup_helper.get_state() != 'needs-setup' and app.is_enabled():
diff --git a/plinth/modules/tor/__init__.py b/plinth/modules/tor/__init__.py
index 72d03e591..6c55029f2 100644
--- a/plinth/modules/tor/__init__.py
+++ b/plinth/modules/tor/__init__.py
@@ -15,6 +15,7 @@ from plinth.modules.apache.components import diagnose_url
from plinth.modules.firewall.components import Firewall
from plinth.modules.names.components import DomainType
from plinth.signals import domain_added, domain_removed
+from plinth.modules.users.components import UsersAndGroups
from . import utils
from .manifest import backup, clients # noqa, pylint: disable=unused-import
@@ -38,8 +39,6 @@ _description = [
'Tor Browser.')
]
-reserved_usernames = ['debian-tor']
-
app = None
@@ -82,6 +81,10 @@ class TorApp(app_module.App):
(9040, 'tcp6'), (9053, 'udp4'), (9053, 'udp6')])
self.add(daemon)
+ users_and_groups = UsersAndGroups('users-and-groups-tor',
+ reserved_usernames=['debian-tor'])
+ self.add(users_and_groups)
+
def diagnose(self):
"""Run diagnostics and return the results."""
results = super().diagnose()
diff --git a/plinth/modules/transmission/__init__.py b/plinth/modules/transmission/__init__.py
index eac87db5f..29bf1367c 100644
--- a/plinth/modules/transmission/__init__.py
+++ b/plinth/modules/transmission/__init__.py
@@ -13,7 +13,8 @@ from plinth import frontpage, menu
from plinth.daemon import Daemon
from plinth.modules.apache.components import Webserver
from plinth.modules.firewall.components import Firewall
-from plinth.modules.users import add_user_to_share_group, register_group
+from plinth.modules.users import add_user_to_share_group
+from plinth.modules.users.components import UsersAndGroups
from .manifest import backup, clients # noqa, pylint: disable=unused-import
@@ -29,12 +30,10 @@ _description = [
'BitTorrent is not anonymous.'),
]
-reserved_usernames = ['debian-transmission']
-
-group = ('bit-torrent', _('Download files using BitTorrent applications'))
-
app = None
+SYSTEM_USER = 'debian-transmission'
+
class TransmissionApp(app_module.App):
"""FreedomBox app for Transmission."""
@@ -44,6 +43,10 @@ class TransmissionApp(app_module.App):
def __init__(self):
"""Create components for the app."""
super().__init__()
+
+ groups = {
+ 'bit-torrent': _('Download files using BitTorrent applications')
+ }
info = app_module.Info(app_id=self.app_id, version=version,
name=_('Transmission'),
icon_filename='transmission',
@@ -61,7 +64,7 @@ class TransmissionApp(app_module.App):
'shortcut-transmission', info.name,
short_description=info.short_description, icon=info.icon_filename,
url='/transmission', clients=info.clients, login_required=True,
- allowed_groups=[group[0]])
+ allowed_groups=list(groups))
self.add(shortcut)
firewall = Firewall('firewall-transmission', info.name,
@@ -76,12 +79,16 @@ class TransmissionApp(app_module.App):
listen_ports=[(9091, 'tcp4')])
self.add(daemon)
+ users_and_groups = UsersAndGroups('users-and-groups-transmission',
+ reserved_usernames=[SYSTEM_USER],
+ groups=groups)
+ self.add(users_and_groups)
+
def init():
"""Initialize the Transmission module."""
global app
app = TransmissionApp()
- register_group(group)
setup_helper = globals()['setup_helper']
if setup_helper.get_state() != 'needs-setup' and app.is_enabled():
@@ -99,5 +106,5 @@ def setup(helper, old_version=None):
helper.call('post', actions.superuser_run, 'transmission',
['merge-configuration'],
input=json.dumps(new_configuration).encode())
- add_user_to_share_group(reserved_usernames[0], managed_services[0])
+ add_user_to_share_group(SYSTEM_USER, managed_services[0])
helper.call('post', app.enable)
diff --git a/plinth/modules/transmission/forms.py b/plinth/modules/transmission/forms.py
index 30a081cee..20a1abf46 100644
--- a/plinth/modules/transmission/forms.py
+++ b/plinth/modules/transmission/forms.py
@@ -5,18 +5,19 @@ FreedomBox app for configuring Transmission.
from django.utils.translation import ugettext_lazy as _
-from plinth.modules.transmission import reserved_usernames
from plinth.modules.storage.forms import (DirectorySelectForm,
DirectoryValidator)
+from . import SYSTEM_USER
+
class TransmissionForm(DirectorySelectForm):
"""Transmission configuration form"""
def __init__(self, *args, **kw):
- validator = DirectoryValidator(
- username=reserved_usernames[0], check_creatable=True)
- super(TransmissionForm, self).__init__(
- title=_('Download directory'),
- default='/var/lib/transmission-daemon/downloads',
- validator=validator, *args, **kw)
+ validator = DirectoryValidator(username=SYSTEM_USER,
+ check_creatable=True)
+ super(TransmissionForm,
+ self).__init__(title=_('Download directory'),
+ default='/var/lib/transmission-daemon/downloads',
+ validator=validator, *args, **kw)
diff --git a/plinth/modules/ttrss/__init__.py b/plinth/modules/ttrss/__init__.py
index 1fdc95917..d60155ed2 100644
--- a/plinth/modules/ttrss/__init__.py
+++ b/plinth/modules/ttrss/__init__.py
@@ -12,7 +12,7 @@ from plinth import cfg, frontpage, menu
from plinth.daemon import Daemon
from plinth.modules.apache.components import Webserver
from plinth.modules.firewall.components import Firewall
-from plinth.modules.users import register_group
+from plinth.modules.users.components import UsersAndGroups
from plinth.utils import Version, format_lazy
from .manifest import backup, clients # noqa, pylint: disable=unused-import
@@ -39,8 +39,6 @@ _description = [
'/tt-rss-app for connecting.'))
]
-group = ('feed-reader', _('Read and subscribe to news feeds'))
-
app = None
@@ -52,6 +50,9 @@ class TTRSSApp(app_module.App):
def __init__(self):
"""Create components for the app."""
super().__init__()
+
+ groups = {'feed-reader': _('Read and subscribe to news feeds')}
+
info = app_module.Info(app_id=self.app_id, version=version,
name=_('Tiny Tiny RSS'), icon_filename='ttrss',
short_description=_('News Feed Reader'),
@@ -69,7 +70,7 @@ class TTRSSApp(app_module.App):
icon=info.icon_filename, url='/tt-rss',
clients=info.clients,
login_required=True,
- allowed_groups=[group[0]])
+ allowed_groups=list(groups))
self.add(shortcut)
firewall = Firewall('firewall-ttrss', info.name,
@@ -83,6 +84,10 @@ class TTRSSApp(app_module.App):
daemon = Daemon('daemon-ttrss', managed_services[0])
self.add(daemon)
+ users_and_groups = UsersAndGroups('users-and-groups-ttrss',
+ groups=groups)
+ self.add(users_and_groups)
+
def enable(self):
"""Enable components and API access."""
super().enable()
@@ -93,7 +98,6 @@ def init():
"""Initialize the module."""
global app
app = TTRSSApp()
- register_group(group)
setup_helper = globals()['setup_helper']
if setup_helper.get_state() != 'needs-setup' and app.is_enabled():
diff --git a/plinth/modules/users/__init__.py b/plinth/modules/users/__init__.py
index 84e702a31..ddb27f584 100644
--- a/plinth/modules/users/__init__.py
+++ b/plinth/modules/users/__init__.py
@@ -14,6 +14,8 @@ from plinth import cfg, menu
from plinth.daemon import Daemon
from plinth.utils import format_lazy
+from .components import UsersAndGroups
+
version = 3
is_essential = True
@@ -45,9 +47,6 @@ _description = [
box_name=_(cfg.box_name))
]
-# All FreedomBox user groups
-groups = dict()
-
app = None
@@ -75,6 +74,12 @@ class UsersApp(app_module.App):
listen_ports=[(389, 'tcp4'), (389, 'tcp6')])
self.add(daemon)
+ # Add the admin group
+ groups = {'admin': _('Access to all services and system settings')}
+ users_and_groups = UsersAndGroups('users-and-groups-admin',
+ groups=groups)
+ self.add(users_and_groups)
+
def diagnose(self):
"""Run diagnostics and return the results."""
results = super().diagnose()
@@ -129,10 +134,6 @@ def remove_group(group):
actions.superuser_run('users', options=['remove-group', group])
-def register_group(group):
- groups[group[0]] = group[1]
-
-
def get_last_admin_user():
"""If there is only one admin user return its name else return None."""
output = actions.superuser_run('users', ['get-group-users', 'admin'])
diff --git a/plinth/modules/users/components.py b/plinth/modules/users/components.py
new file mode 100644
index 000000000..1d79e412b
--- /dev/null
+++ b/plinth/modules/users/components.py
@@ -0,0 +1,56 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+"""
+App component to manage users and groups.
+"""
+
+import itertools
+
+from plinth import app
+
+
+class UsersAndGroups(app.FollowerComponent):
+ """Component to manage users and groups of an app."""
+
+ # Class variable to hold a list of user groups for apps
+ _all_components = set()
+
+ def __init__(self, component_id, reserved_usernames=[], groups={}):
+ """Store reserved_usernames and groups of the app.
+
+ 'reserved_usernames' is a list of operating system user names that the
+ app uses. It is not permitted to create a FreedomBox user with one of
+ these names.
+
+ 'groups' is a dictionary of the following format: {"group_name": "A
+ localized string describing what permissions are offered to the users
+ of this group"}.
+
+ """
+ super().__init__(component_id)
+
+ self.reserved_usernames = reserved_usernames
+ self.groups = groups
+
+ self._all_components.add(self)
+
+ @classmethod
+ def get_groups(cls):
+ """Return a set of all groups."""
+ all_groups = itertools.chain(*(component.groups.keys()
+ for component in cls._all_components))
+ return set(all_groups)
+
+ @classmethod
+ def get_group_choices(cls):
+ """Return list of groups that can be used as form choices."""
+ all_groups = itertools.chain(*(component.groups.items()
+ for component in cls._all_components))
+ choices = [(group, f'{description} ({group})')
+ for group, description in set(all_groups)]
+ return sorted(choices, key=lambda g: g[0])
+
+ @classmethod
+ def is_username_reserved(cls, username):
+ """Returns whether the given username is reserved or not."""
+ return any((username in component.reserved_usernames
+ for component in cls._all_components))
diff --git a/plinth/modules/users/forms.py b/plinth/modules/users/forms.py
index 96ef0e4f0..19ea922af 100644
--- a/plinth/modules/users/forms.py
+++ b/plinth/modules/users/forms.py
@@ -3,7 +3,6 @@
import pwd
import re
-import plinth.forms
from django import forms
from django.contrib import auth, messages
from django.contrib.auth.forms import SetPasswordForm, UserCreationForm
@@ -13,23 +12,17 @@ from django.core.exceptions import ValidationError
from django.utils.deconstruct import deconstructible
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy
-from plinth import actions, module_loader
+
+import plinth.forms
+from plinth import actions
from plinth.errors import ActionError
-from plinth.modules import first_boot, users
+from plinth.modules import first_boot
from plinth.modules.security import set_restricted_access
from plinth.translation import set_language
from plinth.utils import is_user_admin
from . import get_last_admin_user
-
-
-def get_group_choices():
- """Return localized group description and group name in one string."""
- admin_group = ('admin', _('Access to all services and system settings'))
- users.register_group(admin_group)
- choices = [(k, ('{} ({})'.format(users.groups[k], k)))
- for k in users.groups]
- return sorted(choices, key=lambda g: g[0])
+from .components import UsersAndGroups
class ValidNewUsernameCheckMixin(object):
@@ -40,8 +33,8 @@ class ValidNewUsernameCheckMixin(object):
username = self.cleaned_data['username']
if self.instance.username != username and \
not self.is_valid_new_username():
- raise ValidationError(
- _('Username is taken or is reserved.'), code='invalid')
+ raise ValidationError(_('Username is taken or is reserved.'),
+ code='invalid')
return username
@@ -52,10 +45,8 @@ class ValidNewUsernameCheckMixin(object):
if username.lower() in existing_users:
return False
- for module_name, module in module_loader.loaded_modules.items():
- for reserved_username in getattr(module, 'reserved_usernames', []):
- if username.lower() == reserved_username.lower():
- return False
+ if UsersAndGroups.is_username_reserved(username.lower()):
+ return False
return True
@@ -88,10 +79,9 @@ class CreateUserForm(ValidNewUsernameCheckMixin,
"""
username = USERNAME_FIELD
groups = forms.MultipleChoiceField(
- choices=get_group_choices(), label=ugettext_lazy('Permissions'),
- required=False, widget=forms.CheckboxSelectMultiple,
- help_text=ugettext_lazy(
- 'Select which services should be available to the new '
+ choices=UsersAndGroups.get_group_choices,
+ label=ugettext_lazy('Permissions'), required=False,
+ widget=forms.CheckboxSelectMultiple, help_text=ugettext_lazy(
'user. The user will be able to log in to services that '
'support single sign-on through LDAP, if they are in the '
'appropriate group.
Users in the admin group '
@@ -109,7 +99,6 @@ class CreateUserForm(ValidNewUsernameCheckMixin,
"""Initialize the form with extra request argument."""
self.request = request
super(CreateUserForm, self).__init__(*args, **kwargs)
- self.fields['groups'].choices = get_group_choices()
self.fields['username'].widget.attrs.update({
'autofocus': 'autofocus',
'autocapitalize': 'none',
@@ -175,7 +164,7 @@ class UserUpdateForm(ValidNewUsernameCheckMixin,
def __init__(self, request, username, *args, **kwargs):
"""Initialize the form with extra request argument."""
- group_choices = dict(get_group_choices())
+ group_choices = dict(UsersAndGroups.get_group_choices())
for group in group_choices:
Group.objects.get_or_create(name=group)
@@ -313,9 +302,8 @@ class UserChangePasswordForm(SetPasswordForm):
"""Initialize the form with extra request argument."""
self.request = request
super(UserChangePasswordForm, self).__init__(*args, **kwargs)
- self.fields['new_password1'].widget.attrs.update({
- 'autofocus': 'autofocus'
- })
+ self.fields['new_password1'].widget.attrs.update(
+ {'autofocus': 'autofocus'})
def save(self, commit=True):
"""Save the user model and change LDAP password as well."""
@@ -366,7 +354,7 @@ class FirstBootForm(ValidNewUsernameCheckMixin, auth.forms.UserCreationForm):
_('Failed to add new user to admin group.'))
# Create initial Django groups
- for group_choice in get_group_choices():
+ for group_choice in UsersAndGroups.get_group_choices():
auth.models.Group.objects.get_or_create(name=group_choice[0])
admin_group = auth.models.Group.objects.get(name='admin')
diff --git a/plinth/modules/users/tests/test_components.py b/plinth/modules/users/tests/test_components.py
new file mode 100644
index 000000000..2ab2e0276
--- /dev/null
+++ b/plinth/modules/users/tests/test_components.py
@@ -0,0 +1,67 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+"""
+Tests for the UsersAndGroups app component.
+"""
+
+import pytest
+
+from ..components import UsersAndGroups
+
+
+@pytest.fixture(autouse=True)
+def fixture_empty_components():
+ """Remove all components from the global list before every test."""
+ UsersAndGroups._all_components = set()
+
+
+def test_create_users_and_groups_component():
+ """Test initialization of users and groups component."""
+ component = UsersAndGroups('simple-component')
+ assert component.groups == {}
+ assert component.reserved_usernames == []
+ assert len(component._all_components) == 1
+ assert component in component._all_components
+
+ groups = {'test-group1', 'Test description'}
+ component = UsersAndGroups('another-component', groups=groups,
+ reserved_usernames=['test-user1'])
+ assert component.groups == groups
+ assert component.reserved_usernames == ['test-user1']
+ assert len(component._all_components) == 2
+ assert component in component._all_components
+
+
+def test_get_groups():
+ """Test getting all the groups.
+
+ Test that:
+ 1. Group names are unique
+ 2. All components have the same global set of groups
+
+ """
+ UsersAndGroups('component-with-no-groups')
+ UsersAndGroups('component-with-one-group',
+ groups={'group1': 'description1'})
+ UsersAndGroups('component-with-groups', groups={
+ 'group1': 'description1',
+ 'group2': 'description2'
+ })
+
+ assert UsersAndGroups.get_groups() == {'group1', 'group2'}
+ assert UsersAndGroups.get_group_choices() == [
+ ('group1', 'description1 (group1)'),
+ ('group2', 'description2 (group2)')
+ ]
+
+
+def test_check_username_reservation():
+ """Test username reservations by multiple components."""
+ UsersAndGroups('complex-component',
+ reserved_usernames=['username1', 'username2'],
+ groups={'somegroup', 'some description'})
+ assert not UsersAndGroups.is_username_reserved('something')
+ assert UsersAndGroups.is_username_reserved('username1')
+
+ assert not UsersAndGroups.is_username_reserved('username3')
+ UsersAndGroups('temp-component', reserved_usernames=['username3'])
+ assert UsersAndGroups.is_username_reserved('username3')
diff --git a/plinth/modules/users/tests/test_group.py b/plinth/modules/users/tests/test_group.py
deleted file mode 100644
index 4d6cf141a..000000000
--- a/plinth/modules/users/tests/test_group.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# SPDX-License-Identifier: AGPL-3.0-or-later
-"""
-Test module to exercise group registration.
-
-It is recommended to run this module with root privileges in a virtual machine.
-"""
-
-from plinth.modules import users
-
-
-def test_register_group():
- """Test for multi addition of same group"""
- users.groups = dict() # reset groups
- group = ('TestGroup', 'Group for testing')
- users.register_group(group)
- users.register_group(group)
- assert len(users.groups) == 1
- return users.groups
diff --git a/plinth/modules/users/tests/test_views.py b/plinth/modules/users/tests/test_views.py
index b0e0401b2..bc59deb80 100644
--- a/plinth/modules/users/tests/test_views.py
+++ b/plinth/modules/users/tests/test_views.py
@@ -13,6 +13,8 @@ from django.core.exceptions import PermissionDenied
from plinth import module_loader
from plinth.modules.users import views
+from ..components import UsersAndGroups
+
# For all tests, plinth.urls instead of urls configured for testing, and
# django database
pytestmark = [pytest.mark.urls('plinth.urls'), pytest.mark.django_db]
@@ -43,15 +45,16 @@ def action_run(action, options, **kwargs):
@pytest.fixture(autouse=True)
def module_patch():
"""Patch users module."""
- loaded_modules = [('minetest',
- Mock(reserved_usernames=['Debian-minetest']))]
pwd_users = [Mock(pw_name='root'), Mock(pw_name='plinth')]
- with patch('pwd.getpwall', return_value=pwd_users),\
- patch('plinth.actions.superuser_run', side_effect=action_run),\
- patch('plinth.module_loader.loaded_modules.items',
- return_value=loaded_modules):
+ UsersAndGroups._all_components = set()
+ UsersAndGroups('test-users-and-groups',
+ groups={'admin': 'The admin group'})
+ UsersAndGroups('users-and-groups-minetest',
+ reserved_usernames=['debian-minetest'])
+ with patch('pwd.getpwall', return_value=pwd_users),\
+ patch('plinth.actions.superuser_run', side_effect=action_run):
yield