Files
freeipa/ipa-server/ipaserver/replication.py
Rob Crittenden 17261c2520 Create a user for Windows PassSync and grant password changing permissions
This does 3 things:
1. Create a user for the Windows PassSync service
2. Add this use to the list of users that can skip password policies
3. Add an aci that grants permission to write the password attributes

471130
2008-11-12 15:52:57 -05:00

488 lines
19 KiB
Python

# 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 only
#
# 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 ldap import modlist
from ipa import ipaerror
DIRMAN_CN = "cn=directory manager"
CACERT="/usr/share/ipa/html/ca.crt"
# the default container used by AD for user entries
WIN_USER_CONTAINER="cn=Users"
# the default container used by IPA for user entries
IPA_USER_CONTAINER="cn=users,cn=accounts"
PORT = 636
TIMEOUT = 120
IPA_REPLICA = 1
WINSYNC = 2
class ReplicationManager:
"""Manage replication agreements between DS servers, and sync
agreements with Windows servers"""
def __init__(self, hostname, dirman_passwd):
self.hostname = hostname
self.dirman_passwd = dirman_passwd
self.conn = ipaldap.IPAdmin(hostname, port=PORT, cacert=CACERT)
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 _get_replica_id(self, conn, master_conn):
"""
Returns the replica ID which is unique for each backend.
conn is the connection we are trying to get the replica ID for.
master_conn is the master we are going to replicate with.
"""
# First see if there is already one set
dn = self.replica_dn()
try:
replica = conn.search_s(dn, ldap.SCOPE_BASE, "objectclass=*")[0]
if replica.getValue('nsDS5ReplicaId'):
return int(replica.getValue('nsDS5ReplicaId'))
except ldap.NO_SUCH_OBJECT:
pass
# Ok, either the entry doesn't exist or the attribute isn't set
# so get it from the other master
retval = -1
dn = "cn=replication, cn=etc, %s" % self.suffix
try:
replica = master_conn.search_s(dn, ldap.SCOPE_BASE, "objectclass=*")[0]
if not replica.getValue('nsDS5ReplicaId'):
logging.debug("Unable to retrieve nsDS5ReplicaId from remote server")
raise RuntimeError("Unable to retrieve nsDS5ReplicaId from remote server")
except ldap.NO_SUCH_OBJECT:
logging.debug("Unable to retrieve nsDS5ReplicaId from remote server")
raise
# Now update the value on the master
retval = int(replica.getValue('nsDS5ReplicaId'))
mod = [(ldap.MOD_REPLACE, 'nsDS5ReplicaId', str(retval + 1))]
try:
master_conn.modify_s(dn, mod)
except Exception, e:
logging.debug("Problem updating nsDS5ReplicaID %s" % e)
raise
return retval
def find_replication_dns(self, conn):
filt = "(|(objectclass=nsDSWindowsReplicationAgreement)(objectclass=nsds5ReplicationAgreement))"
try:
ents = conn.search_s("cn=mapping tree,cn=config", ldap.SCOPE_SUBTREE, filt)
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=True):
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, 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()
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 add_passsync_user(self, conn, password):
pass_dn = "uid=passsync,cn=sysaccounts,cn=etc,%s" % self.suffix
print "The user for the Windows PassSync service is %s" % pass_dn
try:
conn.getEntry(pass_dn, ldap.SCOPE_BASE)
print "Windows PassSync entry exists, not resetting password"
return
except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
pass
# The user doesn't exist, add it
entry = ipaldap.Entry(pass_dn)
entry.setValues("objectclass", ["account", "simplesecurityobject"])
entry.setValues("uid", "passsync")
entry.setValues("userPassword", password)
conn.add_s(entry)
# Add it to the list of users allowed to bypass password policy
extop_dn = "cn=ipa_pwd_extop,cn=plugins,cn=config"
entry = conn.getEntry(extop_dn, ldap.SCOPE_BASE)
pass_mgrs = entry.getValues('passSyncManagersDNs')
if not pass_mgrs:
pass_mgrs = []
if not isinstance(pass_mgrs, list):
pass_mgrs = [pass_mgrs]
pass_mgrs.append(pass_dn)
mod = [(ldap.MOD_REPLACE, 'passSyncManagersDNs', pass_mgrs)]
conn.modify_s(extop_dn, mod)
# And finally grant it permission to write passwords
mod = [(ldap.MOD_ADD, 'aci',
['(targetattr = "userPassword || krbPrincipalKey || sambaLMPassword || sambaNTPassword || passwordHistory")(version 3.0; acl "Windows PassSync service can write passwords"; allow (write) userdn="ldap:///%s";)' % pass_dn])]
try:
conn.modify_s(self.suffix, mod)
except ldap.TYPE_OR_VALUE_EXISTS:
logging.debug("passsync aci already exists in suffix %s on %s" % (self.suffix, conn.host))
def setup_winsync_agmt(self, entry, **kargs):
entry.setValues("objectclass", "nsDSWindowsReplicationAgreement")
entry.setValues("nsds7WindowsReplicaSubtree",
kargs.get("win_subtree",
WIN_USER_CONTAINER + "," + self.suffix))
entry.setValues("nsds7DirectoryReplicaSubtree",
kargs.get("ds_subtree",
IPA_USER_CONTAINER + "," + self.suffix))
# for now, just sync users and ignore groups
entry.setValues("nsds7NewWinUserSyncEnabled", kargs.get('newwinusers', 'true'))
entry.setValues("nsds7NewWinGroupSyncEnabled", kargs.get('newwingroups', 'false'))
windomain = ''
if kargs.has_key('windomain'):
windomain = kargs['windomain']
else:
windomain = '.'.join(ldap.explode_dn(self.suffix, 1))
entry.setValues("nsds7WindowsDomain", windomain)
def agreement_dn(self, hostname, port=PORT):
cn = "meTo%s%d" % (hostname, port)
dn = "cn=%s, %s" % (cn, self.replica_dn())
return (cn, dn)
def setup_agreement(self, a, b, **kargs):
cn, dn = self.agreement_dn(b.host)
try:
a.getEntry(dn, ldap.SCOPE_BASE)
return
except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
pass
iswinsync = kargs.get("winsync", False)
repl_man_dn = kargs.get("binddn", self.repl_man_dn)
repl_man_passwd = kargs.get("bindpw", self.repl_man_passwd)
port = kargs.get("port", PORT)
entry = ipaldap.Entry(dn)
entry.setValues('objectclass', "nsds5replicationagreement")
entry.setValues('cn', cn)
entry.setValues('nsds5replicahost', b.host)
entry.setValues('nsds5replicaport', str(port))
entry.setValues('nsds5replicatimeout', str(TIMEOUT))
entry.setValues('nsds5replicabinddn', repl_man_dn)
entry.setValues('nsds5replicacredentials', repl_man_passwd)
entry.setValues('nsds5replicabindmethod', 'simple')
entry.setValues('nsds5replicaroot', self.suffix)
entry.setValues('nsds5replicaupdateschedule', '0000-2359 0123456')
entry.setValues('nsds5replicatransportinfo', 'SSL')
entry.setValues('nsDS5ReplicatedAttributeList', '(objectclass=*) $ EXCLUDE memberOf')
entry.setValues('description', "me to %s%d" % (b.host, port))
if iswinsync:
self.setup_winsync_agmt(entry, **kargs)
a.add_s(entry)
entry = a.waitForEntry(entry)
def delete_agreement(self, hostname):
cn, dn = self.agreement_dn(hostname)
return self.conn.deleteEntry(dn)
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 "[%s] reports: Replica Busy! Status: [%s]" % (conn.host, 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 "[%s] reports: Update failed! Status: [%s]" % (conn.host, 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, conn=None):
print "Starting replication, please wait until this has completed."
if conn == None:
conn = self.conn
cn, dn = self.agreement_dn(conn.host)
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, replica_id):
self.add_replication_manager(conn)
self.local_replica_config(conn, replica_id)
self.setup_changelog(conn)
def setup_replication(self, other_hostname, realm_name, **kargs):
"""
NOTES:
- the directory manager password needs to be the same on
both directories. Or use the optional binddn and bindpw
"""
iswinsync = kargs.get("winsync", False)
oth_port = kargs.get("port", PORT)
oth_cacert = kargs.get("cacert", CACERT)
oth_binddn = kargs.get("binddn", DIRMAN_CN)
oth_bindpw = kargs.get("bindpw", self.dirman_passwd)
# note - there appears to be a bug in python-ldap - it does not
# allow connections using two different CA certs
other_conn = ipaldap.IPAdmin(other_hostname, port=oth_port, cacert=oth_cacert)
try:
other_conn.do_simple_bind(binddn=oth_binddn, bindpw=oth_bindpw)
except Exception, e:
if iswinsync:
logging.info("Could not validate connection to remote server %s:%d - continuing" %
(other_hostname, oth_port))
logging.info("The error was: %s" % e)
else:
raise e
self.suffix = ipaldap.IPAdmin.normalizeDN(dsinstance.realm_to_suffix(realm_name))
if not iswinsync:
local_id = self._get_replica_id(self.conn, other_conn)
else:
# there is no other side to get a replica ID from
local_id = self._get_replica_id(self.conn, self.conn)
self.basic_replication_setup(self.conn, local_id)
if not iswinsync:
other_id = self._get_replica_id(other_conn, other_conn)
self.basic_replication_setup(other_conn, other_id)
self.setup_agreement(other_conn, self.conn)
self.setup_agreement(self.conn, other_conn)
return self.start_replication(other_conn)
else:
self.add_passsync_user(self.conn, kargs.get("passsync"))
self.setup_agreement(self.conn, other_conn, **kargs)
return self.start_replication(self.conn, other_conn)
def initialize_replication(self, dn, conn):
mod = [(ldap.MOD_ADD, 'nsds5BeginReplicaRefresh', 'start')]
try:
conn.modify_s(dn, mod)
except ldap.ALREADY_EXISTS:
return
def force_synch(self, dn, schedule, conn):
newschedule = '2358-2359 0'
# On the remote chance of a match. We force a synch to happen right
# now by changing the schedule to something else and quickly changing
# it back.
if newschedule == schedule:
newschedule = '2358-2359 1'
logging.info("Changing agreement %s schedule to %s to force synch" %
(dn, newschedule))
mod = [(ldap.MOD_REPLACE, 'nsDS5ReplicaUpdateSchedule', [ newschedule ])]
conn.modify_s(dn, mod)
time.sleep(1)
logging.info("Changing agreement %s to restore original schedule %s" %
(dn, schedule))
mod = [(ldap.MOD_REPLACE, 'nsDS5ReplicaUpdateSchedule', [ schedule ])]
conn.modify_s(dn, mod)
def get_agreement_type(self, hostname):
cn, dn = self.agreement_dn(hostname)
entry = self.conn.getEntry(dn, ldap.SCOPE_BASE)
objectclass = entry.getValues("objectclass")
for o in objectclass:
if o.lower() == "nsdswindowsreplicationagreement":
return WINSYNC
return IPA_REPLICA