#!/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 . # """ Configuration helper for the LDAP user directory """ import argparse import subprocess import augeas from plinth import action_utils ACCESS_CONF = '/etc/security/access.conf' LDAPSCRIPTS_CONF = '/etc/ldapscripts/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( 'pre-install', help='Preseed debconf values before packages are installed') subparsers.add_parser('setup', help='Setup LDAP') return parser.parse_args() def subcommand_pre_install(_): """Preseed debconf values before packages are installed.""" subprocess.run( ['debconf-set-selections'], input=b'slapd slapd/domain string thisbox', check=True) subprocess.run( ['debconf-set-selections'], input=b'nslcd nslcd/ldap-uris string "ldapi:///"', check=True) subprocess.run( ['debconf-set-selections'], input=b'nslcd nslcd/ldap-base string "dc=thisbox"', check=True) subprocess.run( ['debconf-set-selections'], input=b'nslcd nslcd/ldap-auth-type select SASL', check=True) subprocess.run( ['debconf-set-selections'], input=b'nslcd nslcd/ldap-sasl-mech select EXTERNAL', check=True) subprocess.run( ['debconf-set-selections'], input=b'libnss-ldapd libnss-ldapd/nsswitch multiselect ' b'group, passwd, shadow', check=True) def subcommand_setup(_): """Setup LDAP.""" configure_access_conf() # Update pam configs for access and mkhomedir. subprocess.run(['pam-auth-update', '--package'], check=True) configure_ldapscripts() configure_slapd() def configure_slapd(): """Configure LDAP authentication and basic structure.""" was_running = action_utils.service_is_running('slapd') if not was_running: action_utils.service_start('slapd') try: setup_admin() create_organizational_unit('users') create_organizational_unit('groups') finally: if not was_running: action_utils.service_stop('slapd') 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_access_conf(): """Restrict console login to users in admin or sudo group.""" with open(ACCESS_CONF, 'r') as conffile: lines = conffile.readlines() for line in lines: if '-:ALL EXCEPT root fbx (admin) (sudo):ALL' in line: return with open(ACCESS_CONF, 'a') as conffile: conffile.write('-:ALL EXCEPT root fbx (admin) (sudo):ALL\n') def configure_ldapscripts(): """Set the configuration used by ldapscripts for later user management.""" 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 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()