diff --git a/actions/samba b/actions/samba
index 5545fa52e..ffe065afe 100755
--- a/actions/samba
+++ b/actions/samba
@@ -51,11 +51,11 @@ CONF = r'''
logging = file
panic action = /usr/share/samba/panic-action %d
server role = standalone server
- obey pam restrictions = yes
- unix password sync = yes
- passwd program = /usr/bin/passwd %u
- passwd chat = *Enter\snew\s*\spassword:* %n\n *Retype\snew\s*\spassword:* %n\n *password\supdated\ssuccessfully* .
- pam password change = yes
+ #obey pam restrictions = yes
+ #unix password sync = yes
+ #passwd program = /usr/bin/passwd %u
+ #passwd chat = *Enter\snew\s*\spassword:* %n\n *Retype\snew\s*\spassword:* %n\n *password\supdated\ssuccessfully* .
+ #pam password change = yes
map to guest = bad user
# connection inactivity timeout in minutes
deadtime = 5
@@ -73,9 +73,13 @@ def parse_arguments():
subparsers.add_parser('get-shares', help='Get configured samba shares')
+ subparsers.add_parser('get-users', help='Get users from Samba database')
+
subparser = subparsers.add_parser('add-share', help='Add new samba share')
subparser.add_argument('--mount-point', help='Path of the mount point',
required=True)
+ subparser.add_argument('--share-type', help='Type of the share',
+ required=True, choices=['open', 'group', 'home'])
subparser.add_argument('--windows-filesystem', required=False,
default=False, action='store_true',
help='Path is Windows filesystem')
@@ -84,6 +88,8 @@ def parse_arguments():
'delete-share', help='Delete a samba share configuration')
subparser.add_argument('--mount-point', help='Path of the mount point',
required=True)
+ subparser.add_argument('--share-type', help='Type of the share',
+ required=True, choices=['open', 'group', 'home'])
subparsers.add_parser('dump-shares',
help='Dump share configuration to file')
@@ -104,20 +110,38 @@ def _conf_command(parameters, **kwargs):
subprocess.check_call(['net', 'conf'] + parameters, **kwargs)
-def _create_share(mount_point, windows_filesystem=False):
- """Create a samba share."""
+def _create_share(mount_point, share_type, windows_filesystem=False):
+ """Create samba public, group and private shares."""
+ if share_type == 'open':
+ subdir = 'open_share'
+ elif share_type == 'group':
+ subdir = 'group_share'
+ elif share_type == 'home':
+ subdir = 'homes'
+
shares_path = _get_shares_path(mount_point)
- open_share_path = os.path.join(mount_point, shares_path, 'open_share')
- os.makedirs(open_share_path, exist_ok=True)
+ share_path = os.path.join(mount_point, shares_path, subdir)
+ os.makedirs(share_path, exist_ok=True)
_make_mounts_readable_by_others(mount_point)
# FAT and NTFS partitions don't support setting permissions
if not windows_filesystem:
- _set_open_share_permissions(open_share_path)
+ if share_type in ['open', 'group']:
+ _set_share_permissions(share_path)
+ else:
+ shutil.chown(share_path, group='users')
+ os.chmod(share_path, 0o0775)
share_name = _create_share_name(mount_point)
- _define_open_share(share_name, open_share_path, windows_filesystem)
+
+ if share_type == 'open':
+ _define_open_share(share_name, share_path, windows_filesystem)
+ elif share_type == 'group':
+ _define_group_share(share_name + '_group', share_path,
+ windows_filesystem)
+ elif share_type == 'home':
+ _define_homes_share(share_name + '_home', share_path)
def _create_share_name(mount_point):
@@ -141,17 +165,40 @@ def _define_open_share(name, path, windows_filesystem=False):
_conf_command(['setparm', name, 'inherit permissions', 'yes'])
+def _define_group_share(name, path, windows_filesystem=False):
+ """Define a group samba share."""
+ try:
+ _conf_command(['delshare', name], stderr=subprocess.DEVNULL)
+ except subprocess.CalledProcessError:
+ pass
+ _conf_command(['addshare', name, path, 'writeable=y', 'guest_ok=n'])
+ _conf_command(['setparm', name, 'valid users', '@freedombox-share @admin'])
+ if not windows_filesystem:
+ _conf_command(['setparm', name, 'force group', 'freedombox-share'])
+ _conf_command(['setparm', name, 'inherit permissions', 'yes'])
+
+
+def _define_homes_share(name, path):
+ """Define a samba share for private homes."""
+ try:
+ _conf_command(['delshare', name], stderr=subprocess.DEVNULL)
+ except subprocess.CalledProcessError:
+ pass
+ userpath = os.path.join(path, '%u')
+ _conf_command(['addshare', name, userpath, 'writeable=y', 'guest_ok=n'])
+ _conf_command(['setparm', name, 'valid users', '@freedombox-share @admin'])
+ _conf_command(
+ ['setparm', name, 'preexec', 'mkdir -p -m 755 {}'.format(userpath)])
+
+
def _get_mount_point(path):
"""Get the mount point where the share is."""
subpath = 'FreedomBox/shares/'
if '/var/lib/freedombox/shares/' in path:
- try:
- # test whether var directory is a mount point
- _validate_mount_point(path.split('lib/freedombox/shares/')[0])
- except RuntimeError:
- subpath = 'var/lib/freedombox/shares/'
- else:
+ if os.path.ismount(path.split('lib/freedombox/shares/')[0]):
subpath = 'lib/freedombox/shares/'
+ else:
+ subpath = 'var/lib/freedombox/shares/'
return path.split(subpath)[0]
@@ -160,13 +207,20 @@ def _get_shares():
"""Get shares."""
shares = []
output = subprocess.check_output(['net', 'conf', 'list'])
- config = configparser.ConfigParser()
+ config = configparser.RawConfigParser()
config.read_string(output.decode())
for name in config.sections():
- path = config[name]['path']
- mount_point = _get_mount_point(path)
+ share_type = 'open'
+ if name.endswith('_group'):
+ share_type = 'group'
+ elif name.endswith('_home'):
+ share_type = 'home'
+ share_path = config[name]['path']
+ mount_point = _get_mount_point(share_path)
mount_point = os.path.normpath(mount_point)
- shares.append(dict(name=name, mount_point=mount_point, path=path))
+ shares.append(
+ dict(name=name, mount_point=mount_point, path=share_path,
+ share_type=share_type))
return shares
@@ -221,19 +275,31 @@ def _use_config_file(conf_file):
aug.save()
-def _validate_mount_point(path):
- """Validate that given path string is a mount point."""
- if path != '/':
- parent_path = os.path.dirname(path)
- if os.stat(path).st_dev == os.stat(parent_path).st_dev:
- raise RuntimeError('Path "{0}" is not a mount point.'.format(path))
+def _set_share_permissions(directory):
+ """Set file and directory permissions for a share."""
+ shutil.chown(directory, group='freedombox-share')
+ os.chmod(directory, 0o2775)
+ for root, dirs, files in os.walk(directory):
+ for subdir in dirs:
+ subdir_path = os.path.join(root, subdir)
+ shutil.chown(subdir_path, group='freedombox-share')
+ os.chmod(subdir_path, 0o2775)
+ for file in files:
+ file_path = os.path.join(root, file)
+ shutil.chown(file_path, group='freedombox-share')
+ os.chmod(file_path, 0o0664)
+ subprocess.check_call(['setfacl', '-Rm', 'g::rwX', directory])
+ subprocess.check_call(['setfacl', '-Rdm', 'g::rwX', directory])
def subcommand_add_share(arguments):
"""Create a samba share."""
mount_point = os.path.normpath(arguments.mount_point)
- _validate_mount_point(mount_point)
- _create_share(mount_point, arguments.windows_filesystem)
+ if not os.path.ismount(mount_point):
+ raise RuntimeError(
+ 'Path "{0}" is not a mount point.'.format(mount_point))
+ _create_share(mount_point, arguments.share_type,
+ arguments.windows_filesystem)
def subcommand_delete_share(arguments):
@@ -241,13 +307,10 @@ def subcommand_delete_share(arguments):
mount_point = os.path.normpath(arguments.mount_point)
shares = _get_shares()
for share in shares:
- if share['mount_point'] == mount_point:
+ if share['mount_point'] == mount_point and share[
+ 'share_type'] == arguments.share_type:
_close_share(share['name'])
_conf_command(['delshare', share['name']])
- break
- else:
- raise RuntimeError(
- 'Mount point "{0}" is not shared.'.format(mount_point))
def subcommand_get_shares(_):
@@ -255,11 +318,20 @@ def subcommand_get_shares(_):
print(json.dumps(_get_shares()))
+def subcommand_get_users(_):
+ """Get users from Samba database."""
+ output = subprocess.check_output(['pdbedit', '-L']).decode()
+ samba_users = [line.split(':')[0] for line in output.split()]
+ print(json.dumps({'users': samba_users}))
+
+
def subcommand_setup(_):
"""Configure samba, use custom samba config file."""
with open(CONF_PATH, 'w') as file_handle:
file_handle.write(CONF)
_use_config_file(CONF_PATH)
+ os.makedirs('/var/lib/freedombox', exist_ok=True)
+ os.chmod('/var/lib/freedombox', 0o0755)
if action_utils.service_is_running('smbd'):
action_utils.service_restart('smbd')
diff --git a/actions/users b/actions/users
index 605b91156..3b26f1398 100755
--- a/actions/users
+++ b/actions/users
@@ -27,7 +27,6 @@ import subprocess
import sys
import augeas
-
from plinth import action_utils
ACCESS_CONF = '/etc/security/access.conf'
@@ -80,6 +79,12 @@ def parse_arguments():
subparser.add_argument('username', help='LDAP user to add to group')
subparser.add_argument('groupname', help='LDAP group to add the user to')
+ subparser = subparsers.add_parser('set-user-status',
+ help='Set user as active or inactive')
+ subparser.add_argument('username', help='User to change status')
+ subparser.add_argument('status', choices=['active', 'inactive'],
+ help='New status of the user')
+
subparser = subparsers.add_parser(
'remove-user-from-group',
help='Remove an LDAP user from an LDAP group')
@@ -227,6 +232,23 @@ def configure_ldapscripts():
aug.save()
+def get_samba_users():
+ """Get users from the Samba user database."""
+ # 'pdbedit -L' is better for listing users but is installed only with samba
+ stdout = subprocess.check_output(
+ ['tdbdump', '/var/lib/samba/private/passdb.tdb']).decode()
+ return re.findall(r'USER_(.*)\\0', stdout)
+
+
+def disconnect_samba_user(username):
+ """Disconnect a Samba user."""
+ try:
+ subprocess.check_call(['pkill', '-U', username, 'smbd'])
+ except subprocess.CalledProcessError as error:
+ if error.returncode != 1:
+ raise
+
+
def read_password():
"""Read the password from stdin."""
return ''.join(sys.stdin)
@@ -235,8 +257,10 @@ def read_password():
def subcommand_create_user(arguments):
"""Create an LDAP user, set password and flush cache."""
_run(['ldapadduser', arguments.username, 'users'])
- set_user_password(arguments.username, read_password())
+ password = read_password()
+ set_user_password(arguments.username, password)
flush_cache()
+ set_samba_user(arguments.username, password)
def subcommand_remove_user(arguments):
@@ -244,10 +268,15 @@ def subcommand_remove_user(arguments):
username = arguments.username
groups = get_user_groups(username)
+ if username in get_samba_users():
+ subprocess.check_call(['smbpasswd', '-x', username])
+ disconnect_samba_user(username)
+
for group in groups:
remove_user_from_group(username, group)
_run(['ldapdeleteuser', username])
+
flush_cache()
@@ -275,9 +304,25 @@ def set_user_password(username, password):
_run(['ldapsetpasswd', username, password])
+def set_samba_user(username, password):
+ """Insert a user to the Samba database.
+
+ If a user already exists, update password.
+ """
+ proc = subprocess.Popen(['smbpasswd', '-a', '-s', username],
+ stdin=subprocess.PIPE, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ _, stderr = proc.communicate(input='{0}\n{0}\n'.format(password).encode(),
+ timeout=10)
+ if proc.returncode != 0:
+ raise RuntimeError('Unable to add Samba user: ', stderr)
+
+
def subcommand_set_user_password(arguments):
"""Set a user's password."""
- set_user_password(arguments.username, read_password())
+ password = read_password()
+ set_user_password(arguments.username, password)
+ set_samba_user(arguments.username, password)
def get_user_groups(username):
@@ -352,6 +397,8 @@ def subcommand_remove_user_from_group(arguments):
"""Remove an LDAP user from an LDAP group."""
remove_user_from_group(arguments.username, arguments.groupname)
flush_cache()
+ if arguments.groupname == 'freedombox-share':
+ disconnect_samba_user(arguments.username)
def subcommand_get_group_users(arguments):
@@ -369,6 +416,22 @@ def subcommand_get_group_users(arguments):
print(user)
+def subcommand_set_user_status(arguments):
+ """Set the status of the user."""
+ username = arguments.username
+ status = arguments.status
+
+ if status == 'active':
+ flag = '-e'
+ else:
+ flag = '-d'
+
+ if username in get_samba_users():
+ subprocess.check_call(['smbpasswd', flag, username])
+ if status == 'inactive':
+ disconnect_samba_user(username)
+
+
def flush_cache():
"""Flush nscd and apache2 cache."""
_run(['nscd', '--invalidate=passwd'])
diff --git a/plinth/modules/samba/__init__.py b/plinth/modules/samba/__init__.py
index 9524ed27f..937299b1a 100644
--- a/plinth/modules/samba/__init__.py
+++ b/plinth/modules/samba/__init__.py
@@ -18,7 +18,10 @@
FreedomBox app to configure samba.
"""
+import grp
import json
+import os
+import pwd
import socket
from django.urls import reverse_lazy
@@ -34,7 +37,7 @@ from plinth.utils import format_lazy
from .manifest import backup, clients # noqa, pylint: disable=unused-import
-version = 1
+version = 2
managed_services = ['smbd', 'nmbd']
@@ -51,12 +54,18 @@ description = [
'other computers in your local network.'),
format_lazy(
_('After installation, you can choose which disks to use for sharing. '
- 'Enabled {hostname} shares are open to everyone in your local '
- 'network and are accessible under Network section in the file '
- 'manager on your computer.'), hostname=socket.gethostname().upper())
+ 'Enabled shares are accessbile in the file manager on your computer '
+ 'at location \\\\{hostname} (on Windows) or smb://{hostname}.local '
+ '(on Linux and Mac). There are three types of shares '
+ 'you can choose from: '), hostname=socket.gethostname()),
+ _('Open share - accessible to everyone in your local network.'),
+ _('Group share - accessible only to FreedomBox users who are in the '
+ 'freedombox-share group.'),
+ _('Home share - every user in the freedombox-share group can have their '
+ 'own private space.'),
]
-group = ('freedombox-share', _('Access shared folders from inside the server'))
+group = ('freedombox-share', _('Access to the private shares'))
clients = clients
@@ -124,18 +133,43 @@ def diagnose():
return results
-def add_share(mount_point, filesystem):
+def add_share(mount_point, share_type, filesystem):
"""Add a share."""
- command = ['add-share', '--mount-point', mount_point]
+ command = [
+ 'add-share', '--mount-point', mount_point, '--share-type', share_type
+ ]
if filesystem in ['ntfs', 'vfat']:
command = command + ['--windows-filesystem']
actions.superuser_run('samba', command)
-def delete_share(mount_point):
+def delete_share(mount_point, share_type):
"""Delete a share."""
- command = ['delete-share', '--mount-point', mount_point]
- actions.superuser_run('samba', command)
+ actions.superuser_run('samba', [
+ 'delete-share', '--mount-point', mount_point, '--share-type',
+ share_type
+ ])
+
+
+def get_users():
+ """Get non-system users who are in the freedombox-share or admin group."""
+ output = actions.superuser_run('samba', ['get-users'])
+ samba_users = json.loads(output)['users']
+ group_users = grp.getgrnam('freedombox-share').gr_mem + grp.getgrnam(
+ 'admin').gr_mem
+
+ allowed_users = []
+ for group_user in group_users:
+ uid = pwd.getpwnam(group_user).pw_uid
+ if uid > 1000:
+ allowed_users.append(group_user)
+
+ return {
+ 'access_ok':
+ sorted(set(allowed_users) & set(samba_users)),
+ 'password_re_enter_needed':
+ sorted(set(allowed_users) - set(samba_users))
+ }
def get_shares():
@@ -145,6 +179,15 @@ def get_shares():
return json.loads(output)
+def disk_name(mount_point):
+ """Get a disk name."""
+ share_name = os.path.basename(mount_point)
+ if not share_name:
+ share_name = 'disk'
+
+ return share_name
+
+
def backup_pre(packet):
"""Save registry share configuration."""
actions.superuser_run('samba', ['dump-shares'])
diff --git a/plinth/modules/samba/static/samba.js b/plinth/modules/samba/static/samba.js
deleted file mode 100644
index 9fd6f45e1..000000000
--- a/plinth/modules/samba/static/samba.js
+++ /dev/null
@@ -1,33 +0,0 @@
-/**
- * @licstart The following is the entire license notice for the JavaScript
- * code in this page.
- *
- * This file is part of FreedomBox.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see
{% blocktrans trimmed %} - Note: only specially created directory will be shared on selected disks, + Note: only specially created directories will be shared on selected disks, not the whole disk. {% endblocktrans %}
-| - | {% trans "Device" %} | -{% trans "Label" %} | -{% trans "Mount Point" %} | -{% trans "Type" %} | +{% trans "Disk Name" %} | +{% trans "Shares" %} | {% trans "Used" %} |
|---|---|---|---|---|---|---|---|
| {{ disk.name|default_if_none:"" }} | - {% if disk.mount_point in shared_mounts %} - - {% else %} - - {% endif %} + | -{{ disk.device }} | -{{ disk.label|default_if_none:"" }} | -{{ disk.mount_point }} | -{{ disk.filesystem_type }} | -+ |
{% if disk.percent_used < 75 %}
-
- {{ disk.used_str }} / {{ disk.size_str }}
+ role="progressbar" aria-valuenow="{{ disk.percent_used }}"
+ aria-valuemin="0" aria-valuemax="100"
+ style="width: {{ disk.percent_used }}%;">
+ {{ disk.percent_used }}%
+
+
+ {{ disk.used_str }} / {{ disk.size_str }}
|
+ {% url 'storage:index' as storage_url %} + {% url 'users:index' as users_url %} + {% blocktrans trimmed %} + You can find additional information about disks on the + storage module page and configure + access to the shares on the users module page. + {% endblocktrans %}
+ +{% trans "Users who can currently access group and home shares" %}: + {{ users.access_ok|join:", " }}
+ + {% if users.password_re_enter_needed %} +{% trans "Users who need to re-enter their password on the password change page to access group and home shares" %}: + {{ users.password_re_enter_needed|join:", " }}.
+ {% endif %} {% if unavailable_shares %} -- {% trans "If the disk is plugged back in, sharing will be automatically enabled." %} + {% blocktrans trimmed %} + Shares that are configured but the disk is not available. If the disk + is plugged back in, sharing will be automatically enabled. + {% endblocktrans %}
| {{ share.name }} | @@ -135,6 +151,3 @@ {% endif %} {% endblock %} -{% block page_js %} - -{% endblock %} diff --git a/plinth/modules/samba/urls.py b/plinth/modules/samba/urls.py index 93961194e..8df63f5a4 100644 --- a/plinth/modules/samba/urls.py +++ b/plinth/modules/samba/urls.py @@ -25,7 +25,5 @@ from . import views urlpatterns = [ url(r'^apps/samba/$', views.SambaAppView.as_view(), name='index'), url(r'^apps/samba/share/(?P |