James Valleroy 87eecb7c62
users: Handle upgrade for ldapscripts config
Increment users module version so existing users will get FreedomBox
copy of ldapscripts config file.

Avoid reconfiguration of slapd during module upgrade, because this
will move the existing database.

Signed-off-by: James Valleroy <jvalleroy@mailbox.org>
Reviewed-by: Sunil Mohan Adapa <sunil@medhas.org>
2017-12-14 11:43:07 +05:30

372 lines
12 KiB
Python
Executable File

#!/usr/bin/python3
# -*- mode: python -*-
#
# This file is part of Plinth.
#
# 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 <http://www.gnu.org/licenses/>.
#
"""
Configuration helper for the LDAP user directory
"""
import argparse
import augeas
import os
import re
import shutil
import subprocess
import sys
from plinth import action_utils
ACCESS_CONF = '/etc/security/access.conf'
LDAPSCRIPTS_CONF = '/etc/ldapscripts/freedombox-ldapscripts.conf'
def parse_arguments():
"""Return parsed command line arguments as dictionary"""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
subparsers.add_parser('first-setup', help='Perform initial setup of LDAP')
subparsers.add_parser('setup', help='Setup LDAP')
subparser = subparsers.add_parser('create-user',
help='Create an LDAP user')
subparser.add_argument('username', help='Name of the LDAP user to create')
subparser = subparsers.add_parser('remove-user',
help='Delete an LDAP user')
subparser.add_argument('username', help='Name of the LDAP user to delete')
subparser = subparsers.add_parser('rename-user',
help='Rename an LDAP user')
subparser.add_argument('oldusername', help='Old name of the LDAP user')
subparser.add_argument('newusername', help='New name of the LDAP user')
subparser = subparsers.add_parser('set-user-password',
help='Set the password of an LDAP user')
subparser.add_argument(
'username', help='Name of the LDAP user to set the password for')
subparser = subparsers.add_parser('create-group',
help='Create an LDAP group')
subparser.add_argument('groupname',
help='Name of the LDAP group to create')
subparser = subparsers.add_parser('remove-group',
help='Delete an LDAP group')
subparser.add_argument('groupname',
help='Name of the LDAP group to delete')
subparser = subparsers.add_parser(
'get-user-groups', help='Get all the LDAP groups for an LDAP user')
subparser.add_argument('username',
help='LDAP user to retrieve the groups for')
subparser = subparsers.add_parser('add-user-to-group',
help='Add an LDAP user to an LDAP group')
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(
'remove-user-from-group',
help='Remove an LDAP user from an LDAP group')
subparser.add_argument('username', help='LDAP user to remove from group')
subparser.add_argument('groupname',
help='LDAP group to remove the user from')
subparsers.required = True
return parser.parse_args()
def subcommand_first_setup(_):
"""Perform initial setup of LDAP."""
# Avoid reconfiguration of slapd during module upgrades, because
# this will move the existing database.
action_utils.dpkg_reconfigure('slapd', {'domain': 'thisbox'})
def subcommand_setup(_):
"""Setup LDAP."""
# Update pam configs for access and mkhomedir.
subprocess.run(['pam-auth-update', '--package'], check=True)
configure_ldapscripts()
configure_ldap_authentication()
configure_ldap_structure()
def configure_ldap_authentication():
"""Configure LDAP authentication."""
action_utils.dpkg_reconfigure('nslcd', {
'ldap-uris': 'ldapi:///',
'ldap-base': 'dc=thisbox',
'ldap-auth-type': 'SASL',
'ldap-sasl-mech': 'EXTERNAL'
})
action_utils.dpkg_reconfigure('libnss-ldapd',
{'nsswitch': 'group, passwd, shadow'})
action_utils.service_restart('nscd')
# XXX: Workaround for login issue
action_utils.service_enable('slapd')
action_utils.service_start('slapd')
action_utils.service_enable('nslcd')
action_utils.service_start('nslcd')
def configure_ldap_structure():
"""Configure LDAP basic structure."""
was_running = action_utils.service_is_running('slapd')
if not was_running:
action_utils.service_start('slapd')
setup_admin()
create_organizational_unit('users')
create_organizational_unit('groups')
def create_organizational_unit(unit):
"""Create an organizational unit in LDAP."""
distinguished_name = 'ou={unit},dc=thisbox'.format(unit=unit)
try:
subprocess.run([
'ldapsearch', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///', '-s',
'base', '-b', distinguished_name, '(objectclass=*)'
], stdout=subprocess.DEVNULL, check=True)
return # Already exists
except subprocess.CalledProcessError:
input = '''
dn: ou={unit},dc=thisbox
objectClass: top
objectClass: organizationalUnit
ou: {unit}'''.format(unit=unit)
subprocess.run(['ldapadd', '-Q', '-Y', 'EXTERNAL', '-H',
'ldapi:///'], input=input.encode(),
stdout=subprocess.DEVNULL, check=True)
def setup_admin():
"""Remove LDAP admin password and Allow root to modify the users."""
process = subprocess.run([
'ldapsearch', '-Q', '-L', '-L', '-L', '-Y', 'EXTERNAL', '-H',
'ldapi:///', '-s', 'base', '-b', 'olcDatabase={1}mdb,cn=config',
'(objectclass=*)', 'olcRootDN', 'olcRootPW'
], check=True, stdout=subprocess.PIPE)
ldap_object = {}
for line in process.stdout.decode().splitlines():
if line:
line = line.split(':')
ldap_object[line[0]] = line[1]
if 'olcRootPW' in ldap_object:
subprocess.run(
['ldapmodify', '-Q', '-Y', 'EXTERNAL', '-H',
'ldapi:///'], check=True, stdout=subprocess.DEVNULL, input=b'''
dn: olcDatabase={1}mdb,cn=config
changetype: modify
delete: olcRootPW''')
root_dn = 'gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth'
if ldap_object['olcRootDN'] != root_dn:
subprocess.run(
['ldapmodify', '-Q', '-Y', 'EXTERNAL', '-H',
'ldapi:///'], check=True, stdout=subprocess.DEVNULL, input=b'''
dn: olcDatabase={1}mdb,cn=config
changetype: modify
replace: olcRootDN
olcRootDN: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
''')
def configure_ldapscripts():
"""Set the configuration used by ldapscripts for later user management."""
# modify a copy of the config file
shutil.copy('/etc/ldapscripts/ldapscripts.conf', LDAPSCRIPTS_CONF)
aug = augeas.Augeas(
flags=augeas.Augeas.NO_LOAD + augeas.Augeas.NO_MODL_AUTOLOAD)
aug.set('/augeas/load/Shellvars/lens', 'Shellvars.lns')
aug.set('/augeas/load/Shellvars/incl[last() + 1]', LDAPSCRIPTS_CONF)
aug.load()
# XXX: Password setting on users is disabled as changing passwords
# using SASL Auth is not supported.
aug.set('/files' + LDAPSCRIPTS_CONF + '/SERVER', '"ldapi://"')
aug.set('/files' + LDAPSCRIPTS_CONF + '/SASLAUTH', '"EXTERNAL"')
aug.set('/files' + LDAPSCRIPTS_CONF + '/SUFFIX', '"dc=thisbox"')
aug.set('/files' + LDAPSCRIPTS_CONF + '/USUFFIX', '"ou=Users"')
aug.set('/files' + LDAPSCRIPTS_CONF + '/GSUFFIX', '"ou=Groups"')
aug.set('/files' + LDAPSCRIPTS_CONF + '/PASSWORDGEN', '"true"')
aug.save()
def read_password():
"""Read the password from stdin."""
return ''.join(sys.stdin)
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())
flush_cache()
def subcommand_remove_user(arguments):
"""Remove an LDAP user."""
username = arguments.username
groups = get_user_groups(username)
for group in groups:
remove_user_from_group(username, group)
_run(['ldapdeleteuser', username])
flush_cache()
def subcommand_rename_user(arguments):
"""Rename an LDAP user."""
old_username = arguments.oldusername
new_username = arguments.newusername
groups = get_user_groups(old_username)
for group in groups:
remove_user_from_group(old_username, group)
_run(['ldaprenameuser', old_username, new_username])
for group in groups:
add_user_to_group(new_username, group)
flush_cache()
def set_user_password(username, password):
"""Set a user's password."""
process = _run(['slappasswd', '-s', password], stdout=subprocess.PIPE)
password = process.stdout.decode().strip()
_run(['ldapsetpasswd', username, password])
def subcommand_set_user_password(arguments):
"""Set a user's password."""
set_user_password(arguments.username, read_password())
def get_user_groups(username):
"""Returns only the supplementary groups of the given user.
Exclude the 'users' primary group from the returned list."""
process = _run(['ldapid', username], stdout=subprocess.PIPE, check=False)
output = process.stdout.decode().strip()
if output:
groups_part = output.split(' ')[2]
groups = groups_part.split('=')[1]
group_names = [
user.strip('()') for user in re.findall('\(.*?\)', groups)
]
group_names.remove('users')
return group_names
return []
def subcommand_get_user_groups(arguments):
"""Return list of a given user's groups."""
groups = get_user_groups(arguments.username)
if groups:
print(*groups, sep='\n')
def group_exists(groupname):
"""Return whether a group already exits."""
process = _run(['ldapgid', groupname], check=False)
return process.returncode == 0
def create_group(groupname):
"""Add an LDAP group."""
if not group_exists(groupname):
_run(['ldapaddgroup', groupname])
def subcommand_create_group(arguments):
"""Add an LDAP group."""
create_group(arguments.groupname)
flush_cache()
def subcommand_remove_group(arguments):
"""Remove an LDAP group."""
if group_exists(arguments.groupname):
_run(['ldapdeletegroup', arguments.groupname])
flush_cache()
def add_user_to_group(username, groupname):
"""Add an LDAP user to an LDAP group."""
create_group(groupname)
_run(['ldapaddusertogroup', username, groupname])
def subcommand_add_user_to_group(arguments):
"""Add an LDAP user to an LDAP group."""
add_user_to_group(arguments.username, arguments.groupname)
flush_cache()
def remove_user_from_group(username, groupname):
"""Remove an LDAP user from an LDAP group."""
_run(['ldapdeleteuserfromgroup', username, groupname])
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()
def flush_cache():
"""Flush nscd cache."""
_run(['nscd', '--invalidate=passwd'])
_run(['nscd', '--invalidate=group'])
def _run(arguments, **kwargs):
"""Run a command. Check return code and suppress output by default."""
env = dict(os.environ, LDAPSCRIPTS_CONF=LDAPSCRIPTS_CONF)
kwargs['stdout'] = kwargs.get('stdout', subprocess.DEVNULL)
kwargs['stderr'] = kwargs.get('stderr', subprocess.DEVNULL)
kwargs['check'] = kwargs.get('check', True)
return subprocess.run(arguments, env=env, **kwargs)
def main():
"""Parse arguments and perform all duties"""
arguments = parse_arguments()
subcommand = arguments.subcommand.replace('-', '_')
subcommand_method = globals()['subcommand_' + subcommand]
subcommand_method(arguments)
if __name__ == '__main__':
main()