X-Git-Url: https://git.adam-barratt.org.uk/?p=mirror%2Fuserdir-ldap.git;a=blobdiff_plain;f=userdir_ldap.py;h=00f9d4b6448fcd367e07cc8002fa8f1d9853feb7;hp=21b215bd7c24f0634b495cbeb00b0de6a0cdcb3a;hb=871ab5f2e8bda25130c70834052fa8fb020a5373;hpb=51f98437460dd3adac2cacd9510551dd2f3d54a7 diff --git a/userdir_ldap.py b/userdir_ldap.py index 21b215b..00f9d4b 100644 --- a/userdir_ldap.py +++ b/userdir_ldap.py @@ -1,5 +1,28 @@ +# Copyright (c) 1999-2000 Jason Gunthorpe +# Copyright (c) 2001-2003 Ryan Murray +# Copyright (c) 2004-2005 Joey Schulze +# Copyright (c) 2008 Peter Palfrader +# Copyright (c) 2008 Thomas Viehmann +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + # Some routines and configuration that are used by the ldap progams -import termios, TERMIOS, re, string, imp, ldap, sys, whrandom, crypt; +import termios, re, imp, ldap, sys, crypt, rfc822, pwd, os, getpass +import userdir_gpg +import hmac +import hashlib try: File = open("/etc/userdir-ldap/userdir-ldap.conf"); @@ -8,20 +31,63 @@ except: ConfModule = imp.load_source("userdir_config","/etc/userdir-ldap.conf",File); File.close(); +# Cheap hack BaseDn = ConfModule.basedn; -BaseDn = ConfModule.basedn; +HostBaseDn = ConfModule.hostbasedn; LDAPServer = ConfModule.ldaphost; EmailAppend = ConfModule.emailappend; AdminUser = ConfModule.adminuser; GenerateDir = ConfModule.generatedir; -GenerateConf = ConfModule.generateconf; -DefaultGID = ConfModule.defaultgid; +AllowedGroupsPreload = ConfModule.allowedgroupspreload; +HomePrefix = ConfModule.homeprefix; TemplatesDir = ConfModule.templatesdir; PassDir = ConfModule.passdir; +Ech_ErrorLog = ConfModule.ech_errorlog; +Ech_MainLog = ConfModule.ech_mainlog; +HostDomain = getattr(ConfModule, "hostdomain", EmailAppend) + +try: + UseSSL = ConfModule.usessl; +except AttributeError: + UseSSL = False; + +try: + BaseBaseDn = ConfModule.basebasedn; +except AttributeError: + BaseBaseDn = BaseDn + +try: + IgnoreUsersForUIDNumberGen = ConfModule.ignoreusersforuidnumbergen +except AttributeError: + IgnoreUsersForUIDNumberGen = ['nobody'] + + +# Break up the keyring list +userdir_gpg.SetKeyrings(ConfModule.keyrings.split(":")) # This is a list of common last-name prefixes -LastNamesPre = {"van": None, "le": None, "de": None, "di": None}; - +LastNamesPre = {"van": None, "von": None, "le": None, "de": None, "di": None}; + +# This is a list of common groups on Debian hosts +DebianGroups = { + "Debian": 800, + "guest": 60000, + "nogroup": 65534 + } + +# ObjectClasses for different object types +UserObjectClasses = ("top", "inetOrgPerson", "debianAccount", "shadowAccount", "debianDeveloper") +RoleObjectClasses = ("top", "debianAccount", "shadowAccount", "debianRoleAccount") +GroupObjectClasses = ("top", "debianGroup") + +# SSH Key splitting. The result is: +# (options,size,modulous,exponent,comment) +SSHAuthSplit = re.compile('^(.* )?(\d+) (\d+) (\d+) ?(.+)$'); +SSH2AuthSplit = re.compile('^(.* )?ssh-(dss|rsa|ecdsa-sha2-nistp(?:256|384|521)|ed25519) ([a-zA-Z0-9=/+]+) ?(.+)$'); +#'^([^\d](?:[^ "]+(?:".*")?)*)? ?(\d+) (\d+) (\d+) (.+)$'); + +AddressSplit = re.compile("(.*).*<([^@]*)@([^>]*)>"); + # Safely get an attribute from a tuple representing a dn and an attribute # list. It returns the first attribute if there are multi. def GetAttr(DnRecord,Attribute,Default = ""): @@ -53,25 +119,49 @@ def PrettyShow(DnRecord): Result = Result + "%s: %s\n" % (x,i); return Result[:-1]; -# Function to prompt for a password -def getpass(prompt = "Password: "): - import termios, TERMIOS, sys; - fd = sys.stdin.fileno(); - old = termios.tcgetattr(fd); - new = termios.tcgetattr(fd); - new[3] = new[3] & ~TERMIOS.ECHO; # lflags - try: - termios.tcsetattr(fd, TERMIOS.TCSADRAIN, new); - passwd = raw_input(prompt); - finally: - termios.tcsetattr(fd, TERMIOS.TCSADRAIN, old); - print; - return passwd; +def connectLDAP(server = None): + if server == None: + global LDAPServer + server = LDAPServer + l = ldap.open(server); + global UseSSL + if UseSSL: + l.start_tls_s(); + return l; + +def passwdAccessLDAP(BaseDn, AdminUser): + """ + Ask for the AdminUser's password and connect to the LDAP server. + Returns the connection handle. + """ + print "Accessing LDAP directory as '" + AdminUser + "'"; + while (1): + if 'LDAP_PASSWORD' in os.environ: + Password = os.environ['LDAP_PASSWORD'] + else: + Password = getpass.getpass(AdminUser + "'s password: ") + + if len(Password) == 0: + sys.exit(0) + + l = connectLDAP() + UserDn = "uid=" + AdminUser + "," + BaseDn; + + # Connect to the ldap server + try: + l.simple_bind_s(UserDn,Password); + except ldap.INVALID_CREDENTIALS: + if 'LDAP_PASSWORD' in os.environ: + print "password in environment does not work" + del os.environ['LDAP_PASSWORD'] + continue + break + return l # Split up a name into multiple components. This tries to best guess how # to split up a name def NameSplit(Name): - Words = re.split(" ",string.strip(Name)); + Words = re.split(" ", Name.strip()) # Insert an empty middle name if (len(Words) == 2): @@ -93,16 +183,15 @@ def NameSplit(Name): break; # Merge any of the middle initials - if len(Words) > 2: - while len(Words[2]) == 2 and Words[2][1] == '.': - Words[1] = Words[1] + Words[2]; - del Words[2]; + while len(Words) > 2 and len(Words[2]) == 2 and Words[2][1] == '.': + Words[1] = Words[1] + Words[2]; + del Words[2]; while len(Words) < 2: Words.append(''); # Merge any of the last name prefixes into one big last name - while LastNamesPre.has_key(string.lower(Words[-2])): + while LastNamesPre.has_key(Words[-2].lower()): Words[-1] = Words[-2] + " " + Words[-1]; del Words[-2]; @@ -113,10 +202,10 @@ def NameSplit(Name): # If the name is multi-word then we glob them all into the last name and # do not worry about a middle name if (len(Words) > 3): - Words[2] = string.join(Words[1:]); + Words[2] = " ".join(Words[1:]) Words[1] = ""; - return (string.strip(Words[0]),string.strip(Words[1]),string.strip(Words[2])); + return (Words[0].strip(), Words[1].strip(), Words[2].strip()); # Compute a random password using /dev/urandom def GenPass(): @@ -124,7 +213,7 @@ def GenPass(): SaltVals = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/."; Rand = open("/dev/urandom"); Password = ""; - for i in range(0,10): + for i in range(0,15): Password = Password + SaltVals[ord(Rand.read(1)[0]) % len(SaltVals)]; return Password; @@ -132,10 +221,11 @@ def GenPass(): def HashPass(Password): # Hash it telling glibc to use the MD5 algorithm - if you dont have # glibc then just change Salt = "$1$" to Salt = ""; - SaltVals = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/."; + SaltVals = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/."; Salt = "$1$"; + Rand = open("/dev/urandom"); for x in range(0,10): - Salt = Salt + SaltVals[whrandom.randint(0,len(SaltVals)-1)]; + Salt = Salt + SaltVals[ord(Rand.read(1)[0]) % len(SaltVals)]; Pass = crypt.crypt(Password,Salt); if len(Pass) < 14: raise "Password Error", "MD5 password hashing failed, not changing the password!"; @@ -170,9 +260,9 @@ def FlushOutstanding(l,Outstanding,Fast=0): return Outstanding; # Convert a lat/long attribute into Decimal degrees -def DecDegree(Attr,Type,Anon=0): - Parts = re.match('[+-]?(\d*)\\.?(\d*)?',GetAttr(Attr,Type)).groups(); - Val = string.atof(GetAttr(Attr,Type)); +def DecDegree(Posn,Anon=0): + Parts = re.match('[-+]?(\d*)\\.?(\d*)',Posn).groups(); + Val = float(Posn); if (abs(Val) >= 1806060.0): raise ValueError,"Too Big"; @@ -198,3 +288,173 @@ def DecDegree(Attr,Type,Anon=0): if Val >= 0: return "+" + Str; return Str; + +def FormatSSH2Auth(Str): + Match = SSH2AuthSplit.match(Str); + if Match == None: + return ""; + G = Match.groups(); + + if G[0] == None: + return "ssh-%s %s..%s %s"%(G[1],G[2][:8],G[2][-8:],G[3]); + return "%s ssh-%s %s..%s %s"%(G[0],G[1],G[2][:8],G[2][-8:],G[3]); + +def FormatSSHAuth(Str): + Match = SSHAuthSplit.match(Str); + if Match == None: + return FormatSSH2Auth(Str); + G = Match.groups(); + + # No options + if G[0] == None: + return "%s %s %s..%s %s"%(G[1],G[2],G[3][:8],G[3][-8:],G[4]); + return "%s %s %s %s..%s %s"%(G[0],G[1],G[2],G[3][:8],G[3][-8:],G[4]); + +def FormatPGPKey(Str): + Res = ""; + + # PGP 2.x Print + if (len(Str) == 32): + I = 0; + while (I < len(Str)): + if I == 32/2: + Res = "%s %s%s "%(Res,Str[I],Str[I+1]); + else: + Res = "%s%s%s "%(Res,Str[I],Str[I+1]); + I = I + 2; + elif (len(Str) == 40): + # OpenPGP Print + I = 0; + while (I < len(Str)): + if I == 40/2: + Res = "%s %s%s%s%s "%(Res,Str[I],Str[I+1],Str[I+2],Str[I+3]); + else: + Res = "%s%s%s%s%s "%(Res,Str[I],Str[I+1],Str[I+2],Str[I+3]); + I = I + 4; + else: + Res = Str; + return Res.strip() + +# Take an email address and split it into 3 parts, (Name,UID,Domain) +def SplitEmail(Addr): + # Is not an email address at all + if Addr.find('@') == -1: + return (Addr,"",""); + + Res1 = rfc822.AddrlistClass(Addr).getaddress(); + if len(Res1) != 1: + return ("","",Addr); + Res1 = Res1[0]; + if Res1[1] == None: + return (Res1[0],"",""); + + # If there is no @ then the address was not parsed well. Try the alternate + # Parsing scheme. This is particularly important when scanning PGP keys. + Res2 = Res1[1].split("@"); + if len(Res2) != 2: + Match = AddressSplit.match(Addr); + if Match == None: + return ("","",Addr); + return Match.groups(); + + return (Res1[0],Res2[0],Res2[1]); + +# Convert the PGP name string to a uid value. The return is a tuple of +# (uid,[message strings]). UnknownMpa is a hash from email to uid that +# overrides normal searching. +def GetUID(l,Name,UnknownMap = {}): + # Crack up the email address into a best guess first/middle/last name + (cn,mn,sn) = NameSplit(re.sub('["]','',Name[0])) + + # Brackets anger the ldap searcher + cn = re.sub('[(")]','?',cn); + sn = re.sub('[(")]','?',sn); + + # First check the unknown map for the email address + if UnknownMap.has_key(Name[1] + '@' + Name[2]): + Stat = "unknown map hit for "+str(Name); + return (UnknownMap[Name[1] + '@' + Name[2]],[Stat]); + + # Then the cruft component (ie there was no email address to match) + if UnknownMap.has_key(Name[2]): + Stat = "unknown map hit for"+str(Name); + return (UnknownMap[Name[2]],[Stat]); + + # Then the name component (another ie there was no email address to match) + if UnknownMap.has_key(Name[0]): + Stat = "unknown map hit for"+str(Name); + return (UnknownMap[Name[0]],[Stat]); + + # Search for a possible first/last name hit + try: + Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"(&(cn=%s)(sn=%s))"%(cn,sn),["uid"]); + except ldap.FILTER_ERROR: + Stat = "Filter failure: (&(cn=%s)(sn=%s))"%(cn,sn); + return (None,[Stat]); + + # Try matching on the email address + if (len(Attrs) != 1): + try: + Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"emailforward=%s"%(Name[2]),["uid"]); + except ldap.FILTER_ERROR: + pass; + + # Hmm, more than one/no return + if (len(Attrs) != 1): + # Key claims a local address + if Name[2] == EmailAppend: + + # Pull out the record for the claimed user + Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"(uid=%s)"%(Name[1]),["uid","sn","cn"]); + + # We require the UID surname to be someplace in the key name, this + # deals with special purpose keys like 'James Troup (Alternate Debian key)' + # Some people put their names backwards on their key too.. check that as well + if len(Attrs) == 1 and \ + ( sn.lower().find(Attrs[0][1]["sn"][0].lower()) != -1 or \ + cn.lower().find(Attrs[0][1]["sn"][0].lower()) != -1 ): + Stat = EmailAppend+" hit for "+str(Name); + return (Name[1],[Stat]); + + # Attempt to give some best guess suggestions for use in editing the + # override file. + Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"(sn~=%s)"%(sn),["uid","sn","cn"]); + + Stat = []; + if len(Attrs) != 0: + Stat = ["None for %s"%(str(Name))]; + for x in Attrs: + Stat.append("But might be: %s %s <%s@debian.org>"%(x[1]["cn"][0],x[1]["sn"][0],x[1]["uid"][0])); + return (None,Stat); + else: + return (Attrs[0][1]["uid"][0],None); + + return (None,None); + +def Group2GID(l, name): + """ + Returns the numerical id of a common group + on error returns -1 + """ + for g in DebianGroups.keys(): + if name == g: + return DebianGroups[g] + + filter = "(gid=%s)" % name + res = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,filter,["gidNumber"]); + if res: + return int(GetAttr(res[0], "gidNumber")) + + return -1 + +def make_hmac(str): + if 'UD_HMAC_KEY' in os.environ: + HmacKey = os.environ['UD_HMAC_KEY'] + else: + File = open(PassDir+"/key-hmac-"+pwd.getpwuid(os.getuid())[0],"r"); + HmacKey = File.readline().strip() + File.close(); + return hmac.new(HmacKey, str, hashlib.sha1).hexdigest() + +def make_passwd_hmac(status, purpose, uid, uuid, hosts, cryptedpass): + return make_hmac(':'.join([status, purpose, uid, uuid, hosts, cryptedpass]))