0000-12-31 18:09:24 -05:50
|
|
|
# Authors: Rob Crittenden <rcritten@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 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
|
|
|
|
#
|
|
|
|
|
0000-12-31 18:09:24 -05:50
|
|
|
import sys
|
|
|
|
sys.path.append("/usr/share/ipa")
|
|
|
|
|
0000-12-31 18:09:24 -05:50
|
|
|
import ldap
|
0000-12-31 18:09:24 -05:50
|
|
|
import ipaserver.dsinstance
|
|
|
|
import ipaserver.ipaldap
|
0000-12-31 18:09:24 -05:50
|
|
|
import ipaserver.util
|
0000-12-31 18:09:24 -05:50
|
|
|
import xmlrpclib
|
0000-12-31 18:09:24 -05:50
|
|
|
import ipa.config
|
2007-08-22 12:30:51 -05:00
|
|
|
from ipa import ipaerror
|
|
|
|
|
|
|
|
import string
|
|
|
|
from types import *
|
2007-08-13 15:41:38 -05:00
|
|
|
import os
|
2007-08-21 16:26:36 -05:00
|
|
|
import re
|
2007-08-13 15:41:38 -05:00
|
|
|
|
|
|
|
# Need a global to store this between requests
|
|
|
|
_LDAPPool = None
|
|
|
|
|
|
|
|
#
|
|
|
|
# Apache runs in multi-process mode so each process will have its own
|
|
|
|
# connection. This could theoretically drive the total number of connections
|
|
|
|
# very high but since this represents just the administrative interface
|
|
|
|
# this is not anticipated.
|
|
|
|
class IPAConnPool:
|
|
|
|
def __init__(self):
|
|
|
|
self.numentries = 0
|
|
|
|
self.freelist = []
|
|
|
|
|
|
|
|
def getConn(self, host, port, bindca, bindcert, bindkey, proxydn=None):
|
|
|
|
self.numentries = self.numentries + 1
|
|
|
|
if len(self.freelist) > 0:
|
|
|
|
conn = self.freelist.pop()
|
|
|
|
else:
|
|
|
|
conn = ipaserver.ipaldap.IPAdmin(host,port,bindca,bindcert,bindkey)
|
|
|
|
conn.set_proxydn(proxydn)
|
|
|
|
return conn
|
|
|
|
|
|
|
|
def releaseConn(self, conn):
|
|
|
|
self.freelist.append(conn)
|
0000-12-31 18:09:24 -05:50
|
|
|
|
2007-08-06 09:05:53 -05:00
|
|
|
class IPAServer:
|
|
|
|
|
|
|
|
def __init__(self):
|
2007-08-13 15:41:38 -05:00
|
|
|
global _LDAPPool
|
2007-08-06 09:05:53 -05:00
|
|
|
# FIXME, this needs to be auto-discovered
|
|
|
|
self.host = 'localhost'
|
|
|
|
self.port = 636
|
|
|
|
self.bindcert = "/usr/share/ipa/cert.pem"
|
|
|
|
self.bindkey = "/usr/share/ipa/key.pem"
|
|
|
|
self.bindca = "/usr/share/ipa/cacert.asc"
|
|
|
|
|
2007-08-13 15:41:38 -05:00
|
|
|
if _LDAPPool is None:
|
|
|
|
_LDAPPool = IPAConnPool()
|
2007-08-06 09:05:53 -05:00
|
|
|
ipa.config.init_config()
|
|
|
|
self.basedn = ipaserver.util.realm_to_suffix(ipa.config.config.get_realm())
|
|
|
|
self.scope = ldap.SCOPE_SUBTREE
|
|
|
|
self.princ = None
|
|
|
|
|
|
|
|
def set_principal(self, princ):
|
|
|
|
self.princ = princ
|
|
|
|
|
|
|
|
def get_dn_from_principal(self, princ):
|
|
|
|
"""Given a kerberls principal get the LDAP uid"""
|
2007-08-13 15:41:38 -05:00
|
|
|
global _LDAPPool
|
|
|
|
|
|
|
|
# FIXME: should we search for this in a specific area of the tree?
|
2007-08-06 09:05:53 -05:00
|
|
|
filter = "(krbPrincipalName=" + princ + ")"
|
2007-08-22 12:30:51 -05:00
|
|
|
# The only anonymous search we should have
|
|
|
|
m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,None)
|
|
|
|
ent = m1.getEntry(self.basedn, self.scope, filter, ['dn'])
|
|
|
|
_LDAPPool.releaseConn(m1)
|
2007-08-06 09:05:53 -05:00
|
|
|
|
|
|
|
return "dn:" + ent.dn
|
|
|
|
|
|
|
|
def convert_entry(self, ent):
|
|
|
|
|
|
|
|
# Convert to LDIF
|
|
|
|
entry = str(ent)
|
|
|
|
|
|
|
|
# Strip off any junk
|
|
|
|
entry = entry.strip()
|
|
|
|
|
|
|
|
# Don't need to identify binary fields and this breaks the parser so
|
|
|
|
# remove double colons
|
|
|
|
entry = entry.replace('::', ':')
|
|
|
|
specs = [spec.split(':') for spec in entry.split('\n')]
|
|
|
|
|
|
|
|
# Convert into a dict. We need to handle multi-valued attributes as well
|
|
|
|
# so we'll convert those into lists.
|
|
|
|
user={}
|
|
|
|
for (k,v) in specs:
|
|
|
|
k = k.lower()
|
|
|
|
if user.get(k) is not None:
|
|
|
|
if isinstance(user[k],list):
|
|
|
|
user[k].append(v.strip())
|
|
|
|
else:
|
|
|
|
first = user[k]
|
|
|
|
user[k] = []
|
|
|
|
user[k].append(first)
|
|
|
|
user[k].append(v.strip())
|
0000-12-31 18:09:24 -05:50
|
|
|
else:
|
2007-08-06 09:05:53 -05:00
|
|
|
user[k] = v.strip()
|
|
|
|
|
|
|
|
return user
|
|
|
|
|
2007-08-17 09:03:33 -05:00
|
|
|
def get_user (self, args, sattrs=None, opts=None):
|
2007-08-06 09:05:53 -05:00
|
|
|
"""Get a specific user's entry. Return as a dict of values.
|
|
|
|
Multi-valued fields are represented as lists.
|
|
|
|
"""
|
2007-08-13 15:41:38 -05:00
|
|
|
global _LDAPPool
|
2007-08-06 09:05:53 -05:00
|
|
|
ent=""
|
2007-08-17 09:03:33 -05:00
|
|
|
|
|
|
|
# The XML-RPC server marshals the arguments into one variable
|
|
|
|
# while the direct caller has them separate. So do a little
|
|
|
|
# bit of gymnastics to figure things out. There has to be a
|
|
|
|
# better way, so FIXME
|
|
|
|
if isinstance(args,tuple):
|
|
|
|
opts = sattrs
|
|
|
|
if len(args) == 2:
|
|
|
|
username = args[0]
|
|
|
|
sattrs = args[1]
|
|
|
|
else:
|
|
|
|
username = args
|
|
|
|
sattrs = None
|
|
|
|
else:
|
|
|
|
username = args
|
|
|
|
|
2007-08-06 09:05:53 -05:00
|
|
|
if opts:
|
|
|
|
self.set_principal(opts['remoteuser'])
|
|
|
|
if (isinstance(username, tuple)):
|
|
|
|
username = username[0]
|
|
|
|
|
2007-08-22 12:30:51 -05:00
|
|
|
dn = self.get_dn_from_principal(self.princ)
|
2007-08-06 09:05:53 -05:00
|
|
|
|
|
|
|
filter = "(uid=" + username + ")"
|
2007-08-22 12:30:51 -05:00
|
|
|
|
|
|
|
m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,dn)
|
|
|
|
ent = m1.getEntry(self.basedn, self.scope, filter, sattrs)
|
|
|
|
_LDAPPool.releaseConn(m1)
|
2007-08-06 09:05:53 -05:00
|
|
|
|
|
|
|
return self.convert_entry(ent)
|
|
|
|
|
2007-08-17 16:27:54 -05:00
|
|
|
def add_user (self, args, user_container="ou=users,ou=default",opts=None):
|
2007-08-17 09:03:33 -05:00
|
|
|
"""Add a user in LDAP. Takes as input a dict where the key is the
|
|
|
|
attribute name and the value is either a string or in the case
|
|
|
|
of a multi-valued field a list of values. user_container sets
|
|
|
|
where in the tree the user is placed."""
|
2007-08-13 15:41:38 -05:00
|
|
|
global _LDAPPool
|
2007-08-17 16:27:54 -05:00
|
|
|
|
|
|
|
# The XML-RPC server marshals the arguments into one variable
|
|
|
|
# while the direct caller has them separate. So do a little
|
|
|
|
# bit of gymnastics to figure things out. There has to be a
|
|
|
|
# better way, so FIXME
|
|
|
|
if isinstance(args,tuple):
|
|
|
|
opts = user_container
|
|
|
|
if len(args) == 2:
|
|
|
|
user = args[0]
|
|
|
|
user_container = args[1]
|
|
|
|
else:
|
|
|
|
user = args
|
|
|
|
user_container = "ou=users,ou=default"
|
|
|
|
else:
|
|
|
|
user = args
|
|
|
|
|
2007-08-06 09:05:53 -05:00
|
|
|
if (isinstance(user, tuple)):
|
|
|
|
user = user[0]
|
2007-08-17 16:27:54 -05:00
|
|
|
|
2007-08-10 09:30:15 -05:00
|
|
|
dn="uid=%s,%s,%s" % (user['uid'], user_container,self.basedn)
|
2007-08-17 17:32:05 -05:00
|
|
|
entry = ipaserver.ipaldap.Entry(dn)
|
2007-08-06 09:05:53 -05:00
|
|
|
|
|
|
|
# some required objectclasses
|
|
|
|
entry.setValues('objectClass', 'top', 'posixAccount', 'shadowAccount', 'account', 'person', 'inetOrgPerson', 'organizationalPerson', 'krbPrincipalAux', 'krbTicketPolicyAux')
|
|
|
|
|
|
|
|
# Fill in shadow fields
|
|
|
|
entry.setValue('shadowMin', '0')
|
|
|
|
entry.setValue('shadowMax', '99999')
|
|
|
|
entry.setValue('shadowWarning', '7')
|
|
|
|
entry.setValue('shadowExpire', '-1')
|
|
|
|
entry.setValue('shadowInactive', '-1')
|
|
|
|
entry.setValue('shadowFlag', '-1')
|
|
|
|
|
|
|
|
# FIXME: calculate shadowLastChange
|
|
|
|
|
|
|
|
# fill in our new entry with everything sent by the user
|
|
|
|
for u in user:
|
2007-08-17 17:32:05 -05:00
|
|
|
entry.setValues(u, user[u])
|
2007-08-06 09:05:53 -05:00
|
|
|
|
|
|
|
if opts:
|
|
|
|
self.set_principal(opts['remoteuser'])
|
|
|
|
|
2007-08-22 12:30:51 -05:00
|
|
|
dn = self.get_dn_from_principal(self.princ)
|
2007-08-06 09:05:53 -05:00
|
|
|
|
2007-08-22 12:30:51 -05:00
|
|
|
m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,dn)
|
|
|
|
res = m1.addEntry(entry)
|
|
|
|
_LDAPPool.releaseConn(m1)
|
|
|
|
return res
|
2007-08-06 09:05:53 -05:00
|
|
|
|
|
|
|
def get_add_schema (self):
|
|
|
|
"""Get the list of fields to be used when adding users in the GUI."""
|
|
|
|
|
|
|
|
# FIXME: this needs to be pulled from LDAP
|
|
|
|
fields = []
|
|
|
|
|
|
|
|
field1 = {
|
|
|
|
"name": "uid" ,
|
|
|
|
"label": "Login:",
|
|
|
|
"type": "text",
|
|
|
|
"validator": "text",
|
|
|
|
"required": "true"
|
|
|
|
}
|
|
|
|
fields.append(field1)
|
|
|
|
|
|
|
|
field1 = {
|
|
|
|
"name": "givenName" ,
|
|
|
|
"label": "First name:",
|
|
|
|
"type": "text",
|
|
|
|
"validator": "string",
|
|
|
|
"required": "true"
|
|
|
|
}
|
|
|
|
fields.append(field1)
|
|
|
|
|
|
|
|
field1 = {
|
|
|
|
"name": "sn" ,
|
|
|
|
"label": "Last name:",
|
|
|
|
"type": "text",
|
|
|
|
"validator": "string",
|
|
|
|
"required": "true"
|
|
|
|
}
|
|
|
|
fields.append(field1)
|
|
|
|
|
|
|
|
field1 = {
|
|
|
|
"name": "mail" ,
|
|
|
|
"label": "E-mail address:",
|
|
|
|
"type": "text",
|
|
|
|
"validator": "email",
|
|
|
|
"required": "true"
|
|
|
|
}
|
|
|
|
fields.append(field1)
|
|
|
|
|
|
|
|
return fields
|
|
|
|
|
2007-08-16 17:59:58 -05:00
|
|
|
def get_all_users (self, args=None, opts=None):
|
2007-08-06 09:05:53 -05:00
|
|
|
"""Return a list containing a User object for each
|
|
|
|
existing user.
|
|
|
|
"""
|
2007-08-13 15:41:38 -05:00
|
|
|
global _LDAPPool
|
|
|
|
|
|
|
|
if opts:
|
|
|
|
self.set_principal(opts['remoteuser'])
|
|
|
|
|
2007-08-22 12:30:51 -05:00
|
|
|
dn = self.get_dn_from_principal(self.princ)
|
2007-08-06 09:05:53 -05:00
|
|
|
|
|
|
|
# FIXME: Is this the filter we want or should it be more specific?
|
|
|
|
filter = "(objectclass=posixAccount)"
|
2007-08-22 12:30:51 -05:00
|
|
|
|
|
|
|
m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,dn)
|
|
|
|
all_users = m1.getList(self.basedn, self.scope, filter, None)
|
|
|
|
_LDAPPool.releaseConn(m1)
|
2007-08-06 09:05:53 -05:00
|
|
|
|
|
|
|
users = []
|
|
|
|
for u in all_users:
|
|
|
|
users.append(self.convert_entry(u))
|
|
|
|
|
|
|
|
return users
|
2007-08-13 15:41:38 -05:00
|
|
|
|
|
|
|
def find_users (self, args, sattrs=None, opts=None):
|
|
|
|
"""Return a list containing a User object for each
|
|
|
|
existing user that matches the criteria.
|
|
|
|
"""
|
|
|
|
global _LDAPPool
|
|
|
|
|
|
|
|
# The XML-RPC server marshals the arguments into one variable
|
|
|
|
# while the direct caller has them separate. So do a little
|
|
|
|
# bit of gymnastics to figure things out. There has to be a
|
|
|
|
# better way, so FIXME
|
|
|
|
if isinstance(args,tuple):
|
|
|
|
opts = sattrs
|
|
|
|
if len(args) == 2:
|
|
|
|
criteria = args[0]
|
|
|
|
sattrs = args[1]
|
|
|
|
else:
|
|
|
|
criteria = args
|
|
|
|
sattrs = None
|
|
|
|
else:
|
|
|
|
criteria = args
|
|
|
|
|
|
|
|
if opts:
|
|
|
|
self.set_principal(opts['remoteuser'])
|
|
|
|
|
2007-08-22 12:30:51 -05:00
|
|
|
dn = self.get_dn_from_principal(self.princ)
|
2007-08-21 16:26:36 -05:00
|
|
|
|
|
|
|
# TODO: this escaper assumes the python-ldap library will error out
|
|
|
|
# on invalid codepoints. we need to check malformed utf-8 input
|
|
|
|
# where the second byte in a multi-byte character
|
|
|
|
# is (illegally) ')' and make sure python-ldap
|
|
|
|
# bombs out.
|
|
|
|
criteria = re.sub(r'[\(\)\\]', ldap_search_escape, criteria)
|
|
|
|
|
2007-08-13 15:41:38 -05:00
|
|
|
# FIXME: Is this the filter we want or do we want to do searches of
|
|
|
|
# cn as well? Or should the caller pass in the filter?
|
2007-08-20 16:23:23 -05:00
|
|
|
filter = "(|(uid=%s)(cn=%s))" % (criteria, criteria)
|
2007-08-13 15:41:38 -05:00
|
|
|
try:
|
|
|
|
m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,dn)
|
|
|
|
results = m1.getList(self.basedn, self.scope, filter, sattrs)
|
|
|
|
_LDAPPool.releaseConn(m1)
|
2007-08-22 12:30:51 -05:00
|
|
|
except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
|
2007-08-17 12:58:56 -05:00
|
|
|
results = []
|
2007-08-22 12:30:51 -05:00
|
|
|
|
2007-08-13 15:41:38 -05:00
|
|
|
users = []
|
|
|
|
for u in results:
|
|
|
|
users.append(self.convert_entry(u))
|
|
|
|
|
|
|
|
return users
|
2007-08-14 16:22:05 -05:00
|
|
|
|
2007-08-20 12:50:11 -05:00
|
|
|
def convert_scalar_values(self, orig_dict):
|
|
|
|
"""LDAP update dicts expect all values to be a list (except for dn).
|
|
|
|
This method converts single entries to a list."""
|
|
|
|
new_dict={}
|
|
|
|
for (k,v) in orig_dict.iteritems():
|
|
|
|
if not isinstance(v, list) and k != 'dn':
|
|
|
|
v = [v]
|
|
|
|
new_dict[k] = v
|
|
|
|
|
|
|
|
return new_dict
|
|
|
|
|
2007-08-14 16:22:05 -05:00
|
|
|
def update_user (self, args, newuser=None, opts=None):
|
|
|
|
"""Update a user in LDAP"""
|
|
|
|
global _LDAPPool
|
|
|
|
|
|
|
|
# The XML-RPC server marshals the arguments into one variable
|
|
|
|
# while the direct caller has them separate. So do a little
|
|
|
|
# bit of gymnastics to figure things out. There has to be a
|
|
|
|
# better way, so FIXME
|
|
|
|
if isinstance(args,tuple):
|
|
|
|
opts = newuser
|
|
|
|
if len(args) == 2:
|
|
|
|
olduser = args[0]
|
|
|
|
newuser = args[1]
|
|
|
|
else:
|
|
|
|
olduser = args
|
|
|
|
|
|
|
|
if (isinstance(olduser, tuple)):
|
|
|
|
olduser = olduser[0]
|
|
|
|
if (isinstance(newuser, tuple)):
|
|
|
|
newuser = newuser[0]
|
|
|
|
|
2007-08-20 12:50:11 -05:00
|
|
|
olduser = self.convert_scalar_values(olduser)
|
|
|
|
newuser = self.convert_scalar_values(newuser)
|
|
|
|
|
2007-08-14 16:22:05 -05:00
|
|
|
# Should be able to get this from either the old or new user
|
|
|
|
# but just in case someone has decided to try changing it, use the
|
|
|
|
# original
|
|
|
|
try:
|
|
|
|
moddn = olduser['dn']
|
|
|
|
except KeyError, e:
|
2007-08-22 12:30:51 -05:00
|
|
|
raise ipaerror.gen_exception(ipaerror.LDAP_MISSING_DN)
|
2007-08-14 16:22:05 -05:00
|
|
|
|
|
|
|
if opts:
|
|
|
|
self.set_principal(opts['remoteuser'])
|
|
|
|
|
2007-08-22 12:30:51 -05:00
|
|
|
proxydn = self.get_dn_from_principal(self.princ)
|
2007-08-14 16:22:05 -05:00
|
|
|
|
2007-08-22 12:30:51 -05:00
|
|
|
m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,proxydn)
|
|
|
|
res = m1.updateEntry(moddn, olduser, newuser)
|
|
|
|
_LDAPPool.releaseConn(m1)
|
|
|
|
return res
|
2007-08-17 09:03:33 -05:00
|
|
|
|
|
|
|
def mark_user_deleted (self, args, opts=None):
|
|
|
|
"""Mark a user as inactive in LDAP. We aren't actually deleting
|
|
|
|
users here, just making it so they can't log in, etc."""
|
|
|
|
global _LDAPPool
|
|
|
|
|
|
|
|
uid = args[0]
|
|
|
|
|
|
|
|
if opts:
|
|
|
|
self.set_principal(opts['remoteuser'])
|
|
|
|
|
2007-08-22 12:30:51 -05:00
|
|
|
proxydn = self.get_dn_from_principal(self.princ)
|
2007-08-17 09:03:33 -05:00
|
|
|
|
2007-08-22 12:30:51 -05:00
|
|
|
user = self.get_user(uid, ['dn', 'nsAccountlock'], opts)
|
2007-08-17 09:03:33 -05:00
|
|
|
|
|
|
|
# Are we doing an add or replace operation?
|
|
|
|
if user.has_key('nsaccountlock'):
|
|
|
|
has_key = True
|
|
|
|
else:
|
|
|
|
has_key = False
|
|
|
|
|
2007-08-22 12:30:51 -05:00
|
|
|
m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,proxydn)
|
|
|
|
res = m1.inactivateEntry(user['dn'], has_key)
|
|
|
|
_LDAPPool.releaseConn(m1)
|
|
|
|
return res
|
2007-08-21 16:26:36 -05:00
|
|
|
|
|
|
|
|
|
|
|
def ldap_search_escape(match):
|
|
|
|
"""Escapes out nasty characters from the ldap search.
|
|
|
|
See RFC 2254."""
|
|
|
|
value = match.group()
|
|
|
|
if (len(value) != 1):
|
|
|
|
return ""
|
|
|
|
|
|
|
|
if value == "(":
|
|
|
|
return "\\28"
|
|
|
|
elif value == ")":
|
|
|
|
return "\\29"
|
|
|
|
elif value == "\\":
|
|
|
|
return "\\5c"
|
|
|
|
else:
|
|
|
|
return value
|