diff options
Diffstat (limited to 'ipa-server/ipaserver')
-rw-r--r-- | ipa-server/ipaserver/Makefile.am | 2 | ||||
-rw-r--r-- | ipa-server/ipaserver/dsinstance.py | 112 | ||||
-rw-r--r-- | ipa-server/ipaserver/httpinstance.py | 9 | ||||
-rw-r--r-- | ipa-server/ipaserver/installutils.py | 108 | ||||
-rw-r--r-- | ipa-server/ipaserver/ipaldap.py | 118 | ||||
-rw-r--r-- | ipa-server/ipaserver/krbinstance.py | 228 | ||||
-rw-r--r-- | ipa-server/ipaserver/radiusinstance.py | 4 | ||||
-rw-r--r-- | ipa-server/ipaserver/replication.py | 316 |
8 files changed, 809 insertions, 88 deletions
diff --git a/ipa-server/ipaserver/Makefile.am b/ipa-server/ipaserver/Makefile.am index 25b85687..c2d164b9 100644 --- a/ipa-server/ipaserver/Makefile.am +++ b/ipa-server/ipaserver/Makefile.am @@ -12,6 +12,8 @@ app_PYTHON = \ radiusinstance.py \ webguiinstance.py \ service.py \ + installutils.py \ + replication.py \ $(NULL) EXTRA_DIST = \ diff --git a/ipa-server/ipaserver/dsinstance.py b/ipa-server/ipaserver/dsinstance.py index ce3c154f..79a57182 100644 --- a/ipa-server/ipaserver/dsinstance.py +++ b/ipa-server/ipaserver/dsinstance.py @@ -24,10 +24,14 @@ import tempfile import shutil import logging import pwd +import glob +import sys from ipa.ipautil import * import service +import installutils + SERVER_ROOT_64 = "/usr/lib64/dirsrv" SERVER_ROOT_32 = "/usr/lib/dirsrv" @@ -46,6 +50,61 @@ def find_server_root(): else: return SERVER_ROOT_32 +def realm_to_serverid(realm_name): + return "-".join(realm_name.split(".")) + +def config_dirname(realm_name): + return "/etc/dirsrv/slapd-" + realm_to_serverid(realm_name) + "/" + +def schema_dirname(realm_name): + return config_dirname(realm_name) + "/schema/" + +def erase_ds_instance_data(serverid): + try: + shutil.rmtree("/etc/dirsrv/slapd-%s" % serverid) + except: + pass + try: + shutil.rmtree("/var/lib/dirsrv/slapd-%s" % serverid) + except: + pass + try: + shutil.rmtree("/var/lock/dirsrv/slapd-%s" % serverid) + except: + pass + +def check_existing_installation(): + dirs = glob.glob("/etc/dirsrv/slapd-*") + if not dirs: + return + print "" + print "An existing Directory Server has been detected." + yesno = raw_input("Do you wish to remove it and create a new one? [no]: ") + if not yesno or yesno.lower()[0] != "y": + sys.exit(1) + + try: + run(["/sbin/service", "dirsrv", "stop"]) + except: + pass + for d in dirs: + serverid = os.path.basename(d).split("slapd-", 1)[1] + if serverid: + erase_ds_instance_data(serverid) + +def check_ports(): + ds_unsecure = installutils.port_available(389) + ds_secure = installutils.port_available(636) + if not ds_unsecure or not ds_secure: + print "IPA requires ports 389 and 636 for the Directory Server." + print "These are currently in use:" + if not ds_unsecure: + print "\t389" + if not ds_secure: + print "\t636" + sys.exit(1) + + INF_TEMPLATE = """ [General] FullMachineName= $FQHN @@ -69,20 +128,25 @@ class DsInstance(service.Service): self.dm_password = None self.sub_dict = None - def create_instance(self, ds_user, realm_name, host_name, dm_password): + def create_instance(self, ds_user, realm_name, host_name, dm_password, ro_replica=False): self.ds_user = ds_user self.realm_name = realm_name.upper() - self.serverid = "-".join(self.realm_name.split(".")) + self.serverid = realm_to_serverid(self.realm_name) self.suffix = realm_to_suffix(self.realm_name) self.host_name = host_name self.dm_password = dm_password self.__setup_sub_dict() + + if ro_replica: + self.start_creation(15, "Configuring directory server:") + else: + self.start_creation(15, "Configuring directory server:") - self.start_creation(14, "Configuring directory server:") self.__create_ds_user() self.__create_instance() self.__add_default_schemas() - self.__add_memberof_module() + if not ro_replica: + self.__add_memberof_module() self.__add_referint_module() self.__add_dna_module() self.__create_indeces() @@ -94,9 +158,11 @@ class DsInstance(service.Service): except: # TODO: roll back here? logging.critical("Failed to restart the ds instance") - self.__config_uidgid_gen_first_master() self.__add_default_layout() - self.__add_master_entry_first_master() + if not ro_replica: + self.__config_uidgid_gen_first_master() + self.__add_master_entry_first_master() + self.__init_memberof() self.step("configuring directoy to start on boot") @@ -104,18 +170,10 @@ class DsInstance(service.Service): self.done_creation() - def config_dirname(self): - if not self.serverid: - raise RuntimeError("serverid not set") - return "/etc/dirsrv/slapd-" + self.serverid + "/" - - def schema_dirname(self): - return self.config_dirname() + "/schema/" - def __setup_sub_dict(self): server_root = find_server_root() self.sub_dict = dict(FQHN=self.host_name, SERVERID=self.serverid, - PASSWORD=self.dm_password, SUFFIX=self.suffix, + PASSWORD=self.dm_password, SUFFIX=self.suffix.lower(), REALM=self.realm_name, USER=self.ds_user, SERVER_ROOT=server_root) @@ -161,11 +219,13 @@ class DsInstance(service.Service): def __add_default_schemas(self): self.step("adding default schema") shutil.copyfile(SHARE_DIR + "60kerberos.ldif", - self.schema_dirname() + "60kerberos.ldif") + schema_dirname(self.realm_name) + "60kerberos.ldif") shutil.copyfile(SHARE_DIR + "60samba.ldif", - self.schema_dirname() + "60samba.ldif") + schema_dirname(self.realm_name) + "60samba.ldif") shutil.copyfile(SHARE_DIR + "60radius.ldif", - self.schema_dirname() + "60radius.ldif") + schema_dirname(self.realm_name) + "60radius.ldif") + shutil.copyfile(SHARE_DIR + "60ipaconfig.ldif", + schema_dirname(self.realm_name) + "60ipaconfig.ldif") def __add_memberof_module(self): self.step("enabling memboerof plugin") @@ -177,6 +237,16 @@ class DsInstance(service.Service): logging.critical("Failed to load memberof-conf.ldif: %s" % str(e)) memberof_fd.close() + def __init_memberof(self): + self.step("initializing group membership") + memberof_txt = template_file(SHARE_DIR + "memberof-task.ldif", self.sub_dict) + memberof_fd = write_tmp_file(memberof_txt) + try: + ldap_mod(memberof_fd, "cn=Directory Manager", self.dm_password) + except subprocess.CalledProcessError, e: + logging.critical("Failed to load memberof-conf.ldif: %s" % str(e)) + memberof_fd.close() + def __add_referint_module(self): self.step("enabling referential integrity plugin") referint_txt = template_file(SHARE_DIR + "referint-conf.ldif", self.sub_dict) @@ -219,7 +289,7 @@ class DsInstance(service.Service): def __enable_ssl(self): self.step("configuring ssl for ds instance") - dirname = self.config_dirname() + dirname = config_dirname(self.realm_name) args = ["/usr/share/ipa/ipa-server-setupssl", self.dm_password, dirname, self.host_name] try: @@ -257,7 +327,7 @@ class DsInstance(service.Service): def __certmap_conf(self): self.step("configuring certmap.conf") - dirname = self.config_dirname() + dirname = config_dirname(self.realm_name) certmap_conf = template_file(SHARE_DIR+"certmap.conf.template", self.sub_dict) certmap_fd = open(dirname+"certmap.conf", "w+") certmap_fd.write(certmap_conf) @@ -265,7 +335,7 @@ class DsInstance(service.Service): def change_admin_password(self, password): logging.debug("Changing admin password") - dirname = self.config_dirname() + dirname = config_dirname(self.realm_name) if dir_exists("/usr/lib64/mozldap"): app = "/usr/lib64/mozldap/ldappasswd" else: diff --git a/ipa-server/ipaserver/httpinstance.py b/ipa-server/ipaserver/httpinstance.py index 0433025b..60d33eed 100644 --- a/ipa-server/ipaserver/httpinstance.py +++ b/ipa-server/ipaserver/httpinstance.py @@ -50,13 +50,17 @@ def update_file(filename, orig, subst): else: sys.stdout.write(p.sub(subst, line)) fileinput.close() + return 0 + else: + print "File %s doesn't exist." % filename + return 1 class HTTPInstance(service.Service): def __init__(self): service.Service.__init__(self, "httpd") def create_instance(self, realm, fqdn): - self.sub_dict = { "REALM" : realm } + self.sub_dict = { "REALM" : realm, "FQDN": fqdn } self.fqdn = fqdn self.realm = realm @@ -137,4 +141,5 @@ class HTTPInstance(service.Service): def __set_mod_nss_port(self): self.step("Setting mod_nss port to 443") - update_file(NSS_CONF, '8443', '443') + if update_file(NSS_CONF, '8443', '443') != 0: + print "Updating %s failed." % NSS_CONF diff --git a/ipa-server/ipaserver/installutils.py b/ipa-server/ipaserver/installutils.py new file mode 100644 index 00000000..a403e815 --- /dev/null +++ b/ipa-server/ipaserver/installutils.py @@ -0,0 +1,108 @@ +# Authors: Simo Sorce <ssorce@redhat.com> +# +# Copyright (C) 2007 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; version 2 or later +# +# 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, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +import logging +import socket +import errno +import getpass + +def get_fqdn(): + fqdn = "" + try: + fqdn = socket.getfqdn() + except: + try: + fqdn = socket.gethostname() + except: + fqdn = "" + return fqdn + +def verify_fqdn(host_name): + if len(host_name.split(".")) < 2 or host_name == "localhost.localdomain": + raise RuntimeError("Invalid hostname: " + host_name) + +def port_available(port): + """Try to bind to a port on the wildcard host + Return 1 if the port is available + Return 0 if the port is in use + """ + rv = 1 + + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(('', port)) + s.shutdown(0) + s.close() + except socket.error, e: + if e[0] == errno.EADDRINUSE: + rv = 0 + + if rv: + try: + s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(('', port)) + s.shutdown(0) + s.close() + except socket.error, e: + if e[0] == errno.EADDRINUSE: + rv = 0 + + return rv + +def standard_logging_setup(log_filename, debug=False): + # Always log everything (i.e., DEBUG) to the log + # file. + logging.basicConfig(level=logging.DEBUG, + format='%(asctime)s %(levelname)s %(message)s', + filename=log_filename, + filemode='w') + + console = logging.StreamHandler() + # If the debug option is set, also log debug messages to the console + if debug: + console.setLevel(logging.DEBUG) + else: + # Otherwise, log critical and error messages + console.setLevel(logging.ERROR) + formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s') + console.setFormatter(formatter) + logging.getLogger('').addHandler(console) + +def read_password(user): + correct = False + pwd = "" + while not correct: + pwd = getpass.getpass(user + " password: ") + if not pwd: + continue + if len(pwd) < 8: + print "Password must be at least 8 characters long" + continue + pwd_confirm = getpass.getpass("Password (confirm): ") + if pwd != pwd_confirm: + print "Password mismatch!" + print "" + else: + correct = True + print "" + return pwd + + diff --git a/ipa-server/ipaserver/ipaldap.py b/ipa-server/ipaserver/ipaldap.py index 69f040eb..ef8becec 100644 --- a/ipa-server/ipaserver/ipaldap.py +++ b/ipa-server/ipaserver/ipaldap.py @@ -176,25 +176,90 @@ def wrapper(f,name): return f(*args, **kargs) return inner +class LDIFConn(ldif.LDIFParser): + def __init__( + self, + input_file, + ignored_attr_types=None,max_entries=0,process_url_schemes=None + ): + """ + See LDIFParser.__init__() + + Additional Parameters: + all_records + List instance for storing parsed records + """ + self.dndict = {} # maps dn to Entry + self.dnlist = [] # contains entries in order read + myfile = input_file + if isinstance(input_file,str) or isinstance(input_file,unicode): + myfile = open(input_file, "r") + ldif.LDIFParser.__init__(self,myfile,ignored_attr_types,max_entries,process_url_schemes) + self.parse() + if isinstance(input_file,str) or isinstance(input_file,unicode): + myfile.close() + + def handle(self,dn,entry): + """ + Append single record to dictionary of all records. + """ + if not dn: + dn = '' + newentry = Entry((dn, entry)) + self.dndict[IPAdmin.normalizeDN(dn)] = newentry + self.dnlist.append(newentry) + + def get(self,dn): + ndn = IPAdmin.normalizeDN(dn) + return self.dndict.get(ndn, Entry(None)) + class IPAdmin(SimpleLDAPObject): CFGSUFFIX = "o=NetscapeRoot" DEFAULT_USER_ID = "nobody" + + def getDseAttr(self,attrname): + conffile = self.confdir + '/dse.ldif' + dseldif = LDIFConn(conffile) + cnconfig = dseldif.get("cn=config") + if cnconfig: + return cnconfig.getValue(attrname) + return None def __initPart2(self): if self.binddn and len(self.binddn) and not hasattr(self,'sroot'): try: ent = self.getEntry('cn=config', ldap.SCOPE_BASE, '(objectclass=*)', - [ 'nsslapd-instancedir', 'nsslapd-errorlog' ]) - instdir = ent.getValue('nsslapd-instancedir') - self.sroot, self.inst = re.match(r'(.*)[\/]slapd-(\w+)$', instdir).groups() + [ 'nsslapd-instancedir', 'nsslapd-errorlog', + 'nsslapd-certdir', 'nsslapd-schemadir' ]) self.errlog = ent.getValue('nsslapd-errorlog') - except (ldap.INSUFFICIENT_ACCESS, ldap.CONNECT_ERROR, - ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND)): + self.confdir = None + if self.isLocal: + self.confdir = ent.getValue('nsslapd-certdir') + if not self.confdir or not os.access(self.confdir + '/dse.ldif', os.R_OK): + self.confdir = ent.getValue('nsslapd-schemadir') + if self.confdir: + self.confdir = os.path.dirname(self.confdir) + instdir = ent.getValue('nsslapd-instancedir') + if not instdir: + # get instance name from errorlog + self.inst = re.match(r'(.*)[\/]slapd-([\w-]+)/errors', self.errlog).group(2) + if self.confdir: + instdir = self.getDseAttr('nsslapd-instancedir') + else: + if self.isLocal: + print instdir + self.sroot, self.inst = re.match(r'(.*)[\/]slapd-([\w-]+)$', instdir).groups() + instdir = re.match(r'(.*/slapd-.*)/errors', self.errlog).group(1) + #self.sroot, self.inst = re.match(r'(.*)[\/]slapd-([\w-]+)$', instdir).groups() + ent = self.getEntry('cn=config,cn=ldbm database,cn=plugins,cn=config', + ldap.SCOPE_BASE, '(objectclass=*)', + [ 'nsslapd-directory' ]) + self.dbdir = os.path.dirname(ent.getValue('nsslapd-directory')) + except (ldap.INSUFFICIENT_ACCESS, ldap.CONNECT_ERROR): pass # usually means -# print "ignored exception" except ldap.LDAPError, e: print "caught exception ", e - raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, None, e) + raise def __localinit__(self): """If a CA certificate is provided then it is assumed that we are @@ -209,7 +274,7 @@ class IPAdmin(SimpleLDAPObject): else: SimpleLDAPObject.__init__(self,'ldap://%s:%d' % (self.host,self.port)) - def __init__(self,host,port,cacert,bindcert,bindkey,proxydn=None,debug=None): + def __init__(self,host,port=389,cacert=None,bindcert=None,bindkey=None,proxydn=None,debug=None): """We just set our instance variables and wrap the methods - the real work is done in __localinit__ and __initPart2 - these are separated out this way so that we can call them from places other than @@ -223,7 +288,7 @@ class IPAdmin(SimpleLDAPObject): ldap.set_option(ldap.OPT_X_TLS_KEYFILE,bindkey) self.__wrapmethods() - self.port = port or 389 + self.port = port self.host = host self.cacert = cacert self.bindcert = bindcert @@ -272,6 +337,12 @@ class IPAdmin(SimpleLDAPObject): self.principal = principal self.proxydn = None + def do_simple_bind(self, binddn="cn=directory manager", bindpw=""): + self.binddn = binddn + self.bindpwd = bindpw + self.simple_bind_s(binddn, bindpw) + self.__initPart2() + def getEntry(self,*args): """This wraps the search function. It is common to just get one entry""" @@ -283,8 +354,9 @@ class IPAdmin(SimpleLDAPObject): try: res = self.search(*args) type, obj = self.result(res) - - # res = self.search_ext(args[0], args[1], filterstr=args[2], attrlist=args[3], serverctrls=sctrl) + except ldap.NO_SUCH_OBJECT: + raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND, + "no such entry for " + str(args)) except ldap.LDAPError, e: raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, None, e) @@ -377,6 +449,23 @@ class IPAdmin(SimpleLDAPObject): raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, None, e) return "Success" + def updateRDN(self, dn, newrdn): + """Wrap the modrdn function.""" + + sctrl = self.__get_server_controls__() + + if dn == newrdn: + # no need to report an error + return "Success" + + try: + if sctrl is not None: + self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) + self.modrdn_s(dn, newrdn, delold=1) + except ldap.LDAPError, e: + raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, None, e) + return "Success" + def updateEntry(self,dn,olduser,newuser): """This wraps the mod function. It assumes that the entry is already populated with all of the desired objectclasses and attributes""" @@ -521,7 +610,7 @@ class IPAdmin(SimpleLDAPObject): print "Export task %s for file %s completed successfully" % (cn,file) return rc - def waitForEntry(self, dn, timeout=7200, attr='', quiet=False): + def waitForEntry(self, dn, timeout=7200, attr='', quiet=True): scope = ldap.SCOPE_BASE filter = "(objectclass=*)" attrlist = [] @@ -543,7 +632,8 @@ class IPAdmin(SimpleLDAPObject): entry = self.getEntry(dn, scope, filter, attrlist) except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): pass # found entry, but no attr - except ldap.NO_SUCH_OBJECT: pass # no entry yet + except ldap.NO_SUCH_OBJECT: + pass # no entry yet except ldap.LDAPError, e: # badness print "\nError reading entry", dn, e break @@ -557,7 +647,7 @@ class IPAdmin(SimpleLDAPObject): print "\nwaitForEntry timeout for %s for %s" % (self,dn) elif entry and not quiet: print "\nThe waited for entry is:", entry - else: + elif not entry: print "\nError: could not read entry %s from %s" % (dn,self) return entry diff --git a/ipa-server/ipaserver/krbinstance.py b/ipa-server/ipaserver/krbinstance.py index c4ebde50..c83002f7 100644 --- a/ipa-server/ipaserver/krbinstance.py +++ b/ipa-server/ipaserver/krbinstance.py @@ -26,29 +26,32 @@ import logging import fileinput import re import sys -from random import Random -from time import gmtime import os import pwd import socket import time +import shutil import service from ipa.ipautil import * +from ipa import ipaerror + +import ipaldap + +import ldap +from ldap import LDAPError +from ldap import ldapobject + +from pyasn1.type import univ, namedtype +import pyasn1.codec.ber.encoder +import pyasn1.codec.ber.decoder +import struct +import base64 def host_to_domain(fqdn): s = fqdn.split(".") return ".".join(s[1:]) -def generate_kdc_password(): - rndpwd = '' - r = Random() - r.seed(gmtime()) - for x in range(12): -# rndpwd += chr(r.randint(32,126)) - rndpwd += chr(r.randint(65,90)) #stricter set for testing - return rndpwd - def ldap_mod(fd, dn, pwd): args = ["/usr/bin/ldapmodify", "-h", "127.0.0.1", "-xv", "-D", dn, "-w", pwd, "-f", fd.name] run(args) @@ -79,18 +82,26 @@ class KrbInstance(service.Service): self.kdc_password = None self.sub_dict = None - def create_instance(self, ds_user, realm_name, host_name, admin_password, master_password): + def __common_setup(self, ds_user, realm_name, host_name, admin_password): self.ds_user = ds_user - self.fqdn = host_name - self.ip = socket.gethostbyname(host_name) + self.fqdn = host_name self.realm = realm_name.upper() self.host = host_name.split(".")[0] - self.domain = host_to_domain(host_name) - self.admin_password = admin_password - self.master_password = master_password - + self.ip = socket.gethostbyname(host_name) + self.domain = host_to_domain(host_name) self.suffix = realm_to_suffix(self.realm) - self.kdc_password = generate_kdc_password() + self.kdc_password = ipa_generate_password() + self.admin_password = admin_password + + self.__setup_sub_dict() + + # get a connection to the DS + try: + self.conn = ipaldap.IPAdmin(self.fqdn) + self.conn.do_simple_bind(bindpw=self.admin_password) + except ipaerror.exception_for(ipaerror.LDAP_DATABASE_ERROR), e: + logging.critical("Could not connect to DS") + raise e try: self.stop() @@ -98,22 +109,7 @@ class KrbInstance(service.Service): # It could have been not running pass - self.start_creation(10, "Configuring Kerberos KDC") - - self.__configure_kdc_account_password() - - self.__setup_sub_dict() - - self.__configure_ldap() - - self.__create_instance() - - self.__create_ds_keytab() - - self.__export_kadmin_changepw_keytab() - - self.__add_pwd_extop_module() - + def __common_post_setup(self): try: self.step("starting the KDC") self.start() @@ -129,8 +125,49 @@ class KrbInstance(service.Service): self.step("starting ipa-kpasswd") service.start("ipa-kpasswd") + + def create_instance(self, ds_user, realm_name, host_name, admin_password, master_password): + self.master_password = master_password + + self.__common_setup(ds_user, realm_name, host_name, admin_password) + + self.start_creation(11, "Configuring Kerberos KDC") + + self.__configure_kdc_account_password() + self.__configure_sasl_mappings() + self.__add_krb_entries() + self.__create_instance() + self.__create_ds_keytab() + self.__export_kadmin_changepw_keytab() + self.__add_pwd_extop_module() + + self.__common_post_setup() + + self.done_creation() + + + def create_replica(self, ds_user, realm_name, host_name, admin_password, ldap_passwd_filename): + + self.__common_setup(ds_user, realm_name, host_name, admin_password) + + self.start_creation(9, "Configuring Kerberos KDC") + self.__copy_ldap_passwd(ldap_passwd_filename) + self.__configure_sasl_mappings() + self.__write_stash_from_ds() + self.__create_instance(replica=True) + self.__create_ds_keytab() + self.__export_kadmin_changepw_keytab() + + self.__common_post_setup() + self.done_creation() + + def __copy_ldap_passwd(self, filename): + shutil.copy(filename, "/var/kerberos/krb5kdc/ldappwd") + os.chmod("/var/kerberos/krb5kdc/ldappwd", 0600) + + def __configure_kdc_account_password(self): self.step("setting KDC account password") hexpwd = '' @@ -139,6 +176,7 @@ class KrbInstance(service.Service): pwd_fd = open("/var/kerberos/krb5kdc/ldappwd", "w") pwd_fd.write("uid=kdc,cn=sysaccounts,cn=etc,"+self.suffix+"#{HEX}"+hexpwd+"\n") pwd_fd.close() + os.chmod("/var/kerberos/krb5kdc/ldappwd", 0600) def __setup_sub_dict(self): self.sub_dict = dict(FQDN=self.fqdn, @@ -149,9 +187,60 @@ class KrbInstance(service.Service): HOST=self.host, REALM=self.realm) - def __configure_ldap(self): - self.step("adding kerberos configuration to the directory") - #TODO: test that the ldif is ok with any random charcter we may use in the password + def __configure_sasl_mappings(self): + self.step("adding sasl mappings to the directory") + # we need to remove any existing SASL mappings in the directory as otherwise they + # they may conflict. There is no way to define the order they are used in atm. + + # FIXME: for some reason IPAdmin dies here, so we switch + # it out for a regular ldapobject. + conn = self.conn + self.conn = ldapobject.SimpleLDAPObject("ldap://127.0.0.1/") + self.conn.bind("cn=directory manager", self.admin_password) + try: + msgid = self.conn.search("cn=mapping,cn=sasl,cn=config", ldap.SCOPE_ONELEVEL, "(objectclass=nsSaslMapping)") + res = self.conn.result(msgid) + for r in res[1]: + mid = self.conn.delete_s(r[0]) + #except LDAPError, e: + # logging.critical("Error during SASL mapping removal: %s" % str(e)) + except Exception, e: + print type(e) + print dir(e) + raise e + + self.conn = conn + + entry = ipaldap.Entry("cn=Full Principal,cn=mapping,cn=sasl,cn=config") + entry.setValues("objectclass", "top", "nsSaslMapping") + entry.setValues("cn", "Full Principal") + entry.setValues("nsSaslMapRegexString", '\(.*\)@\(.*\)') + entry.setValues("nsSaslMapBaseDNTemplate", self.suffix) + entry.setValues("nsSaslMapFilterTemplate", '(krbPrincipalName=\\1@\\2)') + + try: + self.conn.add_s(entry) + except ldap.ALREADY_EXISTS: + logging.critical("failed to add Full Principal Sasl mapping") + raise e + + entry = ipaldap.Entry("cn=Name Only,cn=mapping,cn=sasl,cn=config") + entry.setValues("objectclass", "top", "nsSaslMapping") + entry.setValues("cn", "Name Only") + entry.setValues("nsSaslMapRegexString", '\(.*\)') + entry.setValues("nsSaslMapBaseDNTemplate", self.suffix) + entry.setValues("nsSaslMapFilterTemplate", '(krbPrincipalName=\\1@%s)' % self.realm) + + try: + self.conn.add_s(entry) + except ldap.ALREADY_EXISTS: + logging.critical("failed to add Name Only Sasl mapping") + raise e + + def __add_krb_entries(self): + self.step("adding kerberos entries to the DS") + + #TODO: test that the ldif is ok with any random charcter we may use in the password kerberos_txt = template_file(SHARE_DIR + "kerberos.ldif", self.sub_dict) kerberos_fd = write_tmp_file(kerberos_txt) try: @@ -169,7 +258,7 @@ class KrbInstance(service.Service): logging.critical("Failed to load default-aci.ldif: %s" % str(e)) aci_fd.close() - def __create_instance(self): + def __create_instance(self, replica=False): self.step("configuring KDC") kdc_conf = template_file(SHARE_DIR+"kdc.conf.template", self.sub_dict) kdc_fd = open("/var/kerberos/krb5kdc/kdc.conf", "w+") @@ -197,12 +286,34 @@ class KrbInstance(service.Service): krb_fd.write(krb_realm) krb_fd.close() - #populate the directory with the realm structure - args = ["/usr/kerberos/sbin/kdb5_ldap_util", "-D", "uid=kdc,cn=sysaccounts,cn=etc,"+self.suffix, "-w", self.kdc_password, "create", "-s", "-P", self.master_password, "-r", self.realm, "-subtrees", self.suffix, "-sscope", "sub"] + if not replica: + #populate the directory with the realm structure + args = ["/usr/kerberos/sbin/kdb5_ldap_util", "-D", "uid=kdc,cn=sysaccounts,cn=etc,"+self.suffix, "-w", self.kdc_password, "create", "-s", "-P", self.master_password, "-r", self.realm, "-subtrees", self.suffix, "-sscope", "sub"] + try: + run(args) + except subprocess.CalledProcessError, e: + print "Failed to populate the realm structure in kerberos", e + + def __write_stash_from_ds(self): + self.step("writing stash file from DS") try: - run(args) - except subprocess.CalledProcessError, e: - print "Failed to populate the realm structure in kerberos", e + entry = self.conn.getEntry("cn=%s, cn=kerberos, %s" % (self.realm, self.suffix), ldap.SCOPE_SUBTREE) + except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND), e: + logging.critical("Could not find master key in DS") + raise e + + krbMKey = pyasn1.codec.ber.decoder.decode(entry.krbmkey) + keytype = int(krbMKey[0][1][0]) + keydata = str(krbMKey[0][1][1]) + + format = '=hi%ss' % len(keydata) + s = struct.pack(format, keytype, len(keydata), keydata) + try: + fd = open("/var/kerberos/krb5kdc/.k5."+self.realm, "w") + fd.write(s) + except os.error, e: + logging.critical("failed to write stash file") + raise e #add the password extop module def __add_pwd_extop_module(self): @@ -215,12 +326,31 @@ class KrbInstance(service.Service): logging.critical("Failed to load pwd-extop-conf.ldif: %s" % str(e)) extop_fd.close() - #add an ACL to let the DS user read the master key - args = ["/usr/bin/setfacl", "-m", "u:"+self.ds_user+":r", "/var/kerberos/krb5kdc/.k5."+self.realm] + #get the Master Key from the stash file try: - run(args) - except subprocess.CalledProcessError, e: - logging.critical("Failed to set the ACL on the master key: %s" % str(e)) + stash = open("/var/kerberos/krb5kdc/.k5."+self.realm, "r") + keytype = struct.unpack('h', stash.read(2))[0] + keylen = struct.unpack('i', stash.read(4))[0] + keydata = stash.read(keylen) + except os.error: + logging.critical("Failed to retrieve Master Key from Stash file: %s") + #encode it in the asn.1 attribute + MasterKey = univ.Sequence() + MasterKey.setComponentByPosition(0, univ.Integer(keytype)) + MasterKey.setComponentByPosition(1, univ.OctetString(keydata)) + krbMKey = univ.Sequence() + krbMKey.setComponentByPosition(0, univ.Integer(0)) #we have no kvno + krbMKey.setComponentByPosition(1, MasterKey) + asn1key = pyasn1.codec.ber.encoder.encode(krbMKey) + + entry = ipaldap.Entry("cn="+self.realm+",cn=kerberos,"+self.suffix) + dn = "cn="+self.realm+",cn=kerberos,"+self.suffix + mod = [(ldap.MOD_ADD, 'krbMKey', str(asn1key))] + try: + self.conn.modify_s(dn, mod) + except ldap.TYPE_OR_VALUE_EXISTS, e: + logging.critical("failed to add master key to kerberos database\n") + raise e def __create_ds_keytab(self): self.step("creating a keytab for the directory") diff --git a/ipa-server/ipaserver/radiusinstance.py b/ipa-server/ipaserver/radiusinstance.py index dd14bf20..3b89018f 100644 --- a/ipa-server/ipaserver/radiusinstance.py +++ b/ipa-server/ipaserver/radiusinstance.py @@ -26,6 +26,7 @@ import shutil import logging import pwd import time +import sys from ipa.ipautil import * from ipa import radius_util @@ -147,8 +148,7 @@ class RadiusInstance(service.Service): retry += 1 if retry > 15: print "Error timed out waiting for kadmin to finish operations\n" - sys.exit() - + sys.exit(1) try: pent = pwd.getpwnam(radius_util.RADIUS_USER) os.chown(radius_util.RADIUS_IPA_KEYTAB_FILEPATH, pent.pw_uid, pent.pw_gid) diff --git a/ipa-server/ipaserver/replication.py b/ipa-server/ipaserver/replication.py new file mode 100644 index 00000000..580ec27b --- /dev/null +++ b/ipa-server/ipaserver/replication.py @@ -0,0 +1,316 @@ +# Authors: Karl MacMillan <kmacmillan@mentalrootkit.com> +# +# Copyright (C) 2007 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; version 2 or later +# +# 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, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +import time, logging + +import ipaldap, ldap, dsinstance +from ipa import ipaerror + +DIRMAN_CN = "cn=directory manager" +PORT = 389 +TIMEOUT = 120 + +class ReplicationManager: + """Manage replicatin agreements between DS servers""" + def __init__(self, hostname, dirman_passwd): + self.hostname = hostname + self.dirman_passwd = dirman_passwd + self.conn = ipaldap.IPAdmin(hostname) + self.conn.do_simple_bind(bindpw=dirman_passwd) + + self.repl_man_passwd = dirman_passwd + + # these are likely constant, but you could change them + # at runtime if you really want + self.repl_man_dn = "cn=replication manager,cn=config" + self.repl_man_cn = "replication manager" + self.suffix = "" + + def find_replication_dns(self, conn): + filt = "(objectlcass=nsds5ReplicationAgreement)" + try: + ents = conn.search_s("cn=mapping tree,cn-config", ldap.SCOPE_SUBTREE, filt, ["cn"]) + except ldap.NO_SUCH_OBJECT: + return [] + return [ent.dn for ent in ents] + + def add_replication_manager(self, conn, passwd=None): + """ + Create a pseudo user to use for replication. If no password + is provided the directory manager password will be used. + """ + + if passwd: + self.repl_man_passwd = passwd + + ent = ipaldap.Entry(self.repl_man_dn) + ent.setValues("objectclass", "top", "person") + ent.setValues("cn", self.repl_man_cn) + ent.setValues("userpassword", self.repl_man_passwd) + ent.setValues("sn", "replication manager pseudo user") + + try: + conn.add_s(ent) + except ldap.ALREADY_EXISTS: + # should we set the password here? + pass + + def delete_replication_manager(self, conn, dn="cn=replication manager,cn=config"): + try: + conn.delete_s(dn) + except ldap.NO_SUCH_OBJECT: + pass + + def get_replica_type(self, master): + if master: + return "3" + else: + return "2" + + def replica_dn(self): + return 'cn=replica, cn="%s", cn=mapping tree, cn=config' % self.suffix + + + def local_replica_config(self, conn, master, replica_id): + dn = self.replica_dn() + + try: + conn.getEntry(dn, ldap.SCOPE_BASE) + # replication is already configured + return + except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): + pass + + replica_type = self.get_replica_type(master) + + entry = ipaldap.Entry(dn) + entry.setValues('objectclass', "top", "nsds5replica", "extensibleobject") + entry.setValues('cn', "replica") + entry.setValues('nsds5replicaroot', self.suffix) + entry.setValues('nsds5replicaid', str(replica_id)) + entry.setValues('nsds5replicatype', replica_type) + entry.setValues('nsds5flags', "1") + entry.setValues('nsds5replicabinddn', [self.repl_man_dn]) + entry.setValues('nsds5replicalegacyconsumer', "off") + + conn.add_s(entry) + + def setup_changelog(self, conn): + dn = "cn=changelog5, cn=config" + dirpath = conn.dbdir + "/cldb" + entry = ipaldap.Entry(dn) + entry.setValues('objectclass', "top", "extensibleobject") + entry.setValues('cn', "changelog5") + entry.setValues('nsslapd-changelogdir', dirpath) + try: + conn.add_s(entry) + except ldap.ALREADY_EXISTS: + return + + def setup_chaining_backend(self, conn): + chaindn = "cn=chaining database, cn=plugins, cn=config" + benamebase = "chaindb" + urls = [self.to_ldap_url(conn)] + cn = "" + benum = 1 + done = False + while not done: + try: + cn = benamebase + str(benum) # e.g. localdb1 + dn = "cn=" + cn + ", " + chaindn + entry = ipaldap.Entry(dn) + entry.setValues('objectclass', 'top', 'extensibleObject', 'nsBackendInstance') + entry.setValues('cn', cn) + entry.setValues('nsslapd-suffix', self.suffix) + entry.setValues('nsfarmserverurl', urls) + entry.setValues('nsmultiplexorbinddn', self.repl_man_dn) + entry.setValues('nsmultiplexorcredentials', self.repl_man_passwd) + + self.conn.add_s(entry) + done = True + except ldap.ALREADY_EXISTS: + benum += 1 + except ldap.LDAPError, e: + print "Could not add backend entry " + dn, e + raise + + return cn + + def to_ldap_url(self, conn): + return "ldap://%s:%d/" % (conn.host, conn.port) + + def setup_chaining_farm(self, conn): + try: + conn.modify_s(self.suffix, [(ldap.MOD_ADD, 'aci', + [ "(targetattr = \"*\")(version 3.0; acl \"Proxied authorization for database links\"; allow (proxy) userdn = \"ldap:///%s\";)" % self.repl_man_dn ])]) + except ldap.TYPE_OR_VALUE_EXISTS: + logging.debug("proxy aci already exists in suffix %s on %s" % (self.suffix, conn.host)) + + def get_mapping_tree_entry(self): + try: + entry = self.conn.getEntry("cn=mapping tree,cn=config", ldap.SCOPE_ONELEVEL, + "(cn=\"%s\")" % (self.suffix)) + except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND), e: + logging.debug("failed to find mappting tree entry for %s" % self.suffix) + raise e + + return entry + + + def enable_chain_on_update(self, bename): + mtent = self.get_mapping_tree_entry() + dn = mtent.dn + + plgent = self.conn.getEntry("cn=Multimaster Replication Plugin,cn=plugins,cn=config", + ldap.SCOPE_BASE, "(objectclass=*)", ['nsslapd-pluginPath']) + path = plgent.getValue('nsslapd-pluginPath') + + mod = [(ldap.MOD_REPLACE, 'nsslapd-state', 'backend'), + (ldap.MOD_ADD, 'nsslapd-backend', bename), + (ldap.MOD_ADD, 'nsslapd-distribution-plugin', path), + (ldap.MOD_ADD, 'nsslapd-distribution-funct', 'repl_chain_on_update')] + + try: + self.conn.modify_s(dn, mod) + except ldap.TYPE_OR_VALUE_EXISTS: + logging.debug("chainOnUpdate already enabled for %s" % self.suffix) + + + def setup_chain_on_update(self, other_conn): + chainbe = self.setup_chaining_backend(other_conn) + self.enable_chain_on_update(chainbe) + + + def agreement_dn(self, conn): + cn = "meTo%s%d" % (conn.host, PORT) + dn = "cn=%s, %s" % (cn, self.replica_dn()) + + return (cn, dn) + + + def setup_agreement(self, a, b): + cn, dn = self.agreement_dn(b) + try: + a.getEntry(dn, ldap.SCOPE_BASE) + return + except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): + pass + + entry = ipaldap.Entry(dn) + entry.setValues('objectclass', "top", "nsds5replicationagreement") + entry.setValues('cn', cn) + entry.setValues('nsds5replicahost', b.host) + entry.setValues('nsds5replicaport', str(PORT)) + entry.setValues('nsds5replicatimeout', str(TIMEOUT)) + entry.setValues('nsds5replicabinddn', self.repl_man_dn) + entry.setValues('nsds5replicacredentials', self.repl_man_passwd) + entry.setValues('nsds5replicabindmethod', 'simple') + entry.setValues('nsds5replicaroot', self.suffix) + entry.setValues('nsds5replicaupdateschedule', '0000-2359 0123456') + entry.setValues('description', "me to %s%d" % (b.host, PORT)) + + a.add_s(entry) + + entry = a.waitForEntry(entry) + + + def check_repl_init(self, conn, agmtdn): + done = False + hasError = 0 + attrlist = ['cn', 'nsds5BeginReplicaRefresh', 'nsds5replicaUpdateInProgress', + 'nsds5ReplicaLastInitStatus', 'nsds5ReplicaLastInitStart', + 'nsds5ReplicaLastInitEnd'] + entry = conn.getEntry(agmtdn, ldap.SCOPE_BASE, "(objectclass=*)", attrlist) + if not entry: + print "Error reading status from agreement", agmtdn + hasError = 1 + else: + refresh = entry.nsds5BeginReplicaRefresh + inprogress = entry.nsds5replicaUpdateInProgress + status = entry.nsds5ReplicaLastInitStatus + if not refresh: # done - check status + if not status: + print "No status yet" + elif status.find("replica busy") > -1: + print "Update failed - replica busy - status", status + done = True + hasError = 2 + elif status.find("Total update succeeded") > -1: + print "Update succeeded" + done = True + elif inprogress.lower() == 'true': + print "Update in progress yet not in progress" + else: + print "Update failed: status", status + hasError = 1 + done = True + else: + print "Update in progress" + + return done, hasError + + + def wait_for_repl_init(self, conn, agmtdn): + done = False + haserror = 0 + while not done and not haserror: + time.sleep(1) # give it a few seconds to get going + done, haserror = self.check_repl_init(conn, agmtdn) + return haserror + + def start_replication(self, other_conn): + print "starting replication" + cn, dn = self.agreement_dn(self.conn) + + mod = [(ldap.MOD_ADD, 'nsds5BeginReplicaRefresh', 'start')] + other_conn.modify_s(dn, mod) + + return self.wait_for_repl_init(other_conn, dn) + + + def basic_replication_setup(self, conn, master, replica_id): + self.add_replication_manager(conn) + self.local_replica_config(conn, master, replica_id) + if master: + self.setup_changelog(conn) + + def setup_replication(self, other_hostname, realm_name, master=True): + """ + NOTES: + - the directory manager password needs to be the same on + both directories. + """ + other_conn = ipaldap.IPAdmin(other_hostname) + other_conn.do_simple_bind(bindpw=self.dirman_passwd) + self.suffix = ipaldap.IPAdmin.normalizeDN(dsinstance.realm_to_suffix(realm_name)) + + self.basic_replication_setup(self.conn, master, 1) + self.basic_replication_setup(other_conn, True, 2) + + self.setup_agreement(other_conn, self.conn) + if master: + self.setup_agreement(self.conn, other_conn) + else: + self.setup_chaining_farm(other_conn) + self.setup_chain_on_update(other_conn) + + return self.start_replication(other_conn) + + + |