mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-21 07:55:00 +00:00
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:
parent
e6e67f7d53
commit
e04ae48637
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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"""
|
||||
|
||||
@ -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 = [
|
||||
'<a href="https://git-scm.com/docs/gittutorial">Git tutorial</a>.')
|
||||
]
|
||||
|
||||
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':
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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</a>.')
|
||||
]
|
||||
|
||||
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."""
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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</a>.')
|
||||
]
|
||||
|
||||
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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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</a> 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():
|
||||
|
||||
@ -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'])
|
||||
|
||||
56
plinth/modules/users/components.py
Normal file
56
plinth/modules/users/components.py
Normal 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))
|
||||
@ -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.<br /><br />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')
|
||||
|
||||
67
plinth/modules/users/tests/test_components.py
Normal file
67
plinth/modules/users/tests/test_components.py
Normal 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')
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user