users: Add component for managing users and groups

- Handle groups needed by an app.
- Handle reserved usernames for an app.
- Updated documentation
- Updated unit tests

Tests performed:
  - Reserved usernames: ez-ipupd, ejabberd, Debian-minetest, mldonkey,
    monkeysphere, mumble-server, privoxy, quasselcore, radicale, debian-tor,
    debian-transmission
  - Reserved usernames checks should work in the following forms:
    - Create user
    - Update user
    - First boot user creation
  - Full list of available groups should appear in following cases:
    - Create user form
    - Update user form
  - Full list of groups should get created in Django DB during:
    - Update user form display
    - First boot form save
  - When updating the last admin user, the 'admin' group choice is checked
    and disabled.
  - Following groups show up (sorted by group name):
    - bit-torrent: Download files using BitTorrent applications
    - git-access: Read-write access to Git repositories
    - i2p: Manage I2P application
    - wiki: View and edit wiki applications
    - minidlna: Media streaming server
    - ed2k: Download files using eDonkey applications
    - freedombox-share: Access to the private shares
    - web-search: Search the web
    - syncthing: Administer Syncthing application
    - feed-reader: Read and subscribe to news feeds
    - admin: Access to all services and system settings
  - Directory validation form checks for write permissions for following apps:
    - deluge with debian-deluged user
    - transmission with debian-transmission user
  - Sharing app should show all the groups in add/edit share forms
  - The following apps should get added to share group during setup:
    debian-transmission
    debian-deluged
  - Unit tests pass
  - Functional tests for users and groups pass
  - Test that an app (example syncthing) provides the necessary
    permissions to users in that group (but not in admin group).

Signed-off-by: Joseph Nuthalapati <njoseph@riseup.net>
[sunil: Fix i18n of group descriptions]
[sunil: Update developer documentation]
[sunil: Separate out cosmetic changes]
[sunil: Fix component ID for mumble]
[sunil: sharing: Remove unneeded dependency on users app]
[sunil: Implement better API for getting groups in component]
[sunil: Fix incorrect regression change ttrss app]
[sunil: Make iterating over gourps more readable]
[sunil: Improve tests, drop single use fixtures]
[sunil: Simplify test_view.py fixture]
Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Tested-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Joseph Nuthalapati 2020-03-20 17:16:46 +05:30 committed by James Valleroy
parent e6e67f7d53
commit e04ae48637
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
30 changed files with 344 additions and 166 deletions

View File

@ -163,11 +163,19 @@ with the FreedomBox framework in ``__init.py__``.
.. code-block:: python3 .. 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 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 group (and, of course, admin users) should be allowed to access our app. In the

View File

@ -11,7 +11,8 @@ from plinth import frontpage, menu
from plinth.daemon import Daemon from plinth.daemon import Daemon
from plinth.modules.apache.components import Webserver from plinth.modules.apache.components import Webserver
from plinth.modules.firewall.components import Firewall 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 from .manifest import backup, clients # noqa, pylint: disable=unused-import
@ -27,12 +28,10 @@ _description = [
'change it immediately after enabling this service.') 'change it immediately after enabling this service.')
] ]
group = ('bit-torrent', _('Download files using BitTorrent applications'))
reserved_usernames = ['debian-deluged']
app = None app = None
SYSTEM_USER = 'debian-deluged'
class DelugeApp(app_module.App): class DelugeApp(app_module.App):
"""FreedomBox app for Deluge.""" """FreedomBox app for Deluge."""
@ -42,6 +41,11 @@ class DelugeApp(app_module.App):
def __init__(self): def __init__(self):
"""Create components for the app.""" """Create components for the app."""
super().__init__() super().__init__()
groups = {
'bit-torrent': _('Download files using BitTorrent applications')
}
info = app_module.Info(app_id=self.app_id, version=version, info = app_module.Info(app_id=self.app_id, version=version,
name=_('Deluge'), icon_filename='deluge', name=_('Deluge'), icon_filename='deluge',
short_description=_('BitTorrent Web Client'), short_description=_('BitTorrent Web Client'),
@ -59,7 +63,7 @@ class DelugeApp(app_module.App):
url='/deluge', icon=info.icon_filename, url='/deluge', icon=info.icon_filename,
clients=info.clients, clients=info.clients,
login_required=True, login_required=True,
allowed_groups=[group[0]]) allowed_groups=list(groups))
self.add(shortcut) self.add(shortcut)
firewall = Firewall('firewall-deluge', info.name, firewall = Firewall('firewall-deluge', info.name,
@ -78,12 +82,16 @@ class DelugeApp(app_module.App):
listen_ports=[(8112, 'tcp4')]) listen_ports=[(8112, 'tcp4')])
self.add(daemon_web) 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(): def init():
"""Initialize the Deluge module.""" """Initialize the Deluge module."""
global app global app
app = DelugeApp() app = DelugeApp()
register_group(group)
setup_helper = globals()['setup_helper'] setup_helper = globals()['setup_helper']
if setup_helper.get_state() != 'needs-setup' and app.is_enabled(): 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.""" """Install and configure the module."""
helper.install(managed_packages) helper.install(managed_packages)
helper.call('post', actions.superuser_run, 'deluge', ['setup']) 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) helper.call('post', app.enable)

View File

@ -5,18 +5,18 @@ Forms for Deluge app.
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from plinth.modules.deluge import reserved_usernames
from plinth.modules.storage.forms import (DirectorySelectForm, from plinth.modules.storage.forms import (DirectorySelectForm,
DirectoryValidator) DirectoryValidator)
from . import SYSTEM_USER
class DelugeForm(DirectorySelectForm): class DelugeForm(DirectorySelectForm):
"""Deluge configuration form""" """Deluge configuration form"""
def __init__(self, *args, **kw): def __init__(self, *args, **kw):
validator = DirectoryValidator(username=reserved_usernames[0], validator = DirectoryValidator(username=SYSTEM_USER,
check_creatable=True) check_creatable=True)
super(DelugeForm, self).__init__( super(DelugeForm, self).__init__(title=_('Download directory'),
title=_('Download directory'), default='/var/lib/deluged/Downloads',
default='/var/lib/deluged/Downloads', validator=validator, *args, validator=validator, *args, **kw)
**kw)

View File

@ -11,6 +11,7 @@ from plinth import cfg, menu
from plinth.modules.names.components import DomainType from plinth.modules.names.components import DomainType
from plinth.signals import domain_added from plinth.signals import domain_added
from plinth.utils import format_lazy from plinth.utils import format_lazy
from plinth.modules.users.components import UsersAndGroups
from .manifest import backup # noqa, pylint: disable=unused-import 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.') 'name, they will get a response with your current IP address.')
] ]
reserved_usernames = ['ez-ipupd']
app = None app = None
@ -67,6 +66,10 @@ class DynamicDNSApp(app_module.App):
can_have_certificate=True) can_have_certificate=True)
self.add(domain_type) self.add(domain_type)
users_and_groups = UsersAndGroups('users-and-groups-dynamicdns',
reserved_usernames=['ez-ipupd'])
self.add(users_and_groups)
def init(): def init():
"""Initialize the module.""" """Initialize the module."""

View File

@ -21,6 +21,7 @@ from plinth.modules.letsencrypt.components import LetsEncrypt
from plinth.signals import (domain_added, post_hostname_change, from plinth.signals import (domain_added, post_hostname_change,
pre_hostname_change) pre_hostname_change)
from plinth.utils import format_lazy from plinth.utils import format_lazy
from plinth.modules.users.components import UsersAndGroups
from .manifest import backup, clients # noqa, pylint: disable=unused-import from .manifest import backup, clients # noqa, pylint: disable=unused-import
@ -45,8 +46,6 @@ _description = [
jsxc_url=reverse_lazy('jsxc:index')) jsxc_url=reverse_lazy('jsxc:index'))
] ]
reserved_usernames = ['ejabberd']
port_forwarding_info = [ port_forwarding_info = [
('TCP', 5222), ('TCP', 5222),
('TCP', 5269), ('TCP', 5269),
@ -110,6 +109,10 @@ class EjabberdApp(app_module.App):
(5269, 'tcp6'), (5443, 'tcp4'), (5443, 'tcp6')]) (5269, 'tcp6'), (5443, 'tcp4'), (5443, 'tcp6')])
self.add(daemon) self.add(daemon)
users_and_groups = UsersAndGroups('users-and-groups-ejabberd',
reserved_usernames=['ejabberd'])
self.add(users_and_groups)
def init(): def init():
"""Initialize the ejabberd module""" """Initialize the ejabberd module"""

View File

@ -14,7 +14,7 @@ from plinth import frontpage, menu
from plinth.errors import ActionError from plinth.errors import ActionError
from plinth.modules.apache.components import Webserver from plinth.modules.apache.components import Webserver
from plinth.modules.firewall.components import Firewall 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 .forms import is_repo_url
from .manifest import (GIT_REPO_PATH, # noqa, pylint: disable=unused-import from .manifest import (GIT_REPO_PATH, # noqa, pylint: disable=unused-import
@ -36,8 +36,6 @@ _description = [
'<a href="https://git-scm.com/docs/gittutorial">Git tutorial</a>.') '<a href="https://git-scm.com/docs/gittutorial">Git tutorial</a>.')
] ]
group = ('git-access', _('Read-write access to Git repositories'))
app = None app = None
@ -50,6 +48,8 @@ class GitwebApp(app_module.App):
"""Create components for the app.""" """Create components for the app."""
super().__init__() super().__init__()
groups = {'git-access': _('Read-write access to Git repositories')}
self.repos = [] self.repos = []
info = app_module.Info(app_id=self.app_id, version=version, 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/', icon=info.icon_filename, url='/gitweb/',
clients=info.clients, clients=info.clients,
login_required=True, login_required=True,
allowed_groups=[group[0]]) allowed_groups=list(groups))
self.add(shortcut) self.add(shortcut)
firewall = Firewall('firewall-gitweb', info.name, firewall = Firewall('firewall-gitweb', info.name,
@ -84,6 +84,10 @@ class GitwebApp(app_module.App):
'gitweb-freedombox-auth') 'gitweb-freedombox-auth')
self.add(self.auth_webserver) 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): def set_shortcut_login_required(self, login_required):
"""Change the login_required property of shortcut.""" """Change the login_required property of shortcut."""
shortcut = self.remove('shortcut-gitweb') shortcut = self.remove('shortcut-gitweb')
@ -163,7 +167,6 @@ def init():
"""Initialize the module.""" """Initialize the module."""
global app global app
app = GitwebApp() app = GitwebApp()
register_group(group)
setup_helper = globals()['setup_helper'] setup_helper = globals()['setup_helper']
if setup_helper.get_state() != 'needs-setup': if setup_helper.get_state() != 'needs-setup':

View File

@ -12,7 +12,7 @@ from plinth.daemon import Daemon
from plinth.modules.apache.components import Webserver from plinth.modules.apache.components import Webserver
from plinth.modules.firewall.components import Firewall from plinth.modules.firewall.components import Firewall
from plinth.modules.i2p.resources import FAVORITES 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 from .manifest import backup, clients # noqa, pylint: disable=unused-import
@ -35,8 +35,6 @@ _description = [
'configuration process.') 'configuration process.')
] ]
group = ('i2p', _('Manage I2P application'))
port_forwarding_info = [ port_forwarding_info = [
('TCP', 4444), ('TCP', 4444),
('TCP', 4445), ('TCP', 4445),
@ -60,6 +58,9 @@ class I2PApp(app_module.App):
def __init__(self): def __init__(self):
"""Create components for the app.""" """Create components for the app."""
super().__init__() super().__init__()
groups = {'i2p': _('Manage I2P application')}
info = app_module.Info(app_id=self.app_id, version=version, info = app_module.Info(app_id=self.app_id, version=version,
name=_('I2P'), icon_filename='i2p', name=_('I2P'), icon_filename='i2p',
short_description=_('Anonymity Network'), short_description=_('Anonymity Network'),
@ -77,7 +78,7 @@ class I2PApp(app_module.App):
icon=info.icon_filename, url='/i2p/', icon=info.icon_filename, url='/i2p/',
clients=info.clients, clients=info.clients,
login_required=True, login_required=True,
allowed_groups=[group[0]]) allowed_groups=list(groups))
self.add(shortcut) self.add(shortcut)
firewall = Firewall('firewall-i2p-web', info.name, firewall = Firewall('firewall-i2p-web', info.name,
@ -97,12 +98,15 @@ class I2PApp(app_module.App):
listen_ports=[(7657, 'tcp6')]) listen_ports=[(7657, 'tcp6')])
self.add(daemon) self.add(daemon)
users_and_groups = UsersAndGroups('users-and-groups-i2p',
groups=groups)
self.add(users_and_groups)
def init(): def init():
"""Initialize the module.""" """Initialize the module."""
global app global app
app = I2PApp() app = I2PApp()
register_group(group)
setup_helper = globals()['setup_helper'] setup_helper = globals()['setup_helper']
if setup_helper.get_state() != 'needs-setup' and app.is_enabled(): if setup_helper.get_state() != 'needs-setup' and app.is_enabled():

View File

@ -11,7 +11,7 @@ from plinth import app as app_module
from plinth import cfg, frontpage, menu from plinth import cfg, frontpage, menu
from plinth.modules.apache.components import Webserver from plinth.modules.apache.components import Webserver
from plinth.modules.firewall.components import Firewall 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 plinth.utils import format_lazy
from .manifest import backup, clients # noqa, pylint: disable=unused-import from .manifest import backup, clients # noqa, pylint: disable=unused-import
@ -36,8 +36,6 @@ _description = [
users_url=reverse_lazy('users:index')) users_url=reverse_lazy('users:index'))
] ]
group = ('wiki', _('View and edit wiki applications'))
app = None app = None
@ -49,6 +47,7 @@ class IkiwikiApp(app_module.App):
def __init__(self): def __init__(self):
"""Create components for the app.""" """Create components for the app."""
super().__init__() super().__init__()
info = app_module.Info(app_id=self.app_id, version=version, info = app_module.Info(app_id=self.app_id, version=version,
name=_('ikiwiki'), icon_filename='ikiwiki', name=_('ikiwiki'), icon_filename='ikiwiki',
short_description=_('Wiki and Blog'), short_description=_('Wiki and Blog'),
@ -71,6 +70,11 @@ class IkiwikiApp(app_module.App):
urls=['https://{host}/ikiwiki']) urls=['https://{host}/ikiwiki'])
self.add(webserver) 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): def add_shortcut(self, site, title):
"""Add an ikiwiki shortcut to frontpage.""" """Add an ikiwiki shortcut to frontpage."""
shortcut = frontpage.Shortcut('shortcut-ikiwiki-' + site, title, shortcut = frontpage.Shortcut('shortcut-ikiwiki-' + site, title,
@ -101,7 +105,6 @@ def init():
"""Initialize the ikiwiki module.""" """Initialize the ikiwiki module."""
global app global app
app = IkiwikiApp() app = IkiwikiApp()
register_group(group)
setup_helper = globals()['setup_helper'] setup_helper = globals()['setup_helper']
if setup_helper.get_state() != 'needs-setup' and app.is_enabled(): if setup_helper.get_state() != 'needs-setup' and app.is_enabled():

View File

@ -12,6 +12,7 @@ from plinth import cfg, frontpage, menu
from plinth.daemon import Daemon from plinth.daemon import Daemon
from plinth.modules.firewall.components import Firewall from plinth.modules.firewall.components import Firewall
from plinth.utils import format_lazy from plinth.utils import format_lazy
from plinth.modules.users.components import UsersAndGroups
from .manifest import backup, clients # noqa, pylint: disable=unused-import from .manifest import backup, clients # noqa, pylint: disable=unused-import
@ -43,8 +44,6 @@ _description = [
port_forwarding_info = [('UDP', 30000)] port_forwarding_info = [('UDP', 30000)]
reserved_usernames = ['Debian-minetest']
CONFIG_FILE = '/etc/minetest/minetest.conf' CONFIG_FILE = '/etc/minetest/minetest.conf'
AUG_PATH = '/files' + CONFIG_FILE + '/.anon' AUG_PATH = '/files' + CONFIG_FILE + '/.anon'
@ -87,6 +86,11 @@ class MinetestApp(app_module.App):
listen_ports=[(30000, 'udp4')]) listen_ports=[(30000, 'udp4')])
self.add(daemon) self.add(daemon)
users_and_groups = UsersAndGroups(
'users-and-groups-minetest',
reserved_usernames=['Debian-minetest'])
self.add(users_and_groups)
def init(): def init():
"""Initialize the module.""" """Initialize the module."""

View File

@ -9,7 +9,7 @@ from plinth import actions, frontpage, menu
from plinth.daemon import Daemon from plinth.daemon import Daemon
from plinth.modules.apache.components import Webserver from plinth.modules.apache.components import Webserver
from plinth.modules.firewall.components import Firewall 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 from .manifest import backup, clients # noqa
@ -30,8 +30,6 @@ _description = [
'such as PS3 and Xbox 360) or applications such as totem and Kodi.') 'such as PS3 and Xbox 360) or applications such as totem and Kodi.')
] ]
group = ('minidlna', _('Media streaming server'))
app = None app = None
@ -42,6 +40,9 @@ class MiniDLNAApp(app_module.App):
def __init__(self): def __init__(self):
"""Initialize the app components""" """Initialize the app components"""
super().__init__() super().__init__()
groups = {'minidlna': _('Media streaming server')}
info = app_module.Info(app_id=self.app_id, version=version, info = app_module.Info(app_id=self.app_id, version=version,
name='minidlna', icon_filename='minidlna', name='minidlna', icon_filename='minidlna',
short_description=_('Simple Media Server'), short_description=_('Simple Media Server'),
@ -60,16 +61,12 @@ class MiniDLNAApp(app_module.App):
is_external=False) is_external=False)
webserver = Webserver('webserver-minidlna', 'minidlna-freedombox', webserver = Webserver('webserver-minidlna', 'minidlna-freedombox',
urls=['http://localhost:8200/']) urls=['http://localhost:8200/'])
shortcut = frontpage.Shortcut( shortcut = frontpage.Shortcut('shortcut-minidlna', info.name,
'shortcut-minidlna', short_description=info.short_description,
info.name, description=info.description,
short_description=info.short_description, icon=info.icon_filename,
description=info.description, url='/_minidlna/', login_required=True,
icon=info.icon_filename, allowed_groups=list(groups))
url='/_minidlna/',
login_required=True,
allowed_groups=[group[0]],
)
daemon = Daemon('daemon-minidlna', managed_services[0]) daemon = Daemon('daemon-minidlna', managed_services[0])
self.add(menu_item) self.add(menu_item)
@ -78,12 +75,15 @@ class MiniDLNAApp(app_module.App):
self.add(shortcut) self.add(shortcut)
self.add(daemon) self.add(daemon)
users_and_groups = UsersAndGroups('users-and-groups-minidlna',
groups=groups)
self.add(users_and_groups)
def init(): def init():
"""Initialize the module.""" """Initialize the module."""
global app global app
app = MiniDLNAApp() app = MiniDLNAApp()
register_group(group)
setup_helper = globals()['setup_helper'] setup_helper = globals()['setup_helper']
if setup_helper.get_state() != 'needs-setup' and app.is_enabled(): if setup_helper.get_state() != 'needs-setup' and app.is_enabled():

View File

@ -11,7 +11,7 @@ from plinth import cfg, frontpage, menu
from plinth.daemon import Daemon from plinth.daemon import Daemon
from plinth.modules.apache.components import Webserver from plinth.modules.apache.components import Webserver
from plinth.modules.firewall.components import Firewall 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 plinth.utils import format_lazy
from .manifest import backup, clients # noqa, pylint: disable=unused-import from .manifest import backup, clients # noqa, pylint: disable=unused-import
@ -35,10 +35,6 @@ _description = [
'directory.'), box_name=cfg.box_name) 'directory.'), box_name=cfg.box_name)
] ]
reserved_usernames = ['mldonkey']
group = ('ed2k', _('Download files using eDonkey applications'))
app = None app = None
@ -50,6 +46,9 @@ class MLDonkeyApp(app_module.App):
def __init__(self): def __init__(self):
"""Create components for the app.""" """Create components for the app."""
super().__init__() super().__init__()
groups = {'ed2k': _('Download files using eDonkey applications')}
info = app_module.Info( info = app_module.Info(
app_id=self.app_id, version=version, name=_('MLDonkey'), app_id=self.app_id, version=version, name=_('MLDonkey'),
icon_filename='mldonkey', icon_filename='mldonkey',
@ -66,7 +65,7 @@ class MLDonkeyApp(app_module.App):
'shortcut-mldonkey', info.name, 'shortcut-mldonkey', info.name,
short_description=info.short_description, icon=info.icon_filename, short_description=info.short_description, icon=info.icon_filename,
url='/mldonkey/', login_required=True, clients=info.clients, url='/mldonkey/', login_required=True, clients=info.clients,
allowed_groups=[group[0]]) allowed_groups=list(groups))
self.add(shortcuts) self.add(shortcuts)
firewall = Firewall('firewall-mldonkey', info.name, firewall = Firewall('firewall-mldonkey', info.name,
@ -81,12 +80,16 @@ class MLDonkeyApp(app_module.App):
listen_ports=[(4080, 'tcp4')]) listen_ports=[(4080, 'tcp4')])
self.add(daemon) self.add(daemon)
users_and_groups = UsersAndGroups('users-and-groups-mldonkey',
reserved_usernames=['mldonkey'],
groups=groups)
self.add(users_and_groups)
def init(): def init():
"""Initialize the MLDonkey module.""" """Initialize the MLDonkey module."""
global app global app
app = MLDonkeyApp() app = MLDonkeyApp()
register_group(group)
setup_helper = globals()['setup_helper'] setup_helper = globals()['setup_helper']
if setup_helper.get_state() != 'needs-setup' and app.is_enabled(): if setup_helper.get_state() != 'needs-setup' and app.is_enabled():

View File

@ -7,6 +7,7 @@ from django.utils.translation import ugettext_lazy as _
from plinth import app as app_module from plinth import app as app_module
from plinth import menu from plinth import menu
from plinth.modules.users.components import UsersAndGroups
from .manifest import backup # noqa, pylint: disable=unused-import from .manifest import backup # noqa, pylint: disable=unused-import
@ -33,8 +34,6 @@ _description = [
'website</a>.') 'website</a>.')
] ]
reserved_usernames = ['monkeysphere']
app = None app = None
@ -57,6 +56,10 @@ class MonkeysphereApp(app_module.App):
advanced=True) advanced=True)
self.add(menu_item) self.add(menu_item)
users_and_groups = UsersAndGroups('users-and-groups-monkeysphere',
reserved_usernames=['monkeysphere'])
self.add(users_and_groups)
def init(): def init():
"""Initialize the monkeysphere module.""" """Initialize the monkeysphere module."""

View File

@ -10,6 +10,7 @@ from plinth import app as app_module
from plinth import frontpage, menu from plinth import frontpage, menu
from plinth.daemon import Daemon from plinth.daemon import Daemon
from plinth.modules.firewall.components import Firewall from plinth.modules.firewall.components import Firewall
from plinth.modules.users.components import UsersAndGroups
from .manifest import backup, clients # noqa, pylint: disable=unused-import from .manifest import backup, clients # noqa, pylint: disable=unused-import
@ -27,8 +28,6 @@ _description = [
'from your desktop and Android devices are available.') 'from your desktop and Android devices are available.')
] ]
reserved_usernames = ['mumble-server']
port_forwarding_info = [ port_forwarding_info = [
('TCP', 64738), ('TCP', 64738),
('UDP', 64738), ('UDP', 64738),
@ -73,6 +72,10 @@ class MumbleApp(app_module.App):
(64738, 'udp6')]) (64738, 'udp6')])
self.add(daemon) self.add(daemon)
users_and_groups = UsersAndGroups('users-and-groups-mumble',
reserved_usernames=['mumble-server'])
self.add(users_and_groups)
def init(): def init():
"""Initialize the Mumble module.""" """Initialize the Mumble module."""

View File

@ -13,6 +13,7 @@ from plinth.daemon import Daemon
from plinth.modules.apache.components import diagnose_url from plinth.modules.apache.components import diagnose_url
from plinth.modules.firewall.components import Firewall from plinth.modules.firewall.components import Firewall
from plinth.utils import format_lazy from plinth.utils import format_lazy
from plinth.modules.users.components import UsersAndGroups
from .manifest import backup # noqa, pylint: disable=unused-import from .manifest import backup # noqa, pylint: disable=unused-import
@ -39,8 +40,6 @@ _description = [
box_name=_(cfg.box_name)), box_name=_(cfg.box_name)),
] ]
reserved_usernames = ['privoxy']
app = None app = None
@ -78,6 +77,10 @@ class PrivoxyApp(app_module.App):
listen_ports=[(8118, 'tcp4'), (8118, 'tcp6')]) listen_ports=[(8118, 'tcp4'), (8118, 'tcp6')])
self.add(daemon) self.add(daemon)
users_and_groups = UsersAndGroups('users-and-groups-privoxy',
reserved_usernames=['privoxy'])
self.add(users_and_groups)
def diagnose(self): def diagnose(self):
"""Run diagnostics and return the results.""" """Run diagnostics and return the results."""
results = super().diagnose() results = super().diagnose()

View File

@ -16,6 +16,7 @@ from plinth.modules import names
from plinth.modules.firewall.components import Firewall from plinth.modules.firewall.components import Firewall
from plinth.modules.letsencrypt.components import LetsEncrypt from plinth.modules.letsencrypt.components import LetsEncrypt
from plinth.utils import format_lazy from plinth.utils import format_lazy
from plinth.modules.users.components import UsersAndGroups
from .manifest import backup, clients # noqa, pylint: disable=unused-import from .manifest import backup, clients # noqa, pylint: disable=unused-import
@ -43,8 +44,6 @@ _description = [
'are available.'), 'are available.'),
] ]
reserved_usernames = ['quasselcore']
port_forwarding_info = [('TCP', 4242)] port_forwarding_info = [('TCP', 4242)]
app = None app = None
@ -95,6 +94,10 @@ class QuasselApp(app_module.App):
listen_ports=[(4242, 'tcp4'), (4242, 'tcp6')]) listen_ports=[(4242, 'tcp4'), (4242, 'tcp6')])
self.add(daemon) self.add(daemon)
users_and_groups = UsersAndGroups('users-and-groups-quasselcore',
reserved_usernames=['quasselcore'])
self.add(users_and_groups)
def init(): def init():
"""Initialize the quassel module.""" """Initialize the quassel module."""

View File

@ -17,6 +17,7 @@ from plinth import cfg, frontpage, menu
from plinth.daemon import Daemon from plinth.daemon import Daemon
from plinth.modules.apache.components import Uwsgi, Webserver from plinth.modules.apache.components import Uwsgi, Webserver
from plinth.modules.firewall.components import Firewall from plinth.modules.firewall.components import Firewall
from plinth.modules.users.components import UsersAndGroups
from plinth.utils import format_lazy, Version from plinth.utils import format_lazy, Version
from .manifest import backup, clients # noqa, pylint: disable=unused-import from .manifest import backup, clients # noqa, pylint: disable=unused-import
@ -39,8 +40,6 @@ _description = [
'contacts, which must be done using a separate client.'), 'contacts, which must be done using a separate client.'),
] ]
reserved_usernames = ['radicale']
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
CONFIG_FILE = '/etc/radicale/config' CONFIG_FILE = '/etc/radicale/config'
@ -91,6 +90,10 @@ class RadicaleApp(app_module.App):
daemon = RadicaleDaemon('daemon-radicale', managed_services[0]) daemon = RadicaleDaemon('daemon-radicale', managed_services[0])
self.add(daemon) self.add(daemon)
users_and_groups = UsersAndGroups('users-and-groups-radicale',
reserved_usernames=['radicale'])
self.add(users_and_groups)
class RadicaleWebserver(Webserver): class RadicaleWebserver(Webserver):
"""Webserver enable/disable behavior specific for radicale.""" """Webserver enable/disable behavior specific for radicale."""

View File

@ -17,7 +17,7 @@ from plinth import app as app_module
from plinth import frontpage, menu from plinth import frontpage, menu
from plinth.daemon import Daemon from plinth.daemon import Daemon
from plinth.modules.firewall.components import Firewall 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 plinth.utils import format_lazy
from .manifest import backup, clients # noqa, pylint: disable=unused-import from .manifest import backup, clients # noqa, pylint: disable=unused-import
@ -44,8 +44,6 @@ _description = [
'own private space.'), 'own private space.'),
] ]
group = ('freedombox-share', _('Access to the private shares'))
app = None app = None
@ -57,6 +55,9 @@ class SambaApp(app_module.App):
def __init__(self): def __init__(self):
"""Create components for the app.""" """Create components for the app."""
super().__init__() super().__init__()
groups = {'freedombox-share': _('Access to the private shares')}
info = app_module.Info(app_id=self.app_id, version=version, info = app_module.Info(app_id=self.app_id, version=version,
name=_('Samba'), icon_filename='samba', name=_('Samba'), icon_filename='samba',
short_description=_('File Sharing'), short_description=_('File Sharing'),
@ -74,7 +75,7 @@ class SambaApp(app_module.App):
short_description=info.short_description, icon=info.icon_filename, short_description=info.short_description, icon=info.icon_filename,
description=info.description, description=info.description,
configure_url=reverse_lazy('samba:index'), clients=info.clients, 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) self.add(shortcut)
firewall = Firewall('firewall-samba', info.name, ports=['samba']) firewall = Firewall('firewall-samba', info.name, ports=['samba'])
@ -92,12 +93,15 @@ class SambaApp(app_module.App):
self.add(daemon_nmbd) self.add(daemon_nmbd)
users_and_groups = UsersAndGroups('users-and-groups-samba',
groups=groups)
self.add(users_and_groups)
def init(): def init():
"""Initialize the module.""" """Initialize the module."""
global app global app
app = SambaApp() app = SambaApp()
register_group(group)
setup_helper = globals()['setup_helper'] setup_helper = globals()['setup_helper']
if setup_helper.get_state() != 'needs-setup' and app.is_enabled(): if setup_helper.get_state() != 'needs-setup' and app.is_enabled():

View File

@ -12,7 +12,7 @@ from plinth import app as app_module
from plinth import frontpage, menu from plinth import frontpage, menu
from plinth.modules.apache.components import Uwsgi, Webserver from plinth.modules.apache.components import Uwsgi, Webserver
from plinth.modules.firewall.components import Firewall 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 from .manifest import (PUBLIC_ACCESS_SETTING_FILE, # noqa, pylint: disable=unused-import
backup, clients) backup, clients)
@ -28,8 +28,6 @@ _description = [
'It stores no cookies by default.') 'It stores no cookies by default.')
] ]
group = ('web-search', _('Search the web'))
manual_page = 'Searx' manual_page = 'Searx'
app = None app = None
@ -43,6 +41,9 @@ class SearxApp(app_module.App):
def __init__(self): def __init__(self):
"""Create components for the app.""" """Create components for the app."""
super().__init__() super().__init__()
groups = {'web-search': _('Search the web')}
info = app_module.Info(app_id=self.app_id, version=version, info = app_module.Info(app_id=self.app_id, version=version,
name=_('Searx'), icon_filename='searx', name=_('Searx'), icon_filename='searx',
short_description=_('Web Search'), short_description=_('Web Search'),
@ -60,7 +61,7 @@ class SearxApp(app_module.App):
short_description=info.short_description, icon=info.icon_filename, short_description=info.short_description, icon=info.icon_filename,
url='/searx/', clients=info.clients, url='/searx/', clients=info.clients,
login_required=(not is_public_access_enabled()), login_required=(not is_public_access_enabled()),
allowed_groups=[group[0]]) allowed_groups=list(groups))
self.add(shortcut) self.add(shortcut)
firewall = Firewall('firewall-searx', info.name, firewall = Firewall('firewall-searx', info.name,
@ -78,6 +79,10 @@ class SearxApp(app_module.App):
uwsgi = Uwsgi('uwsgi-searx', 'searx') uwsgi = Uwsgi('uwsgi-searx', 'searx')
self.add(uwsgi) 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): def set_shortcut_login_required(self, login_required):
"""Change the login_required property of shortcut.""" """Change the login_required property of shortcut."""
shortcut = self.remove('shortcut-searx') shortcut = self.remove('shortcut-searx')
@ -101,7 +106,6 @@ def init():
"""Initialize the module.""" """Initialize the module."""
global app global app
app = SearxApp() app = SearxApp()
register_group(group)
setup_helper = globals()['setup_helper'] setup_helper = globals()['setup_helper']
if setup_helper.get_state() != 'needs-setup' and app.is_enabled(): if setup_helper.get_state() != 'needs-setup' and app.is_enabled():

View File

@ -8,7 +8,7 @@ from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from plinth.modules import sharing 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): class AddShareForm(forms.Form):
@ -29,6 +29,7 @@ class AddShareForm(forms.Form):
'Make files in this folder available to anyone with the link.')) 'Make files in this folder available to anyone with the link.'))
groups = forms.MultipleChoiceField( groups = forms.MultipleChoiceField(
choices=UsersAndGroups.get_group_choices,
widget=forms.CheckboxSelectMultiple, required=False, widget=forms.CheckboxSelectMultiple, required=False,
label=_('User groups that can read the files in the share'), label=_('User groups that can read the files in the share'),
help_text=_( help_text=_(
@ -38,7 +39,6 @@ class AddShareForm(forms.Form):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Initialize the form with extra request argument.""" """Initialize the form with extra request argument."""
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['groups'].choices = get_group_choices()
self.fields['name'].widget.attrs.update({'autofocus': 'autofocus'}) self.fields['name'].widget.attrs.update({'autofocus': 'autofocus'})
def clean_name(self): def clean_name(self):

View File

@ -11,7 +11,7 @@ from plinth import cfg, frontpage, menu
from plinth.daemon import Daemon from plinth.daemon import Daemon
from plinth.modules.apache.components import Webserver from plinth.modules.apache.components import Webserver
from plinth.modules.firewall.components import Firewall 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 plinth.utils import format_lazy
from .manifest import backup, clients # noqa, pylint: disable=unused-import 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)), 'users belonging to the "admin" group.'), box_name=_(cfg.box_name)),
] ]
group = ('syncthing', _('Administer Syncthing application'))
app = None app = None
@ -50,6 +48,9 @@ class SyncthingApp(app_module.App):
def __init__(self): def __init__(self):
"""Create components for the app.""" """Create components for the app."""
super().__init__() super().__init__()
self.groups = {'syncthing': _('Administer Syncthing application')}
info = app_module.Info(app_id=self.app_id, version=version, info = app_module.Info(app_id=self.app_id, version=version,
name=_('Syncthing'), icon_filename='syncthing', name=_('Syncthing'), icon_filename='syncthing',
short_description=_('File Synchronization'), short_description=_('File Synchronization'),
@ -67,7 +68,7 @@ class SyncthingApp(app_module.App):
icon=info.icon_filename, icon=info.icon_filename,
url='/syncthing/', clients=info.clients, url='/syncthing/', clients=info.clients,
login_required=True, login_required=True,
allowed_groups=[group[0]]) allowed_groups=list(self.groups))
self.add(shortcut) self.add(shortcut)
firewall = Firewall('firewall-syncthing-web', info.name, firewall = Firewall('firewall-syncthing-web', info.name,
@ -85,12 +86,15 @@ class SyncthingApp(app_module.App):
daemon = Daemon('daemon-syncthing', managed_services[0]) daemon = Daemon('daemon-syncthing', managed_services[0])
self.add(daemon) self.add(daemon)
users_and_groups = UsersAndGroups('users-and-groups-syncthing',
groups=self.groups)
self.add(users_and_groups)
def init(): def init():
"""Initialize the module.""" """Initialize the module."""
global app global app
app = SyncthingApp() app = SyncthingApp()
register_group(group)
setup_helper = globals()['setup_helper'] setup_helper = globals()['setup_helper']
if setup_helper.get_state() != 'needs-setup' and app.is_enabled(): if setup_helper.get_state() != 'needs-setup' and app.is_enabled():

View File

@ -15,6 +15,7 @@ from plinth.modules.apache.components import diagnose_url
from plinth.modules.firewall.components import Firewall from plinth.modules.firewall.components import Firewall
from plinth.modules.names.components import DomainType from plinth.modules.names.components import DomainType
from plinth.signals import domain_added, domain_removed from plinth.signals import domain_added, domain_removed
from plinth.modules.users.components import UsersAndGroups
from . import utils from . import utils
from .manifest import backup, clients # noqa, pylint: disable=unused-import from .manifest import backup, clients # noqa, pylint: disable=unused-import
@ -38,8 +39,6 @@ _description = [
'Tor Browser</a>.') 'Tor Browser</a>.')
] ]
reserved_usernames = ['debian-tor']
app = None app = None
@ -82,6 +81,10 @@ class TorApp(app_module.App):
(9040, 'tcp6'), (9053, 'udp4'), (9053, 'udp6')]) (9040, 'tcp6'), (9053, 'udp4'), (9053, 'udp6')])
self.add(daemon) self.add(daemon)
users_and_groups = UsersAndGroups('users-and-groups-tor',
reserved_usernames=['debian-tor'])
self.add(users_and_groups)
def diagnose(self): def diagnose(self):
"""Run diagnostics and return the results.""" """Run diagnostics and return the results."""
results = super().diagnose() results = super().diagnose()

