From 3c3fe1b6a5492b7b074f7e460ea9403d5c4801c2 Mon Sep 17 00:00:00 2001 From: Thierry bordaz (tbordaz) Date: Mar 13 2015 14:10:39 +0000 Subject: Ticket 48127: Using RPM, allows non root user to create/remove DS instance Bug Description: RPM 389-ds scripts to create and delete instance, assumes that the caller can update selinux, tmpfiles and systemD. This prevent regular user to create/delete an instance. Fix Description: Test the caller is root before doing these updates The fix also create a helper command 'dsadm.py' to create/delete/start/stop/restart instance This command is not delivered https://fedorahosted.org/389/ticket/48127 Reviewed by: Rich (thanks Rich !) Platforms tested: F17/F21 Flag Day: no Doc impact: no --- diff --git a/dirsrvtests/cmd/dsadm/dsadm.py b/dirsrvtests/cmd/dsadm/dsadm.py new file mode 100755 index 0000000..0d868c2 --- /dev/null +++ b/dirsrvtests/cmd/dsadm/dsadm.py @@ -0,0 +1,551 @@ +#! /usr/bin/python2 + +# Authors: +# Thierry Bordaz +# +# Copyright (C) 2015 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys +import os +import argparse +import pdb +import tempfile +import time +import pwd +import grp +import platform +import socket +import shutil +from subprocess import Popen, PIPE, STDOUT +import string + +SETUP_DS = "/sbin/setup-ds.pl" +REMOVE_DS = "/sbin/remove-ds.pl" +INITCONFIGDIR = ".dirsrv" +SCRIPT_START = "start-slapd" +SCRIPT_STOP = "stop-slapd" +SCRIPT_RESTART = "restart-slapd" +ENVIRON_SERVERID = '389-SERVER-ID' +ENVIRON_USER = '389-USER' +ENVIRON_GROUP = '389-GROUP' +ENVIRON_DIRECTORY = '389-DIRECTORY' +ENVIRON_PORT = '389-PORT' +ENVIRON_SECURE_PORT = '389-SECURE-PORT' +DEFAULT_PORT_ROOT = str(389) +DEFAULT_PORT_NON_ROOT = str(1389) +DEFAULT_SECURE_PORT_ROOT = str(636) +DEFAULT_SECURE_PORT_NON_ROOT = str(1636) +DEFAULT_USER = 'nobody' +DEFAULT_GROUP = 'nobody' +DEFAULT_ROOT_DN = 'cn=Directory Manager' +DEFAULT_HOSTNAME = socket.gethostname() + + + +def validate_user(user): + ''' + If a user is provided it returns its username + else it returns the current username. + It checks that the userId or userName exists + + :param: user (optional) can be a userName or userId + :return: userName of the provided user, if none is provided, it returns current user name + ''' + assert(user) + if user.isdigit(): + try: + username = pwd.getpwuid(int(user)).pw_name + except KeyError: + raise KeyError('Unknown userId %d' % user) + return username + else: + try: + pwd.getpwnam(user).pw_uid + except KeyError: + raise KeyError('Unknown userName %s' % user) + return user + +def get_default_user(): + user = os.environ.get(ENVIRON_USER, None) + if not user: + user = os.getuid() + return str(user) + +def get_default_group(): + ''' + If a group is provided it returns its groupname + else it returns the current groupname. + It checks that the groupId or groupName exists + + :param: group (optional) can be a groupName or groupId + :return: groupName of the provided group, if none is provided, it returns current group name + ''' + group = os.environ.get(ENVIRON_GROUP, None) + if not group: + return pwd.getpwuid(os.getuid()).pw_name + return group + +def validate_group(group): + assert(group) + if str(group).isdigit(): + try: + groupname = grp.getgrgid(group).gr_name + return groupname + except: + raise KeyError('Unknown groupId %d' % group) + else: + try: + groupname = grp.getgrnam(group).gr_name + return groupname + except: + raise KeyError('Unknown groupName %s' % group) + +def test_get_group(): + try: + grpname = get_default_group() + print 'get_group: %s' % grpname + except: + raise + print "Can not find user group" + pass + try: + grpname = get_default_group(group='tbordaz') + print 'get_group: %s' % grpname + except: + raise + print "Can not find user group" + pass + try: + grpname = get_default_group(group='coucou') + print 'get_group: %s' % grpname + except: + print "Can not find user group coucou" + pass + try: + grpname = get_default_group('thierry') + print 'get_group: %s' % grpname + except: + raise + print "Can not find user group thierry" + pass + try: + grpname = get_default_group(1000) + print 'get_group: %s' % grpname + except: + raise + print "Can not find user group 1000" + pass + try: + grpname = get_default_group(20532) + print 'get_group: %s' % grpname + except: + raise + print "Can not find user group 20532" + pass + try: + grpname = get_default_group(123) + print 'get_group: %s' % grpname + except: + print "Can not find user group 123" + pass + +def get_default_port(): + port = os.environ.get(ENVIRON_PORT, None) + if port: + return port + + if os.getuid() == 0: + return DEFAULT_PORT_ROOT + else: + return DEFAULT_PORT_NON_ROOT + +def validate_port(port): + assert port + if not port.isdigit() or int(port) <= 0 : + raise Exception("port number is invalid: %s" % port) + +def get_default_directory(): + directory = os.environ.get(ENVIRON_DIRECTORY, None) + if not directory: + directory = os.getcwd() + return directory + +def validate_directory(directory): + assert directory + if not os.path.isdir(directory): + raise Exception("Supplied directory path is not a directory") + + if not os.access(directory, os.W_OK): + raise Exception("Supplied directory is not writable") + +def get_default_serverid(): + serverid = os.environ.get(ENVIRON_SERVERID, None) + if not serverid: + serverid = socket.gethostname().split('.')[0] + return serverid + +def validate_serverid(serverid): + if not serverid: + raise Exception("Server id is not defined") + return serverid + + +def get_inst_dir(serverid): + assert serverid + home = os.getenv("HOME") + inst_initconfig_file = "%s/%s/dirsrv-%s" % (home, INITCONFIGDIR, serverid) + if not os.path.isfile(inst_initconfig_file): + raise Exception("%s config file not found" % inst_initconfig_file) + f = open(inst_initconfig_file, "r") + for line in f: + if line.startswith("INST_DIR"): + inst_dir = line.split("=")[1] + inst_dir = inst_dir.replace("\r", "") + inst_dir = inst_dir.replace("\n", "") + return inst_dir + +def sanity_check(): + if os.getuid() == 0: + raise Exception("Not tested for root user.. sorry") + + home = os.getenv("HOME") + inst_initconfig_dir = "%s/%s" % (home, INITCONFIGDIR) + if not os.path.isdir(inst_initconfig_dir): + raise Exception("Please create the directory \'%s\' and retry." % inst_initconfig_dir ) + +class DSadmCmd(object): + def __init__(self): + self.version = '0.1' + + def _start_subparser(self, subparsers): + start_parser = subparsers.add_parser( + 'start', + help='Start a Directory Server Instance') + start_parser.add_argument('-I', '--server-id', dest='server_id', type=str, nargs='?', + metavar='SERVER-ID', + help='Server Identifier (Default: %s) ' % get_default_serverid()) + start_parser.set_defaults(func=self.start_action) + + def _stop_subparser(self, subparsers): + start_parser = subparsers.add_parser( + 'stop', + help='Stop a Directory Server Instance') + start_parser.add_argument('-I', '--server-id', dest='server_id', type=str, nargs='?', + metavar='SERVER-ID', + help='Server Identifier (Default: %s) ' % get_default_serverid()) + start_parser.set_defaults(func=self.stop_action) + + def _restart_subparser(self, subparsers): + start_parser = subparsers.add_parser( + 'restart', + help='Retart a Directory Server Instance') + start_parser.add_argument('-I', '--server-id', dest='server_id', type=str, nargs='?', + metavar='SERVER-ID', + help='Server Identifier (Default: %s) ' % get_default_serverid()) + start_parser.set_defaults(func=self.restart_action) + + def _delete_subparser(self, subparsers): + delete_parser = subparsers.add_parser( + 'delete', + help='Delete a Directory Server Instance') + delete_parser.add_argument('-I', '--server-id', dest='server_id', type=str, nargs='?', + metavar='SERVER-ID', + help='Server Identifier (Default: %s) ' % get_default_serverid()) + delete_parser.add_argument('-debug', '--debug', dest='debug_level', type=int, nargs='?', + metavar='DEBUG_LEVEL', + help='Debug level (Default: 0)') + delete_parser.set_defaults(func=self.delete_action) + + def _create_subparser(self, subparsers): + create_parser = subparsers.add_parser( + 'create', + help='Create a Directory Server Instance') + create_parser.add_argument('-I', '--server-id', dest='server_id', type=str, nargs='?', + metavar='SERVER-ID', + help='Server Identifier (Default: %s) ' % get_default_serverid()) + create_parser.add_argument('-s', '--suffix', dest='suffix', type=str, nargs='?', + metavar='SUFFIX-DN', + help='Suffix (Default: create no suffix)') + create_parser.add_argument('-p', '--port', dest='port', type=int, nargs='?', + metavar='NON-SECURE-PORT', + help='Normal Port to listen (Default: %s(root)/%s(non-root)) ' % (DEFAULT_PORT_ROOT, DEFAULT_PORT_NON_ROOT)) + + create_parser.add_argument('-P', '--secure-port', dest='secure_port', type=int, nargs='?', + metavar='SECURE-PORT', + help='Secure Port to listen (Default: %s(root)/%s(non-root))' % (DEFAULT_SECURE_PORT_ROOT, DEFAULT_SECURE_PORT_NON_ROOT)) + + create_parser.add_argument('-D', '--rootDN', dest='root_dn', type=str, nargs='?', + metavar='ROOT-DN', + help='Uses DN as Directory Manager DN (Default: \'%s\')' % (DEFAULT_ROOT_DN)) + + create_parser.add_argument('-u', '--user-name', dest='user_name', type=str, nargs='?', + metavar='USER-NAME', + help='User name of the instance owner (Default: %s)' % DEFAULT_USER) + + create_parser.add_argument('-g', '--group-name', dest='group_name', type=str, nargs='?', + metavar='GROUP-NAME', + help='Group name of the instance owner (Default: %s)' % DEFAULT_GROUP) + + create_parser.add_argument('-d', '--directory-path', dest='directory_path', type=str, nargs='?', + metavar='DIRECTORY-PATH', + help='Installation directory path (Default: %s)' % get_default_directory()) + create_parser.add_argument('-debug', '--debug', dest='debug_level', type=int, nargs='?', + metavar='DEBUG_LEVEL', + help='Debug level (Default: 0)') + create_parser.add_argument('-k', '--keep_template', dest='keep_template', type=str, nargs='?', + help='Keep template file') + + create_parser.set_defaults(func=self.create_action) + + # + # common function for start/stop/restart actions + # + def script_action(self, args, script, action_str): + args = vars(args) + serverid = args.get('server_id', None) + if not serverid: + serverid = get_default_serverid() + + script_file = "%s/%s" % (get_inst_dir(serverid), script) + if not os.path.isfile(script_file): + raise Exception("%s not found" % script_file) + + if not os.access(script_file, os.X_OK): + raise Exception("%s not executable" % script_file) + + env = os.environ.copy() + prog = [ script_file ] + pipe = Popen(prog, cwd=os.getcwd(), env=env, + stdin=PIPE, stdout=PIPE, stderr=STDOUT) + child_stdin = pipe.stdin + child_stdout = pipe.stdout + for line in child_stdout: + sys.stdout.write(line) + child_stdout.close() + child_stdin.close() + + rc = pipe.wait() + if rc == 0: + print "Directory %s %s" % (serverid, action_str) + else: + print "Failure: directory %s not %s (%s)" % (serverid, action_str, rc) + return + + def start_action(self, args): + self.script_action(args, SCRIPT_START, "started") + + + def stop_action(self, args): + self.script_action(args, SCRIPT_STOP, "stopped") + + + def restart_action(self, args): + + self.script_action(args, SCRIPT_RESTART, "restarted") + + def delete_action(self, args): + args = vars(args) + serverid = args.get('server_id', None) + if not serverid: + serverid = get_default_serverid() + + #prepare the remove-ds options + debug_level = args.get('debug_level', None) + if debug_level: + debug_str = ['-d'] + for i in range(1, int(debug_level)): + debug_str.append('d') + debug_str = ''.join(debug_str) + + env = os.environ.copy() + prog = [REMOVE_DS] + if debug_level: + prog.append(debug_str) + prog.append("-i") + prog.append("slapd-%s" % serverid) + + # run the REMOVE_DS command and print the possible output + pipe = Popen(prog, cwd=os.getcwd(), env=env, + stdin=PIPE, stdout=PIPE, stderr=STDOUT) + child_stdin = pipe.stdin + child_stdout = pipe.stdout + for line in child_stdout: + if debug_level: + sys.stdout.write(line) + child_stdout.close() + child_stdin.close() + + rc = pipe.wait() + if rc == 0: + print "Directory server \'%s\' successfully deleted" % serverid + else: + print "Fail to delete directory \'%s\': %d" % (serverid, rc) + return + + # + # used by create subcommand to build the template file + # + def _create_setup_ds_file(self, args, user=None, group=None): + # Get/checks the argument with the following order + # - parameter + # - Environment + # - default + serverid = args.get('server_id', None) + if not serverid: + serverid = get_default_serverid() + serverid = validate_serverid(serverid) + + username = args.get('user_name', None) + if not username: + username = get_default_user() + username = validate_user(username) + + groupname = args.get('group_name', None) + if not groupname: + groupname = get_default_group() + groupname = validate_group(groupname) + + directoryname = args.get('directory_path', None) + if not directoryname: + directoryname = get_default_directory() + validate_directory(directoryname) + + portnumber = args.get('port', None) + if not portnumber: + portnumber = get_default_port() + validate_port(portnumber) + + suffix = args.get('suffix', None) + + tempf = tempfile.NamedTemporaryFile(delete=False) + + tempf.write('[General]\n') + tempf.write('FullMachineName=%s\n' % DEFAULT_HOSTNAME) + tempf.write('SuiteSpotUserID=%s\n' % username) + tempf.write('SuiteSpotGroup=%s\n' % groupname) + tempf.write('ServerRoot=%s\n' % directoryname) + tempf.write('\n') + tempf.write('[slapd]\n') + tempf.write('ServerPort=1389\n') + tempf.write('ServerIdentifier=%s\n' % serverid) + if suffix: + tempf.write('Suffix=%s\n' % suffix) + tempf.write('RootDN=cn=Directory Manager\n') + tempf.write('RootDNPwd=Secret12\n') + tempf.write('sysconfdir=%s/etc\n' % directoryname) + tempf.write('localstatedir=%s/var\n' % directoryname) + tempf.write('inst_dir=%s/lib/dirsrv/slapd-%s\n'% (directoryname, serverid)) + tempf.write('config_dir=%s/etc/dirsrv/slapd-%s\n' % (directoryname, serverid)) + tempf.close() + + keep_template = args.get('keep_template', None) + if keep_template: + shutil.copy(tempf.name, keep_template) + + + return tempf + + # + # It silently creates an instance. + # After creation the instance is started + # + def create_action(self, args): + args = vars(args) + + # retrieve the serverid here just to log the final status + serverid = args.get('server_id', None) + if not serverid: + serverid = get_default_serverid() + + # prepare the template file + tempf = self._create_setup_ds_file(args) + + #prepare the setup-ds options + debug_level = args.get('debug_level', None) + if debug_level: + debug_str = ['-d'] + for i in range(1, int(debug_level)): + debug_str.append('d') + debug_str = ''.join(debug_str) + + # + # run the SETUP_DS command and print the possible output + # + env = os.environ.copy() + prog = [SETUP_DS] + if debug_level: + prog.append(debug_str) + prog.append("--silent") + prog.append("--file=%s" % tempf.name) + tempf.close() + + pipe = Popen(prog, cwd=os.getcwd(), env=env, + stdin=PIPE, stdout=PIPE, stderr=STDOUT) + child_stdin = pipe.stdin + child_stdout = pipe.stdout + for line in child_stdout: + if debug_level: + sys.stdout.write(line) + child_stdout.close() + child_stdin.close() + + os.unlink(tempf.name) + rc = pipe.wait() + if rc == 0: + print "Directory server \'%s\' successfully created" % serverid + else: + print "Fail to create directory \'%s\': %d" % (serverid, rc) + return + + # + # parser of the main command. It contains subcommands + # + def get_parser(self, argv): + + + parser = argparse.ArgumentParser( + description='Managing a local directory server instance') + + subparsers = parser.add_subparsers( + metavar='SUBCOMMAND', + help='The action to perform') + + #pdb.set_trace() + # subcommands + self._create_subparser(subparsers) + self._delete_subparser(subparsers) + self._start_subparser(subparsers) + self._stop_subparser(subparsers) + self._restart_subparser(subparsers) + + # Sanity check that the debug level is valid + args = vars(parser.parse_args(argv)) + debug_level = args.get('debug_level', None) + if debug_level and (int(debug_level) < 1 or int(debug_level > 5)): + raise Exception("invalid debug level: range 1..5") + + return parser + + def main(self, argv): + sanity_check() + parser = self.get_parser(argv) + args = parser.parse_args(argv) + args.func(args) + return + +if __name__ == '__main__': + DSadmCmd().main(sys.argv[1:]) diff --git a/ldap/admin/src/scripts/DSCreate.pm.in b/ldap/admin/src/scripts/DSCreate.pm.in index 2756b85..ae8d1e3 100644 --- a/ldap/admin/src/scripts/DSCreate.pm.in +++ b/ldap/admin/src/scripts/DSCreate.pm.in @@ -986,7 +986,7 @@ sub updateSelinuxPolicy { my $inf = shift; # if selinux is not available, do nothing - if ("@with_selinux@") { + if ((getLogin() eq 'root') and "@with_selinux@") { my $localstatedir = $inf->{slapd}->{localstatedir}; # run restorecon on all of the parent directories we @@ -1062,7 +1062,7 @@ sub updateTmpfilesDotD { my $parentdir; # if tmpfiles.d is not available, do nothing - if ($dir and -d $dir) { + if ((getLogin() eq 'root') and $dir and -d $dir) { my $filename = "$dir/@package_name@-$inf->{slapd}->{ServerIdentifier}.conf"; if (-f $filename) { debug(3, "Removing the old tmpfile: $filename\n"); @@ -1131,7 +1131,7 @@ sub updateSystemD { my $confbasedir = "@systemdsystemconfdir@"; my $confdir = "$confbasedir/@systemdgroupname@.wants"; - if (!$unitdir or !$confdir or ! -d $unitdir or ! -d $confdir) { + if ((getLogin() ne 'root') or !$unitdir or !$confdir or ! -d $unitdir or ! -d $confdir) { debug(3, "no systemd - skipping\n"); return (); } @@ -1421,7 +1421,7 @@ sub removeDSInstance { my $tmpfilesdir = "@with_tmpfiles_d@"; my $tmpfilesname = "$tmpfilesdir/@package_name@-$inst.conf"; - if ($tmpfilesdir && -d $tmpfilesdir && -f $tmpfilesname) { + if ((getLogin() eq 'root') && $tmpfilesdir && -d $tmpfilesdir && -f $tmpfilesname) { my $rc = unlink($tmpfilesname); if ( 0 == $rc ) { @@ -1431,7 +1431,7 @@ sub removeDSInstance { } # remove the selinux label from the ports if needed - if ("@with_selinux@") { + if ((getLogin() eq 'root') and "@with_selinux@") { foreach my $port (@{$entry->{"nsslapd-port"}}) { my $semanage_err;