1 # Copyright (c) 1999-2000 Jason Gunthorpe <jgg@debian.org>
2 # Copyright (c) 2001-2003 Ryan Murray <rmurray@debian.org>
3 # Copyright (c) 2004-2005 Joey Schulze <joey@infodrom.org>
4 # Copyright (c) 2008 Peter Palfrader <peter@palfrader.org>
5 # Copyright (c) 2008 Thomas Viehmann <tv@beamnet.de>
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 2 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
21 # Some routines and configuration that are used by the ldap progams
38 File = open("/etc/userdir-ldap/userdir-ldap.conf")
40 File = open("userdir-ldap.conf")
41 ConfModule = imp.load_source("userdir_config", "/etc/userdir-ldap.conf", File)
45 BaseDn = ConfModule.basedn
46 HostBaseDn = ConfModule.hostbasedn
47 LDAPServer = ConfModule.ldaphost
48 EmailAppend = ConfModule.emailappend
49 AdminUser = ConfModule.adminuser
50 GenerateDir = ConfModule.generatedir
51 AllowedGroupsPreload = ConfModule.allowedgroupspreload
52 HomePrefix = ConfModule.homeprefix
53 TemplatesDir = ConfModule.templatesdir
54 PassDir = ConfModule.passdir
55 Ech_ErrorLog = ConfModule.ech_errorlog
56 Ech_MainLog = ConfModule.ech_mainlog
57 HostDomain = getattr(ConfModule, "hostdomain", EmailAppend)
60 UseSSL = ConfModule.usessl
61 except AttributeError:
65 BaseBaseDn = ConfModule.basebasedn
66 except AttributeError:
70 IgnoreUsersForUIDNumberGen = ConfModule.ignoreusersforuidnumbergen
71 except AttributeError:
72 IgnoreUsersForUIDNumberGen = ['nobody']
75 # Break up the keyring list
76 userdir_gpg.SetKeyrings(ConfModule.keyrings.split(":"))
78 # This is a list of common last-name prefixes
79 LastNamesPre = {"van": None, "von": None, "le": None, "de": None, "di": None}
81 # This is a list of common groups on Debian hosts
88 # ObjectClasses for different object types
89 UserObjectClasses = ("top", "inetOrgPerson", "debianAccount", "shadowAccount", "debianDeveloper")
90 RoleObjectClasses = ("top", "debianAccount", "shadowAccount", "debianRoleAccount")
91 GroupObjectClasses = ("top", "debianGroup")
93 # SSH Key splitting. The result is:
94 # (options,size,modulous,exponent,comment)
95 SSHAuthSplit = re.compile('^(.* )?(\d+) (\d+) (\d+) ?(.+)$')
96 SSH2AuthSplit = re.compile('^(.* )?ssh-(dss|rsa|ecdsa-sha2-nistp(?:256|384|521)|ed25519) ([a-zA-Z0-9=/+]+) ?(.+)$')
97 AddressSplit = re.compile("(.*).*<([^@]*)@([^>]*)>")
100 # Safely get an attribute from a tuple representing a dn and an attribute
101 # list. It returns the first attribute if there are multi.
102 def GetAttr(DnRecord, Attribute, Default=""):
104 return DnRecord[1][Attribute][0]
112 # Return a printable email address from the attributes.
113 def EmailAddress(DnRecord):
114 cn = GetAttr(DnRecord, "cn")
115 sn = GetAttr(DnRecord, "sn")
116 uid = GetAttr(DnRecord, "uid")
117 if cn == "" and sn == "":
118 return "<" + uid + "@" + EmailAppend + ">"
119 return cn + " " + sn + " <" + uid + "@" + EmailAppend + ">"
122 # Show a dump like ldapsearch
123 def PrettyShow(DnRecord):
125 List = DnRecord[1].keys()
130 Result = Result + "%s: %s\n" % (x, i)
134 def connectLDAP(server=None):
138 lc = ldap.open(server)
145 def passwdAccessLDAP(BaseDn, AdminUser):
147 Ask for the AdminUser's password and connect to the LDAP server.
148 Returns the connection handle.
150 print "Accessing LDAP directory as '" + AdminUser + "'"
152 if 'LDAP_PASSWORD' in os.environ:
153 Password = os.environ['LDAP_PASSWORD']
155 Password = getpass.getpass(AdminUser + "'s password: ")
157 if len(Password) == 0:
161 UserDn = "uid=" + AdminUser + "," + BaseDn
163 # Connect to the ldap server
165 lc.simple_bind_s(UserDn, Password)
166 except ldap.INVALID_CREDENTIALS:
167 if 'LDAP_PASSWORD' in os.environ:
168 print "password in environment does not work"
169 del os.environ['LDAP_PASSWORD']
175 # Split up a name into multiple components. This tries to best guess how
178 Words = re.split(" ", Name.strip())
180 # Insert an empty middle name
186 # Put a dot after any 1 letter words, must be an initial
187 for x in range(0, len(Words)):
188 if len(Words[x]) == 1:
189 Words[x] = Words[x] + '.'
191 # If a word starts with a -, ( or [ we assume it marks the start of some
192 # Non-name information and remove the remainder of the string
193 for x in range(0, len(Words)):
194 if len(Words[x]) != 0 and (Words[x][0] == '-' or
195 Words[x][0] == '(' or Words[x][0] == '['):
199 # Merge any of the middle initials
200 while len(Words) > 2 and len(Words[2]) == 2 and Words[2][1] == '.':
201 Words[1] = Words[1] + Words[2]
204 while len(Words) < 2:
207 # Merge any of the last name prefixes into one big last name
208 while Words[-2].lower() in LastNamesPre:
209 Words[-1] = Words[-2] + " " + Words[-1]
212 # Fix up a missing middle name after lastname globbing
216 # If the name is multi-word then we glob them all into the last name and
217 # do not worry about a middle name
219 Words[2] = " ".join(Words[1:])
222 return (Words[0].strip(), Words[1].strip(), Words[2].strip())
225 # Compute a random password using /dev/urandom
227 # Generate a 10 character random string
228 SaltVals = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/."
229 Rand = open("/dev/urandom")
231 for i in range(0, 15):
232 Password = Password + SaltVals[ord(Rand.read(1)[0]) % len(SaltVals)]
236 # Compute the MD5 crypted version of the given password
237 def HashPass(Password):
238 # Hash it telling glibc to use the MD5 algorithm - if you dont have
239 # glibc then just change Salt = "$1$" to Salt = "";
240 SaltVals = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/."
242 Rand = open("/dev/urandom")
243 for x in range(0, 10):
244 Salt = Salt + SaltVals[ord(Rand.read(1)[0]) % len(SaltVals)]
245 Pass = crypt.crypt(Password, Salt)
247 raise "Password Error", "MD5 password hashing failed, not changing the password!"
251 # Sync with the server, we count the number of async requests that are pending
252 # and make sure result has been called that number of times
253 def FlushOutstanding(l, Outstanding, Fast=0):
254 # Sync with the remote end
256 print "Waiting for {} requests:".format(Outstanding),
257 while (Outstanding > 0):
259 if Fast == 0 or Outstanding > 50:
260 sys.stdout.write(".",)
262 if (l.result(ldap.RES_ANY, 1) != (None, None)):
263 Outstanding = Outstanding - 1
265 if (l.result(ldap.RES_ANY, 1, 0) != (None, None)):
266 Outstanding = Outstanding - 1
269 except ldap.TYPE_OR_VALUE_EXISTS:
271 except ldap.NO_SUCH_ATTRIBUTE:
273 except ldap.NO_SUCH_OBJECT:
280 # Convert a lat/long attribute into Decimal degrees
281 def DecDegree(Posn, Anon=0):
282 Parts = re.match('[-+]?(\d*)\\.?(\d*)', Posn).groups()
285 if (abs(Val) >= 1806060.0):
286 raise ValueError("Too Big")
289 if abs(Val) >= 18060.0 or len(Parts[0]) > 5:
291 Secs = Val - long(Val)
292 Val = long(Val) / 100.0
293 Min = Val - long(Val)
294 Val = long(Val) + (Min * 100.0 + Secs * 100.0 / 60.0) / 60.0
297 elif abs(Val) >= 180 or len(Parts[0]) > 3:
299 Min = Val - long(Val)
300 Val = long(Val) + Min * 100.0 / 60.0
303 Str = "%3.2f" % (Val)
311 def FormatSSH2Auth(Str):
312 Match = SSH2AuthSplit.match(Str)
314 return "<unknown format>"
318 return "ssh-%s %s..%s %s" % (G[1], G[2][:8], G[2][-8:], G[3])
319 return "%s ssh-%s %s..%s %s" % (G[0], G[1], G[2][:8], G[2][-8:], G[3])
322 def FormatSSHAuth(Str):
323 Match = SSHAuthSplit.match(Str)
325 return FormatSSH2Auth(Str)
330 return "%s %s %s..%s %s" % (G[1], G[2], G[3][:8], G[3][-8:], G[4])
331 return "%s %s %s %s..%s %s" % (G[0], G[1], G[2], G[3][:8], G[3][-8:], G[4])
334 def FormatPGPKey(Str):
340 while (indent < len(Str)):
342 Res = "%s %s%s " % (Res, Str[indent], Str[indent + 1])
344 Res = "%s%s%s " % (Res, Str[indent], Str[indent + 1])
346 elif (len(Str) == 40):
349 while (indent < len(Str)):
351 Res = "%s %s%s%s%s " % (Res, Str[indent], Str[indent + 1], Str[indent + 2], Str[indent + 3])
353 Res = "%s%s%s%s%s " % (Res, Str[indent], Str[indent + 1], Str[indent + 2], Str[indent + 3])
360 # Take an email address and split it into 3 parts, (Name,UID,Domain)
361 def SplitEmail(Addr):
362 # Is not an email address at all
363 if Addr.find('@') == -1:
364 return (Addr, "", "")
366 Res1 = rfc822.AddrlistClass(Addr).getaddress()
368 return ("", "", Addr)
371 return (Res1[0], "", "")
373 # If there is no @ then the address was not parsed well. Try the alternate
374 # Parsing scheme. This is particularly important when scanning PGP keys.
375 Res2 = Res1[1].split("@")
377 Match = AddressSplit.match(Addr)
379 return ("", "", Addr)
380 return Match.groups()
382 return (Res1[0], Res2[0], Res2[1])
385 # Convert the PGP name string to a uid value. The return is a tuple of
386 # (uid,[message strings]). UnknownMpa is a hash from email to uid that
387 # overrides normal searching.
388 def GetUID(l, Name, UnknownMap={}):
389 # Crack up the email address into a best guess first/middle/last name
390 (cn, mn, sn) = NameSplit(re.sub('["]', '', Name[0]))
392 # Brackets anger the ldap searcher
393 cn = re.sub('[(")]', '?', cn)
394 sn = re.sub('[(")]', '?', sn)
396 # First check the unknown map for the email address
397 if Name[1] + '@' + Name[2] in UnknownMap:
398 Stat = "unknown map hit for " + str(Name)
399 return (UnknownMap[Name[1] + '@' + Name[2]], [Stat])
401 # Then the cruft component (ie there was no email address to match)
402 if Name[2] in UnknownMap:
403 Stat = "unknown map hit for" + str(Name)
404 return (UnknownMap[Name[2]], [Stat])
406 # Then the name component (another ie there was no email address to match)
407 if Name[0] in UnknownMap:
408 Stat = "unknown map hit for" + str(Name)
409 return (UnknownMap[Name[0]], [Stat])
411 # Search for a possible first/last name hit
413 Attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(cn=%s)(sn=%s))" % (cn, sn), ["uid"])
414 except ldap.FILTER_ERROR:
415 Stat = "Filter failure: (&(cn=%s)(sn=%s))" % (cn, sn)
416 return (None, [Stat])
418 # Try matching on the email address
421 Attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "emailforward=%s" % (Name[2]), ["uid"])
422 except ldap.FILTER_ERROR:
425 # Hmm, more than one/no return
427 # Key claims a local address
428 if Name[2] == EmailAppend:
430 # Pull out the record for the claimed user
431 Attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(uid=%s)" % (Name[1]), ["uid", "sn", "cn"])
433 # We require the UID surname to be someplace in the key name, this
434 # deals with special purpose keys like 'James Troup (Alternate Debian key)'
435 # Some people put their names backwards on their key too.. check that as well
436 if len(Attrs) == 1 and \
437 (sn.lower().find(Attrs[0][1]["sn"][0].lower()) != -1 or
438 cn.lower().find(Attrs[0][1]["sn"][0].lower()) != -1):
439 Stat = EmailAppend + " hit for " + str(Name)
440 return (Name[1], [Stat])
442 # Attempt to give some best guess suggestions for use in editing the
444 Attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(sn~=%s)" % (sn,), ["uid", "sn", "cn"])
448 Stat = ["None for %s" % (str(Name))]
450 Stat.append("But might be: %s %s <%s@debian.org>" % (x[1]["cn"][0], x[1]["sn"][0], x[1]["uid"][0]))
453 return (Attrs[0][1]["uid"][0], None)
458 def Group2GID(l, name):
460 Returns the numerical id of a common group
463 for g in DebianGroups.keys():
465 return DebianGroups[g]
467 filter = "(gid=%s)" % name
468 res = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, filter, ["gidNumber"])
470 return int(GetAttr(res[0], "gidNumber"))
476 if 'UD_HMAC_KEY' in os.environ:
477 HmacKey = os.environ['UD_HMAC_KEY']
479 File = open(PassDir + "/key-hmac-" + pwd.getpwuid(os.getuid())[0], "r")
480 HmacKey = File.readline().strip()
482 return hmac.new(HmacKey, str, hashlib.sha1).hexdigest()
485 def make_passwd_hmac(status, purpose, uid, uuid, hosts, cryptedpass):
486 return make_hmac(':'.join([status, purpose, uid, uuid, hosts, cryptedpass]))