View File

@ -13,7 +13,8 @@ from plinth import frontpage, menu
from plinth.daemon import Daemon from plinth.daemon import Daemon
from plinth.modules.apache.components import Webserver from plinth.modules.apache.components import Webserver
from plinth.modules.firewall.components import Firewall 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 from .manifest import backup, clients # noqa, pylint: disable=unused-import
@ -29,12 +30,10 @@ _description = [
'BitTorrent is not anonymous.'), 'BitTorrent is not anonymous.'),
] ]
reserved_usernames = ['debian-transmission']
group = ('bit-torrent', _('Download files using BitTorrent applications'))
app = None app = None
SYSTEM_USER = 'debian-transmission'
class TransmissionApp(app_module.App): class TransmissionApp(app_module.App):
"""FreedomBox app for Transmission.""" """FreedomBox app for Transmission."""
@ -44,6 +43,10 @@ class TransmissionApp(app_module.App):
def __init__(self): def __init__(self):
"""Create components for the app.""" """Create components for the app."""
super().__init__() super().__init__()
groups = {
'bit-torrent': _('Download files using BitTorrent applications')
}
info = app_module.Info(app_id=self.app_id, version=version, info = app_module.Info(app_id=self.app_id, version=version,
name=_('Transmission'), name=_('Transmission'),
icon_filename='transmission', icon_filename='transmission',
@ -61,7 +64,7 @@ class TransmissionApp(app_module.App):
'shortcut-transmission', info.name, 'shortcut-transmission', info.name,
short_description=info.short_description, icon=info.icon_filename, short_description=info.short_description, icon=info.icon_filename,
url='/transmission', clients=info.clients, login_required=True, url='/transmission', clients=info.clients, login_required=True,
allowed_groups=[group[0]]) allowed_groups=list(groups))
self.add(shortcut) self.add(shortcut)
firewall = Firewall('firewall-transmission', info.name, firewall = Firewall('firewall-transmission', info.name,
@ -76,12 +79,16 @@ class TransmissionApp(app_module.App):
listen_ports=[(9091, 'tcp4')]) listen_ports=[(9091, 'tcp4')])
self.add(daemon) self.add(daemon)
users_and_groups = UsersAndGroups('users-and-groups-transmission',
reserved_usernames=[SYSTEM_USER],
groups=groups)
self.add(users_and_groups)
def init(): def init():
"""Initialize the Transmission module.""" """Initialize the Transmission module."""
global app global app
app = TransmissionApp() app = TransmissionApp()
register_group(group)
setup_helper = globals()['setup_helper'] setup_helper = globals()['setup_helper']
if setup_helper.get_state() != 'needs-setup' and app.is_enabled(): 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', helper.call('post', actions.superuser_run, 'transmission',
['merge-configuration'], ['merge-configuration'],
input=json.dumps(new_configuration).encode()) 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) helper.call('post', app.enable)

View File

@ -5,18 +5,19 @@ FreedomBox app for configuring Transmission.
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from plinth.modules.transmission import reserved_usernames
from plinth.modules.storage.forms import (DirectorySelectForm, from plinth.modules.storage.forms import (DirectorySelectForm,
DirectoryValidator) DirectoryValidator)
from . import SYSTEM_USER
class TransmissionForm(DirectorySelectForm): class TransmissionForm(DirectorySelectForm):
"""Transmission configuration form""" """Transmission configuration form"""
def __init__(self, *args, **kw): def __init__(self, *args, **kw):
validator = DirectoryValidator( validator = DirectoryValidator(username=SYSTEM_USER,
username=reserved_usernames[0], check_creatable=True) check_creatable=True)
super(TransmissionForm, self).__init__( super(TransmissionForm,
title=_('Download directory'), self).__init__(title=_('Download directory'),
default='/var/lib/transmission-daemon/downloads', default='/var/lib/transmission-daemon/downloads',
validator=validator, *args, **kw) validator=validator, *args, **kw)

View File

@ -12,7 +12,7 @@ from plinth import cfg, frontpage, menu
from plinth.daemon import Daemon from plinth.daemon import Daemon
from plinth.modules.apache.components import Webserver from plinth.modules.apache.components import Webserver
from plinth.modules.firewall.components import Firewall 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 plinth.utils import Version, format_lazy
from .manifest import backup, clients # noqa, pylint: disable=unused-import from .manifest import backup, clients # noqa, pylint: disable=unused-import
@ -39,8 +39,6 @@ _description = [
'/tt-rss-app</a> for connecting.')) '/tt-rss-app</a> for connecting.'))
] ]
group = ('feed-reader', _('Read and subscribe to news feeds'))
app = None app = None
@ -52,6 +50,9 @@ class TTRSSApp(app_module.App):
def __init__(self): def __init__(self):
"""Create components for the app.""" """Create components for the app."""
super().__init__() super().__init__()
groups = {'feed-reader': _('Read and subscribe to news feeds')}
info = app_module.Info(app_id=self.app_id, version=version, info = app_module.Info(app_id=self.app_id, version=version,
name=_('Tiny Tiny RSS'), icon_filename='ttrss', name=_('Tiny Tiny RSS'), icon_filename='ttrss',
short_description=_('News Feed Reader'), short_description=_('News Feed Reader'),
@ -69,7 +70,7 @@ class TTRSSApp(app_module.App):
icon=info.icon_filename, url='/tt-rss', icon=info.icon_filename, url='/tt-rss',
clients=info.clients, clients=info.clients,
login_required=True, login_required=True,
allowed_groups=[group[0]]) allowed_groups=list(groups))
self.add(shortcut) self.add(shortcut)
firewall = Firewall('firewall-ttrss', info.name, firewall = Firewall('firewall-ttrss', info.name,
@ -83,6 +84,10 @@ class TTRSSApp(app_module.App):
daemon = Daemon('daemon-ttrss', managed_services[0]) daemon = Daemon('daemon-ttrss', managed_services[0])
self.add(daemon) self.add(daemon)
users_and_groups = UsersAndGroups('users-and-groups-ttrss',
groups=groups)
self.add(users_and_groups)
def enable(self): def enable(self):
"""Enable components and API access.""" """Enable components and API access."""
super().enable() super().enable()
@ -93,7 +98,6 @@ def init():
"""Initialize the module.""" """Initialize the module."""
global app global app
app = TTRSSApp() app = TTRSSApp()
register_group(group)
setup_helper = globals()['setup_helper'] setup_helper = globals()['setup_helper']
if setup_helper.get_state() != 'needs-setup' and app.is_enabled(): if setup_helper.get_state() != 'needs-setup' and app.is_enabled():

View File

@ -14,6 +14,8 @@ from plinth import cfg, menu
from plinth.daemon import Daemon from plinth.daemon import Daemon
from plinth.utils import format_lazy from plinth.utils import format_lazy
from .components import UsersAndGroups
version = 3 version = 3
is_essential = True is_essential = True
@ -45,9 +47,6 @@ _description = [
box_name=_(cfg.box_name)) box_name=_(cfg.box_name))
] ]
# All FreedomBox user groups
groups = dict()
app = None app = None
@ -75,6 +74,12 @@ class UsersApp(app_module.App):
listen_ports=[(389, 'tcp4'), (389, 'tcp6')]) listen_ports=[(389, 'tcp4'), (389, 'tcp6')])
self.add(daemon) 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): def diagnose(self):
"""Run diagnostics and return the results.""" """Run diagnostics and return the results."""
results = super().diagnose() results = super().diagnose()
@ -129,10 +134,6 @@ def remove_group(group):
actions.superuser_run('users', options=['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(): def get_last_admin_user():
"""If there is only one admin user return its name else return None.""" """If there is only one admin user return its name else return None."""
output = actions.superuser_run('users', ['get-group-users', 'admin']) output = actions.superuser_run('users', ['get-group-users', 'admin'])

View File

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

View File

@ -3,7 +3,6 @@
import pwd import pwd
import re import re
import plinth.forms
from django import forms from django import forms
from django.contrib import auth, messages from django.contrib import auth, messages
from django.contrib.auth.forms import SetPasswordForm, UserCreationForm 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.deconstruct import deconstructible
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy 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.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.modules.security import set_restricted_access
from plinth.translation import set_language from plinth.translation import set_language
from plinth.utils import is_user_admin from plinth.utils import is_user_admin
from . import get_last_admin_user from . import get_last_admin_user
from .components import UsersAndGroups
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])
class ValidNewUsernameCheckMixin(object): class ValidNewUsernameCheckMixin(object):
@ -40,8 +33,8 @@ class ValidNewUsernameCheckMixin(object):
username = self.cleaned_data['username'] username = self.cleaned_data['username']
if self.instance.username != username and \ if self.instance.username != username and \
not self.is_valid_new_username(): not self.is_valid_new_username():
raise ValidationError( raise ValidationError(_('Username is taken or is reserved.'),
_('Username is taken or is reserved.'), code='invalid') code='invalid')
return username return username
@ -52,10 +45,8 @@ class ValidNewUsernameCheckMixin(object):
if username.lower() in existing_users: if username.lower() in existing_users:
return False return False
for module_name, module in module_loader.loaded_modules.items(): if UsersAndGroups.is_username_reserved(username.lower()):
for reserved_username in getattr(module, 'reserved_usernames', []): return False
if username.lower() == reserved_username.lower():
return False
return True return True
@ -88,10 +79,9 @@ class CreateUserForm(ValidNewUsernameCheckMixin,
""" """
username = USERNAME_FIELD username = USERNAME_FIELD
groups = forms.MultipleChoiceField( groups = forms.MultipleChoiceField(
choices=get_group_choices(), label=ugettext_lazy('Permissions'), choices=UsersAndGroups.get_group_choices,
required=False, widget=forms.CheckboxSelectMultiple, label=ugettext_lazy('Permissions'), required=False,
help_text=ugettext_lazy( widget=forms.CheckboxSelectMultiple, help_text=ugettext_lazy(
'Select which services should be available to the new '
'user. The user will be able to log in to services that ' 'user. The user will be able to log in to services that '
'support single sign-on through LDAP, if they are in the ' 'support single sign-on through LDAP, if they are in the '
'appropriate group.<br /><br />Users in the admin group ' 'appropriate group.<br /><br />Users in the admin group '
@ -109,7 +99,6 @@ class CreateUserForm(ValidNewUsernameCheckMixin,
"""Initialize the form with extra request argument.""" """Initialize the form with extra request argument."""
self.request = request self.request = request
super(CreateUserForm, self).__init__(*args, **kwargs) super(CreateUserForm, self).__init__(*args, **kwargs)
self.fields['groups'].choices = get_group_choices()
self.fields['username'].widget.attrs.update({ self.fields['username'].widget.attrs.update({
'autofocus': 'autofocus', 'autofocus': 'autofocus',
'autocapitalize': 'none', 'autocapitalize': 'none',
@ -175,7 +164,7 @@ class UserUpdateForm(ValidNewUsernameCheckMixin,
def __init__(self, request, username, *args, **kwargs): def __init__(self, request, username, *args, **kwargs):
"""Initialize the form with extra request argument.""" """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: for group in group_choices:
Group.objects.get_or_create(name=group) Group.objects.get_or_create(name=group)
@ -313,9 +302,8 @@ class UserChangePasswordForm(SetPasswordForm):
"""Initialize the form with extra request argument.""" """Initialize the form with extra request argument."""
self.request = request self.request = request
super(UserChangePasswordForm, self).__init__(*args, **kwargs) super(UserChangePasswordForm, self).__init__(*args, **kwargs)
self.fields['new_password1'].widget.attrs.update({ self.fields['new_password1'].widget.attrs.update(
'autofocus': 'autofocus' {'autofocus': 'autofocus'})
})
def save(self, commit=True): def save(self, commit=True):
"""Save the user model and change LDAP password as well.""" """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.')) _('Failed to add new user to admin group.'))
# Create initial Django groups # 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]) auth.models.Group.objects.get_or_create(name=group_choice[0])
admin_group = auth.models.Group.objects.get(name='admin') admin_group = auth.models.Group.objects.get(name='admin')

View File

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

View File

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

View File

@ -13,6 +13,8 @@ from django.core.exceptions import PermissionDenied
from plinth import module_loader from plinth import module_loader
from plinth.modules.users import views from plinth.modules.users import views
from ..components import UsersAndGroups
# For all tests, plinth.urls instead of urls configured for testing, and # For all tests, plinth.urls instead of urls configured for testing, and
# django database # django database
pytestmark = [pytest.mark.urls('plinth.urls'), pytest.mark.django_db] pytestmark = [pytest.mark.urls('plinth.urls'), pytest.mark.django_db]
@ -43,15 +45,16 @@ def action_run(action, options, **kwargs):
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def module_patch(): def module_patch():
"""Patch users module.""" """Patch users module."""
loaded_modules = [('minetest',
Mock(reserved_usernames=['Debian-minetest']))]
pwd_users = [Mock(pw_name='root'), Mock(pw_name='plinth')] pwd_users = [Mock(pw_name='root'), Mock(pw_name='plinth')]
with patch('pwd.getpwall', return_value=pwd_users),\ UsersAndGroups._all_components = set()
patch('plinth.actions.superuser_run', side_effect=action_run),\ UsersAndGroups('test-users-and-groups',
patch('plinth.module_loader.loaded_modules.items', groups={'admin': 'The admin group'})
return_value=loaded_modules): 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 yield