From 7f188263cb0dfe36223f3834c830a07f514cf8dd Mon Sep 17 00:00:00 2001
From: "Thierry bordaz (tbordaz)" <tbordaz@redhat.com>
Date: Thu, 12 Mar 2015 19:05:25 +0100
Subject: [PATCH] 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: ?

Platforms tested: F17/F21

Flag Day: no

Doc impact: no
---
 dirsrvtests/cmd/dsadm/dsadm.py        | 551 ++++++++++++++++++++++++++++++++++
 ldap/admin/src/scripts/DSCreate.pm.in |  10 +-
 2 files changed, 556 insertions(+), 5 deletions(-)
 create mode 100755 dirsrvtests/cmd/dsadm/dsadm.py

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 <tbordaz@redhat.com>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+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;
-- 
1.7.11.7

