Try to make key acceptance logic clearer
[mirror/userdir-ldap.git] / ud-useradd
1 #!/usr/bin/env python
2 # -*- mode: python -*-
3
4 #   Copyright (c) 1999-2000  Jason Gunthorpe <jgg@debian.org>
5 #   Copyright (c) 2001-2003  James Troup <troup@debian.org>
6 #   Copyright (c) 2004  Joey Schulze <joey@infodrom.org>
7 #   Copyright (c) 2008,2009,2010 Peter Palfrader <peter@palfrader.org>
8 #
9 #   This program is free software; you can redistribute it and/or modify
10 #   it under the terms of the GNU General Public License as published by
11 #   the Free Software Foundation; either version 2 of the License, or
12 #   (at your option) any later version.
13 #
14 #   This program is distributed in the hope that it will be useful,
15 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
16 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 #   GNU General Public License for more details.
18 #
19 #   You should have received a copy of the GNU General Public License
20 #   along with this program; if not, write to the Free Software
21 #   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
22
23 import re, time, ldap, getopt, sys, os, pwd;
24 import email.Header
25 import datetime
26
27 from userdir_ldap import *;
28 from userdir_gpg import *;
29
30 HavePrivateList = getattr(ConfModule, "haveprivatelist", True)
31
32 # This tries to search for a free UID. There are two possible ways to do
33 # this, one is to fetch all the entires and pick the highest, the other
34 # is to randomly guess uids until one is free. This uses the former.
35 # Regrettably ldap doesn't have an integer attribute comparision function
36 # so we can only cut the search down slightly
37
38 def ShouldIgnoreID(uid):
39    for i in IgnoreUsersForUIDNumberGen:
40       try:
41          if i.search(uid) is not None:
42             return True
43       except AttributeError:
44          if uid == i:
45             return True
46
47    return False
48
49 # [JT] This is broken with Woody LDAP and the Schema; for now just
50 #      search through all UIDs.
51 def GetFreeID(l):
52    Attrs = l.search_s(BaseBaseDn,ldap.SCOPE_SUBTREE,
53                       "uidNumber=*",["uidNumber", "gidNumber", "uid"]);
54    HighestUID = 0;
55    gids = [];
56    uids = [];
57    for I in Attrs:
58       ID = int(GetAttr(I,"uidNumber","0"));
59       uids.append(ID)
60       gids.append(int(GetAttr(I, "gidNumber","0")))
61       uid = GetAttr(I, "uid", None)
62       if ID > HighestUID and not uid is None and not ShouldIgnoreID(uid):
63          HighestUID = ID;
64
65    resUID = HighestUID + 1;
66    while resUID in uids or resUID in gids:
67       resUID += 1
68
69    return (resUID, resUID)
70
71 # Main starts here
72 AdminUser = pwd.getpwuid(os.getuid())[0];
73
74 # Process options
75 ForceMail = 0;
76 NoAutomaticIDs = 0;
77 GuestAccount = False
78 OldGPGKeyRings = GPGKeyRings;
79 userdir_gpg.GPGKeyRings = [];
80 (options, arguments) = getopt.getopt(sys.argv[1:], "hgu:man")
81 for (switch, val) in options:
82    if (switch == '-h'):
83       print "Usage: ud-useradd <options>"
84       print "Available options:"
85       print "        -h         Show this help"
86       print "        -u=<user>  Admin user (defaults to current username)"
87       print "        -m         Force mail (for updates)"
88       print "        -a         Use old keyrings instead (??)"
89       print "        -n         Do not automatically assign UID/GIDs (useful for usergroups or non-default group membership"
90       print "        -g         Add a guest account"
91       sys.exit(0)
92    elif (switch == '-u'):
93       AdminUser = val;
94    elif (switch == '-m'):
95       ForceMail = 1;
96    elif (switch == '-a'):
97       userdir_gpg.GPGKeyRings = OldGPGKeyRings;
98    elif (switch == '-n'):
99       NoAutomaticIDs = 1;
100    elif (switch == '-g'):
101       GuestAccount = True
102
103 l = passwdAccessLDAP(BaseDn, AdminUser)
104
105 # Locate the key of the user we are adding
106 if GuestAccount:
107    SetKeyrings(ConfModule.add_keyrings_guest.split(":"))
108 else:
109    SetKeyrings(ConfModule.add_keyrings.split(":"))
110
111 while (1):
112    Foo = raw_input("Who are you going to add (for a GPG search)? ");
113    if Foo == "":
114       sys.exit(0);
115
116    Keys = GPGKeySearch(Foo);
117
118    if len(Keys) == 0:
119       print "Sorry, that search did not turn up any keys."
120       print "Has it been added to the Debian keyring already?"
121       continue;
122    if len(Keys) > 1:
123       print "Sorry, more than one key was found, please specify the key to use by\nfingerprint:";
124       for i in Keys:
125          GPGPrintKeyInfo(i);
126       continue;
127
128    print
129    print "A matching key was found:"
130    GPGPrintKeyInfo(Keys[0]);
131    break;
132
133 # Crack up the email address from the key into a best guess
134 # first/middle/last name
135 Addr = SplitEmail(Keys[0][2]);
136 (cn,mn,sn) = NameSplit(re.sub('["]','',Addr[0]))
137 emailaddr = Addr[1] + '@' + Addr[2];
138 account = Addr[1];
139
140 privsub = emailaddr
141 gidNumber = 0;
142 uidNumber = 0;
143
144 # Decide if we should use IDEA encryption
145 UsePGP2 = 0;
146 while len(Keys[0][1]) < 40:
147    Res = raw_input("Use PGP2.x compatibility [No/yes]? ");
148    if Res == "yes":
149       UsePGP2 = 1;
150       break;
151    if Res == "":
152       break;
153
154 Update = 0
155 Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"keyFingerPrint=" + Keys[0][1]);
156 if len(Attrs) != 0:
157    print "*** This key already belongs to",GetAttr(Attrs[0],"uid");
158    account = GetAttr(Attrs[0],"uid");
159    Update = 1
160
161 # Try to get a uniq account name
162 while 1:
163    if Update == 0:
164       Res = raw_input("Login account [" + account + "]? ");
165       if Res != "":
166          account = Res;
167    Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"uid=" + account);
168    if len(Attrs) == 0:
169       privsub = "%s@debian.org"%(account);
170       break;
171    Res = raw_input("That account already exists, update [No/yes]? ");
172    if Res == "yes":
173       # Update mode, fetch the default values from the directory
174       Update = 1;
175       privsub = GetAttr(Attrs[0],"privateSub");
176       gidNumber = GetAttr(Attrs[0],"gidNumber");
177       uidNumber = GetAttr(Attrs[0],"uidNumber");
178       emailaddr = GetAttr(Attrs[0],"emailForward");
179       cn = GetAttr(Attrs[0],"cn");
180       sn = GetAttr(Attrs[0],"sn");
181       mn = GetAttr(Attrs[0],"mn");
182       if privsub == None or privsub == "":
183          privsub = " ";
184       break;
185    else:
186       sys.exit(1)
187
188 # Prompt for the first/last name and email address
189 Res = raw_input("First name [" + cn + "]? ");
190 if Res != "":
191    cn = Res;
192 Res = raw_input("Middle name [" + mn + "]? ");
193 if Res == " ":
194    mn = ""
195 elif Res != "":
196    mn = Res;
197 Res = raw_input("Last name [" + sn + "]? ");
198 if Res != "":
199    sn = Res;
200 Res = raw_input("Email forwarding address [" + emailaddr + "]? ");
201 if Res != "":
202    emailaddr = Res;
203
204 # Debian-Private subscription
205 if HavePrivateList and not GuestAccount:
206    Res = raw_input("Subscribe to debian-private (space is none) [" + privsub + "]? ");
207    if Res != "":
208       privsub = Res;
209 else:
210    privsub = " "
211
212 if not gidNumber:
213    if not GuestAccount:
214       gidNumber = DefaultGID
215    else:
216       gidNumber = DebianGroups['guest']
217
218 (uidNumber, generatedGID) = GetFreeID(l)
219 UserGroup = 0
220 if NoAutomaticIDs:
221    # UID
222    if not Update:
223       Res = raw_input("User ID Number [%s]? " % (uidNumber));
224       if Res != "":
225          uidNumber = Res;
226    
227    # GID
228    Res = raw_input("Group ID Number (default group is %s, new usergroup %s) [%s]" % (DefaultGID, generatedGID, gidNumber));
229    if Res != "":
230       if Res.isdigit():
231          gidNumber = int(Res);
232       else:
233          gidNumber = Group2GID(l, Res);
234    
235    if gidNumber == generatedGID:
236       UserGroup = 1
237
238 shadowExpire = None
239 hostacl = []
240 if GuestAccount:
241    res = raw_input("Expires in xx days [60] (0 to disable)")
242    if res == "": res = '60'
243    exp = int(res)
244    if exp > 0:
245       shadowExpire = int(time.time() / 3600 / 24) + exp
246    res = raw_input("Hosts to grant access to: ")
247    for h in res.split():
248       if not '.' in h: h = h + '.' + HostDomain
249       if exp > 0: h = h + " " + datetime.datetime.fromtimestamp( time.time() + exp * 24*3600 ).strftime("%Y%m%d")
250       hostacl.append(h)
251
252
253 # Generate a random password
254 if Update == 0 or ForceMail == 1:
255    Password = raw_input("User's Password (Enter for random)? ");
256
257    if Password == "":
258       print "Randomizing and encrypting password"
259       Password = GenPass();
260       Pass = HashPass(Password);
261
262       # Use GPG to encrypt it, pass the fingerprint to ID it
263       CryptedPass = GPGEncrypt("Your new password is '" + Password + "'\n",\
264                                "0x"+Keys[0][1],UsePGP2);
265       Password = None;
266       if CryptedPass == None:
267         raise "Error","Password Encryption failed"
268    else:
269       Pass = HashPass(Password);
270       CryptedPass = "Your password has been set to the previously agreed value.";
271 else:
272    CryptedPass = "";
273    Pass = None;
274
275 # Now we have all the bits of information.
276 if mn != "":
277    FullName = "%s %s %s" % (cn,mn,sn);
278 else:
279    FullName = "%s %s" % (cn,sn);
280 print "------------";
281 print "Final information collected:"
282 print " %s <%s@%s>:" % (FullName,account,EmailAppend);
283 print "   Assigned UID:",uidNumber," GID:", gidNumber;
284 print "   Email forwarded to:",emailaddr
285 if HavePrivateList:
286    print "   Private Subscription:",privsub;
287 print "   GECOS Field: \"%s,,,,\"" % (FullName);
288 print "   Login Shell: /bin/bash";
289 print "   Key Fingerprint:",Keys[0][1];
290 if shadowExpire:
291    print "   ShadowExpire: %d (%s)"%(shadowExpire, datetime.datetime.fromtimestamp( shadowExpire * 24*3600 ).strftime("%Y%m%d") )
292 for h in hostacl:
293    print "   allowedHost: ", h
294
295 Res = raw_input("Continue [No/yes]? ");
296 if Res != "yes":
297    sys.exit(1);
298
299 # Initialize the substitution Map
300 Subst = {}
301
302 encrealname = ''
303 try:
304   encrealname = FullName.decode('us-ascii')
305 except UnicodeError:
306   encrealname = str(email.Header.Header(FullName, 'utf-8', 200))
307
308 Subst["__ENCODED_REALNAME__"] = encrealname
309 Subst["__REALNAME__"] = FullName;
310 Subst["__WHOAMI__"] = pwd.getpwuid(os.getuid())[0];
311 Subst["__DATE__"] = time.strftime("%a, %d %b %Y %H:%M:%S +0000",time.gmtime(time.time()));
312 Subst["__LOGIN__"] = account;
313 Subst["__PRIVATE__"] = privsub;
314 Subst["__EMAIL__"] = emailaddr
315 Subst["__PASSWORD__"] = CryptedPass;
316
317 # Submit the modification request
318 Dn = "uid=" + account + "," + BaseDn;
319 print "Updating LDAP directory..",
320 sys.stdout.flush();
321
322 if Update == 0:
323    # New account
324    Details = [("uid",account),
325               ("objectClass", UserObjectClasses),
326               ("uidNumber",str(uidNumber)),
327               ("gidNumber",str(gidNumber)),
328               ("gecos",FullName+",,,,"),
329               ("loginShell","/bin/bash"),
330               ("keyFingerPrint",Keys[0][1]),
331               ("cn",cn),
332               ("sn",sn),
333               ("emailForward",emailaddr),
334               ("shadowLastChange",str(int(time.time()/24/60/60))),
335               ("shadowMin","0"),
336               ("shadowMax","99999"),
337               ("shadowWarning","7"),
338               ("userPassword","{crypt}"+Pass)];
339    if mn:
340       Details.append(("mn",mn));
341    if privsub != " ":
342       Details.append(("privateSub",privsub))
343    if shadowExpire:
344       Details.append(("shadowExpire",str(shadowExpire)))
345    if len(hostacl) > 0:
346       Details.append(("allowedHost",hostacl))
347
348    l.add_s(Dn,Details);
349
350    #Add user group if needed, then the actual user:
351    if UserGroup == 1:
352       Dn = "gid=" + account + "," + BaseDn;
353       l.add_s(Dn,[("gid",account), ("gidNumber",str(gidNumber)), ("objectClass", GroupObjectClasses)])
354 else:
355    # Modification
356    Rec = [(ldap.MOD_REPLACE,"uidNumber",str(uidNumber)),
357           (ldap.MOD_REPLACE,"gidNumber",str(gidNumber)),
358           (ldap.MOD_REPLACE,"gecos",FullName+",,,,"),
359           (ldap.MOD_REPLACE,"loginShell","/bin/bash"),
360           (ldap.MOD_REPLACE,"keyFingerPrint",Keys[0][1]),
361           (ldap.MOD_REPLACE,"cn",cn),
362           (ldap.MOD_REPLACE,"mn",mn),
363           (ldap.MOD_REPLACE,"sn",sn),
364           (ldap.MOD_REPLACE,"emailForward",emailaddr),
365           (ldap.MOD_REPLACE,"shadowLastChange",str(int(time.time()/24/60/60))),
366           (ldap.MOD_REPLACE,"shadowMin","0"),
367           (ldap.MOD_REPLACE,"shadowMax","99999"),
368           (ldap.MOD_REPLACE,"shadowWarning","7"),
369           (ldap.MOD_REPLACE,"shadowInactive",""),
370           (ldap.MOD_REPLACE,"shadowExpire","")];
371    if privsub != " ":
372       Rec.append((ldap.MOD_REPLACE,"privateSub",privsub));
373    if Pass != None:
374       Rec.append((ldap.MOD_REPLACE,"userPassword","{crypt}"+Pass));
375    # Do it
376    l.modify_s(Dn,Rec);
377
378 print;
379
380 # Abort email sends for an update operation
381 if Update == 1 and ForceMail == 0:
382    print "Account is not new, Not sending mails"
383    sys.exit(0);
384
385 # Send the Welcome message
386 print "Sending Welcome Email"
387 templatepath = TemplatesDir + "/welcome-message-%d" % int(gidNumber)
388 if not os.path.exists(templatepath):
389    templatepath = TemplatesDir + "/welcome-message"
390 Reply = TemplateSubst(Subst,open(templatepath, "r").read())
391 Child = os.popen("/usr/sbin/sendmail -t","w");
392 #Child = os.popen("cat","w");
393 Child.write(Reply);
394 if Child.close() != None:
395    raise Error, "Sendmail gave a non-zero return code";
396
397 # vim:set et:
398 # vim:set ts=3:
399 # vim:set shiftwidth=3: