Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 819 lines (670 sloc) 31.236 kb
81e3981 @stedolan rewrote everything :D (I should really learn to make smaller, more fr…
authored
1 # to be renamed...
094710d @stedolan initial commit
authored
2
81e3981 @stedolan rewrote everything :D (I should really learn to make smaller, more fr…
authored
3 from ldapobject import *
4 import pwd, grp, posix, os, stat, time
5 import re
fbbe386 password reset emails
Root authored
6 from sendmail import *
4bec261 username validation fixes, state changes
Root authored
7 import accountrequests
094710d @stedolan initial commit
authored
8
9 def current_session():
10 '''Current session of Netsoc, e.g. "2008-2009"
11
12 The next session starts at the beginning of August, to give us a
13 month or two to fix things. FIXME: should it?
14 '''
9f088fe better user creation/deletion/state changes
Root authored
15 # year, month = time.gmtime()[0:2]
16 # if month >= 10:
17 # year += 1
18 # return "%4d-%4d" % (year-1, year)
19 return Setting("current_session").tcdnetsoc_value.first()
094710d @stedolan initial commit
authored
20
21
81e3981 @stedolan rewrote everything :D (I should really learn to make smaller, more fr…
authored
22 def read_small_file(file):
23 '''Read a small file (e.g. ~/.plan), carefully'''
24 try:
25 f = open(file, "r")
26 # make sure it's actually a file, not a pipe or somesuch
27 st = os.fstat(f.fileno())
28 if stat.S_ISREG(st[stat.ST_MODE]):
29 # fixed upper limit in case someone creates a huge ~/.plan
30 return f.read(1024)
31 else:
32 return None
33 except:
34 # if it doesn't exist, or it's somewhere invalid, etc., then
35 # don't let the exception propagate
36 return None
094710d @stedolan initial commit
authored
37
1f6ed3e iteration over class instances and Samba support
root authored
38 def _get_samba_domain_sid():
39 return LDAPObject(obj_dn='sambaDomainName=NETSOC,dc=netsoc,dc=tcd,dc=ie').sambaSID
40
a0f8cd4 user creation
root authored
41 def generate_password():
42 '''Generate a random password via pwgen'''
43 import subprocess
44 stdout, stderr = subprocess.Popen(["pwgen", "-nc"],stdout=subprocess.PIPE).communicate()
45 return stdout.strip()
1f6ed3e iteration over class instances and Samba support
root authored
46
47
094710d @stedolan initial commit
authored
48
81e3981 @stedolan rewrote everything :D (I should really learn to make smaller, more fr…
authored
49 class NDObject(LDAPObject):
50 base_dn='dc=netsoc,dc=tcd,dc=ie'
51d089f @stedolan minor changes to noshell logic
authored
51 def can_bind(self):
52 return self.get_attribute("userPassword") is not None
094710d @stedolan initial commit
authored
53
81e3981 @stedolan rewrote everything :D (I should really learn to make smaller, more fr…
authored
54 class User(NDObject):
55 '''A member of Netsoc, past or present. Every member corresponds to a User, even the ones
56 without active shell accounts. If a shell account exists for a user (even if it is disabled)
57 user.has_account() will return True. For those users who have an account, their gidNumber
58 refers to their PersonalGroup (see below)'''
59 rdn_attr = 'uid'
a0f8cd4 user creation
root authored
60 default_objectclass = ['tcdnetsoc-person']
7dad23b search improvements
Root authored
61 default_search_attrs = ['cn','uid']
81e3981 @stedolan rewrote everything :D (I should really learn to make smaller, more fr…
authored
62
63 root_DN = "cn=root,dc=netsoc,dc=tcd,dc=ie"
64
cf7d132 @stedolan added user states, ability to create new objects, and user privileges
authored
65
81e3981 @stedolan rewrote everything :D (I should really learn to make smaller, more fr…
authored
66 def __init__(self, uid=None, obj_dn=None):
67 if uid == "root":
68 NDObject.__init__(self, obj_dn = root_DN)
69 else:
70 NDObject.__init__(self, uid, obj_dn = obj_dn)
71
72 @property
73 def project(self):
74 """Read a user's ~/.project file"""
75 return read_small_file(self.homeDirectory + "/.project")
76 @property
77 def plan(self):
78 """Read a user's ~/.plan file"""
79 return read_small_file(self.homeDirectory + "/.plan")
80
81 def has_account(self):
57376a8 proper support for disabling/reenabling accounts
Root authored
82 return 'posixAccount' in self.objectClass
81e3981 @stedolan rewrote everything :D (I should really learn to make smaller, more fr…
authored
83
1f6ed3e iteration over class instances and Samba support
root authored
84 def gen_samba_sid(self):
85 assert self.has_account()
86 return "%s-%s" % (_get_samba_domain_sid(), self.uidNumber * 2 + 1000)
db9810f @stedolan added ldapi:/// support
authored
87
31e7d2c more flexible username validation
Root authored
88 @staticmethod
89 def username_is_valid(name):
4bec261 username validation fixes, state changes
Root authored
90 '''Test whether a potential username is valid. If the username
91 is already taken, this function will return True.
92
93 Valid usernames must match the valid_username_regex setting
94 (i.e. be short and of sensible characters), and must not
95 be one of the bad_usernames (e.g. root)'''
31e7d2c more flexible username validation
Root authored
96 regex = Setting("valid_username_regex").tcdnetsoc_value.first()
97 return \
98 re.match("^" + regex + "$", name) is not None \
99 and \
100 name not in Setting("bad_usernames").tcdnetsoc_value
101
102
81e3981 @stedolan rewrote everything :D (I should really learn to make smaller, more fr…
authored
103 def destroy(self):
a0f8cd4 user creation
root authored
104 # also destroy group
105 g = self.get_personal_group()
106 if g and g.exists():
107 self.get_personal_group().destroy()
81e3981 @stedolan rewrote everything :D (I should really learn to make smaller, more fr…
authored
108 NDObject.destroy(self)
109
dc16486 MySQL accounts
root authored
110 def reset_mysql_pw(self, pw=None):
111 '''Change the MySQL password for a user. When the password is changed,
112 the database is automatically created'''
113 if pw is None:
9f088fe better user creation/deletion/state changes
Root authored
114 pw = self.get_attribute('tcdnetsoc_mysql_pw')
115 if pw is None:
dc16486 MySQL accounts
root authored
116 pw = generate_password()
117 # when this field changes, the update_ldap_mysql script will notice
118 # and update mysql accordingly
119 self.tcdnetsoc_mysql_pw = pw
120
81e3981 @stedolan rewrote everything :D (I should really learn to make smaller, more fr…
authored
121 def get_personal_group(self):
a0f8cd4 user creation
root authored
122 return PersonalGroup(self.uid)
81e3981 @stedolan rewrote everything :D (I should really learn to make smaller, more fr…
authored
123
9f088fe better user creation/deletion/state changes
Root authored
124
125 def mark_member(self):
126 if current_session() not in self.tcdnetsoc_membership_year:
127 self.tcdnetsoc_membership_year += current_session()
128
4bec261 username validation fixes, state changes
Root authored
129 def send_new_account_email(self):
130 '''Sends either the "You've Renewed, Netsoc Still Works" email, or the
131 "Please Finish Signing Up and Get And Account" email.
132
133 Requires that the user is a current member, see mark_member.
134
135 Users who already have shell accounts are assumed to be renewing'''
136 assert current_session() in self.tcdnetsoc_membership_year
137 st = self.get_state()
138 assert st in ["newmember","shell"]
139 if st == "newmember":
140 # create a url and send it to them
141 url = accountrequests.make_signup_url(self)
142 print "Sending account_creation email for %r to %s" % (self,self.mail)
143 sendmail("account_creation", to=self.mail, url=url)
144 else:
145 # just send an email
146 print "Sending account_renewed email for %r to %s" % (self,self.mail)
147 sendmail("account_renewed", to=self.mail, username=self.uid)
148
149 def comment(self, msg):
150 self.tcdnetsoc_admin_comment += '%s: %s' % (time.asctime(), msg)
151
152 def merge_into(self, other):
153 assert self.get_state() == 'newmember'
154 assert other.get_state() in ['shell','renew','bold','expired','dead']
155 assert current_session() in self.tcdnetsoc_membership_year
156 assert current_session() not in other.tcdnetsoc_membership_year
157 issusername = self.get_attribute("tcdnetsoc_ISS_username") or other.get_attribute("tcdnetsoc_ISS_username")
158 if other.get_attribute("tcdnetsoc_ISS_username") is not None:
159 assert other.tcdnetsoc_ISS_username == issusername
160 else:
161 other.tcdnetsoc_ISS_username = issusername
162 if self.cn != other.cn:
163 lwarn("Names %s and %s don't match when renewing account %s" % (self.cn, other.cn, other.uid))
164 other.comment("Renewed by account with non-matching name %s" % self.cn)
165 if self.mail != other.mail:
166 lwarn("Emails %s and %s don't match when renewing account %s"% (self.mail, other.mail, other.uid))
167 other.comment("Renewed by account with non-matching mail %s" % self.mail)
168
169 self.tcdnetsoc_membership_year -= current_session()
170 other.tcdnetsoc_membership_year += current_session()
171 self.destroy()
172
a0f8cd4 user creation
root authored
173 def passwd(self, new, old=None):
174 '''Change the password of a user from "old" to "new". If the old password
175 is not known, "old" can be omitted but changing the password then requires
176 admin permissions.
177
178 See also generate_password.'''
179
180 # We need to do a Password Modify Extended Operation to get Samba passswords
181 # to update properly and to get secure hashing of the password. This requires
182 # the old password. So, if we don't have it, we temporarily reset the password
183 # via directly mungling userPassword, and then to a proper modify exop.
184 if old is None:
185 self.userPassword = new
186 self._raw_passwd(new, new)
187 else:
188 self._raw_passwd(new, old)
87f819e @stedolan added password-changing support
authored
189
fbbe386 password reset emails
Root authored
190 def reset_password(self):
191 if not self.has_account():
192 raise Exception("User account is disabled, password cannot be reset")
193 pw = generate_password()
194 self.passwd(pw)
195 addr = self.get_attribute("mail")
196 if addr is None:
197 lwarn("No mail address recorded for user %s (%s), can't send password reset message" %
198 (self.get_attribute("uid"), self.get_attribute("cn")))
199 else:
200 sendmail("password_reset", to=addr, username=self.uid, password=pw)
201
1a98aa1 @stedolan access control improvements
authored
202 def has_access(self, service):
203 return service.has_access(self)
204
cf7d132 @stedolan added user states, ability to create new objects, and user privileges
authored
205 def has_priv(self, name):
206 return self in Privilege(name)
207
208 @staticmethod
209 def with_priv(self, name):
210 return Privilege(name).member
211
81e3981 @stedolan rewrote everything :D (I should really learn to make smaller, more fr…
authored
212 def info(self):
213 name = self.cn
214 isCurrentMember = current_session() in self.tcdnetsoc_membership_year
215 hasShellAcct = 'posixAccount' in self.objectClass
51d089f @stedolan minor changes to noshell logic
authored
216 canBind = self.can_bind()
81e3981 @stedolan rewrote everything :D (I should really learn to make smaller, more fr…
authored
217 groups = list(self.memberOf)
218 membershipYears = self.tcdnetsoc_membership_year
219 username = self.uid
220 def has(priv):
cf7d132 @stedolan added user states, ability to create new objects, and user privileges
authored
221 if self.has_priv(priv):
81e3981 @stedolan rewrote everything :D (I should really learn to make smaller, more fr…
authored
222 return priv
223 else:
224 return "no " + priv
225 info = "User #%s: %s (%s), %s\n" % (self.uidNumber, username, name, "current member" if isCurrentMember else "not current member")
51d089f @stedolan minor changes to noshell logic
authored
226 if canBind:
227 if hasShellAcct:
4e69b2f @stedolan schema twiddling. *hate* LDAP schemas. *hate* ASN1.
authored
228 info += "has shell account, "+has('webspace')+"\n"
51d089f @stedolan minor changes to noshell logic
authored
229 info += "in groups: " + ", ".join(g.cn for g in self.memberOf) + "\n"
230 else:
231 info += "no shell account\n"
094710d @stedolan initial commit
authored
232 else:
51d089f @stedolan minor changes to noshell logic
authored
233 info += "Disabled account\n"
81e3981 @stedolan rewrote everything :D (I should really learn to make smaller, more fr…
authored
234 info += "Member of netsoc in " + ", ".join(self.tcdnetsoc_membership_year) + "\n"
235 return info
094710d @stedolan initial commit
authored
236
237 def __repr__(self):
9f088fe better user creation/deletion/state changes
Root authored
238 if self.exists():
239 return "<User %s (%s)>" % (self.uid, self.cn)
240 else:
241 return "<no such user>"
81e3981 @stedolan rewrote everything :D (I should really learn to make smaller, more fr…
authored
242
243
244 @staticmethod
245 def myself():
246 return User(pwd.getpwuid(posix.getuid())[0])
247
cf7d132 @stedolan added user states, ability to create new objects, and user privileges
authored
248
249
01d6324 @stedolan minor fixes
authored
250 first_login_shell = "/usr/local/special_shells/accept_AUP"
cf7d132 @stedolan added user states, ability to create new objects, and user privileges
authored
251 homedir_pattern = "/home/%s"
252 default_login_shell = "/bin/bash"
253
57376a8 proper support for disabling/reenabling accounts
Root authored
254 # These are the states for users with accounts
255 # In theory, there is also a state "archived", when the posixAccount objectclass is removed from the
256 # user and their home directories are archived
9f088fe better user creation/deletion/state changes
Root authored
257 states = ['shell','renew','bold','expired','dead','archived','newmember']
8265da7 @stedolan user states
authored
258
cf7d132 @stedolan added user states, ability to create new objects, and user privileges
authored
259 def get_state(self):
260 if self.has_account():
57376a8 proper support for disabling/reenabling accounts
Root authored
261 for state in User.states:
262 if state != "archived" and self in Privilege(state):
263 return state
9f088fe better user creation/deletion/state changes
Root authored
264 assert 0 # should be impossible to get here
265 else:
266 if self.get_attribute("tcdnetsoc_saved_password") == "***newmember***":
267 return "newmember"
268 else:
269 return "archived"
57376a8 proper support for disabling/reenabling accounts
Root authored
270
cf7d132 @stedolan added user states, ability to create new objects, and user privileges
authored
271
57376a8 proper support for disabling/reenabling accounts
Root authored
272 def set_state(self, newst, newpasswd = None):
cf7d132 @stedolan added user states, ability to create new objects, and user privileges
authored
273 assert newst in User.states
274 st = self.get_state()
275 if st == newst:
276 return
9f088fe better user creation/deletion/state changes
Root authored
277 if st == "newmember" and newst not in ["archived","shell","newmember"]:
278 raise Exception("User doesn't have an account, so it can't be set to %s" % newst)
4bec261 username validation fixes, state changes
Root authored
279
280 if (st, newst) == ("archived","newmember"):
281 self.tcdnetsoc_saved_password = "***newmember***"
282 elif newst == "newmember":
9f088fe better user creation/deletion/state changes
Root authored
283 raise Exception("that makes no sense")
284 elif newst == "archived":
57376a8 proper support for disabling/reenabling accounts
Root authored
285 # self.objectClass -= "posixAccount"
286 # # FIXME: remove other privileges as well??
287 # if self.has_priv("shell"):
288 # Privilege("shell").member -= self
289 # del self.userPassword
290 # return
291 raise Exception("archiving accounts not yet implemented")
292
293 elif st == "archived":
294 raise Exception("un-archiving accounts not yet implemented")
295 # if self._has_disabled_shell():
296 # prevstate = self.loginShell[len(User.disabled_shells_base):]
297 # if newst != prevstate:
298 # raise Exception("Trying to change state of %s from disabled to %s, although account was %s" % (self, newst, prevstate))
299 #
300 # assert not self.has_priv("shell")
301 # if not PersonalGroup(self.uid).exists():
302 # PersonalGroup.create(cn=self.uid,
303 # objectClass=["tcdnetsoc-group"],
304 # gidNumber=self.uidNumber,
305 # member=[self])
306 # self.gidNumber = self.uidNumber
307 # self.homeDirectory = User.homedir_pattern % self.uid
308 # self.objectClass += "posixAccount"
309 # Privilege("shell").member += self
310 else:
9f088fe better user creation/deletion/state changes
Root authored
311 if st == "newmember":
312 del self.tcdnetsoc_saved_password
313 else:
314 Privilege(st).member -= self
57376a8 proper support for disabling/reenabling accounts
Root authored
315 Privilege(newst).member += self
316 if newst == "shell":
9f088fe better user creation/deletion/state changes
Root authored
317 # ensure GID is set
318 if self.get_attribute('gidNumber') == None:
319 self.gidNumber = self.uidNumber
320
321 # ensure homedir is set
322 if self.get_attribute('homeDirectory') is None:
323 self.homeDirectory = '/home/' + self.uid
324
325 # ensure group exists
326 self.create_personal_group()
327
328 self.objectClass += 'posixAccount'
329
330 # (re-)enable Samba
331 if st == 'newmember':
332 self.setup_samba_account()
333 else:
334 self.objectClass += 'sambaSamAccount'
335
336 # restore old password (if any)
57376a8 proper support for disabling/reenabling accounts
Root authored
337 if newpasswd is not None:
338 self.passwd(newpasswd)
339 elif self.get_attribute("tcdnetsoc_saved_password") is not None:
340 self.userPassword = self.tcdnetsoc_saved_password
341 else:
342 pwd = generate_password()
9f088fe better user creation/deletion/state changes
Root authored
343 lwarn("Setting password for %s to %s" % ( self.uid, pwd))
57376a8 proper support for disabling/reenabling accounts
Root authored
344 self.passwd(pwd)
9f088fe better user creation/deletion/state changes
Root authored
345
57376a8 proper support for disabling/reenabling accounts
Root authored
346 # set shell if necessary
9f088fe better user creation/deletion/state changes
Root authored
347 if self.get_attribute("loginShell") is None or "special_shell" in self.loginShell or \
348 st in ["bold","dead"]: # get AUP-violating users to re-accept AUP
57376a8 proper support for disabling/reenabling accounts
Root authored
349 self.loginShell = User.first_login_shell
9f088fe better user creation/deletion/state changes
Root authored
350
351 # expired and newmember users get webspace
352 # renew users didn't lose it
353 # and bold/dead users don't get it without admin intervention
354 if st in ["expired", "newmember"]:
355 self.memberOf += Privilege("webspace")
356
357 self.reset_mysql_pw()
358
359 # FIXME quotas
cf7d132 @stedolan added user states, ability to create new objects, and user privileges
authored
360 else:
9f088fe better user creation/deletion/state changes
Root authored
361 # can't be a new member if the account is being set to [rewew,expired,bold,dead]
362 # since you need to have had an account for those things to happen
363 assert st != "newmember"
57376a8 proper support for disabling/reenabling accounts
Root authored
364 # removing shell, save old password
365 if self.can_bind():
366 self.tcdnetsoc_saved_password = self.userPassword
367 del self.userPassword
368
369 # possibly remove privileges
370 if newst in ["bold","dead"]:
371 for g in list(self.memberOf):
372 if isinstance(g, Privilege) and g.cn != newst:
373 self.memberOf -= g
374
375 # remove samba access
376 if 'sambaSamAccount' in self.objectClass:
377 self.objectClass -= 'sambaSamAccount'
cf7d132 @stedolan added user states, ability to create new objects, and user privileges
authored
378
379 def get_correct_state(self):
380 # Does this person automatically get a shell?
51d089f @stedolan minor changes to noshell logic
authored
381 noexpire = self.has_priv("noexpire")
cf7d132 @stedolan added user states, ability to create new objects, and user privileges
authored
382
383 # Can this person sign up even if they've left college?
384 alwaysrenewable = self.has_priv("alwaysrenewable")
385
386 # Is this person a current TCD student/staff member?
387 current_tcd = True # FIXME
388
389 # Has this person paid the membership fee this year?
390 current_member = current_session() in self.tcdnetsoc_membership_year
391
51d089f @stedolan minor changes to noshell logic
authored
392 entitled_to_renew = noexpire or alwaysrenewable or current_tcd
393 entitled_to_shell = noexpire or (current_member and current_tcd)
cf7d132 @stedolan added user states, ability to create new objects, and user privileges
authored
394
395 st = self.get_state()
57376a8 proper support for disabling/reenabling accounts
Root authored
396 if st in ["shell", "renew", "expired"]:
8265da7 @stedolan user states
authored
397 if entitled_to_shell:
57376a8 proper support for disabling/reenabling accounts
Root authored
398 s = "shell"
8265da7 @stedolan user states
authored
399 else:
cf7d132 @stedolan added user states, ability to create new objects, and user privileges
authored
400 if entitled_to_renew:
401 s = "renew"
402 else:
403 s = "expired"
9f088fe better user creation/deletion/state changes
Root authored
404 elif st == "newmember":
405 # new members' accounts stay as they are until they set up a real account
406 s = "newmember"
57376a8 proper support for disabling/reenabling accounts
Root authored
407 elif st == "archived":
9f088fe better user creation/deletion/state changes
Root authored
408 # archived accounts stay archived
57376a8 proper support for disabling/reenabling accounts
Root authored
409 s = "archived"
cf7d132 @stedolan added user states, ability to create new objects, and user privileges
authored
410 elif st == "bold":
9f088fe better user creation/deletion/state changes
Root authored
411 # bold accounts only get re-enabled with admin intervention
cf7d132 @stedolan added user states, ability to create new objects, and user privileges
authored
412 s = "bold"
413 elif st == "dead":
9f088fe better user creation/deletion/state changes
Root authored
414 # dead accounts only get re-enabled (or more likely, archived) with admin intervention
cf7d132 @stedolan added user states, ability to create new objects, and user privileges
authored
415 s = "dead"
416 return s
417
81e3981 @stedolan rewrote everything :D (I should really learn to make smaller, more fr…
authored
418 def check(self):
419 assert 'tcdnetsoc-person' in self.objectClass
57376a8 proper support for disabling/reenabling accounts
Root authored
420
9f088fe better user creation/deletion/state changes
Root authored
421 stategroups = [Privilege(x) for x in User.states if x not in ["archived","newmember"]]
57376a8 proper support for disabling/reenabling accounts
Root authored
422 currentstategroups = [g for g in stategroups if self in g]
423
8265da7 @stedolan user states
authored
424 st = self.get_state()
9f088fe better user creation/deletion/state changes
Root authored
425 if st in ["archived","newmember"]:
8265da7 @stedolan user states
authored
426 assert not self.has_account()
57376a8 proper support for disabling/reenabling accounts
Root authored
427 assert not self.can_bind()
428 assert len(currentstategroups) == 0
8265da7 @stedolan user states
authored
429 else:
430 assert self.has_account()
57376a8 proper support for disabling/reenabling accounts
Root authored
431 if st == "shell":
432 assert self.can_bind()
433 assert 'sambaSamAccount' in self.objectClass
434 assert len(currentstategroups) == 1 and currentstategroups[0].cn == st
435
81e3981 @stedolan rewrote everything :D (I should really learn to make smaller, more fr…
authored
436 assert self.gidNumber == self.uidNumber
a0f8cd4 user creation
root authored
437 assert self.get_personal_group().exists()
81e3981 @stedolan rewrote everything :D (I should really learn to make smaller, more fr…
authored
438
57376a8 proper support for disabling/reenabling accounts
Root authored
439
1f6ed3e iteration over class instances and Samba support
root authored
440 assert self.sambaSID == self.gen_samba_sid()
441 assert self.get_personal_group().sambaSID == self.sambaPrimaryGroupSID
442
a0f8cd4 user creation
root authored
443 @classmethod
444 def create(cls, **attrs):
445 '''Create a new user. Users are always created in the "active" state, i.e.
446 they have a shell, webspace, etc. Requires that a username (uid), full name
447 (cn) and email address (mail) be chosen, all other attributes will be given
448 correct defaults.
449
450 If a password is not specified (userPassword), a random one will be
451 generated.
452
453 If a uidNumber is not specified, a new one will be allocated. If a gidNumber
454 is specified, it must match the uidNumber and it will be taken to mean that
455 the group has already been created.
456
457 For users who are College students, a tcdnetsoc_ISS_username should be
458 specified.
459
460 By default, newly-created accounts will be marked as members for the curent
461 year. If this is not desired, specify "tcdnetsoc_membership_year=[]".
462
463 Disk quotas are set to the default for each filesystem, they can be changed
464 via User.quota.
465
466 TLDR: User.create(uid="foo",
467 cn="Foo Barbaz",
468 mail="foo@barbaz.com",
469 tcdnetsoc_ISS_username="foob")'''
9f088fe better user creation/deletion/state changes
Root authored
470 for a in ['cn','mail']:
a0f8cd4 user creation
root authored
471 if a not in attrs:
9f088fe better user creation/deletion/state changes
Root authored
472 raise Exception("Users must have a '%s'" % a)
473
474 if 'uidNumber' not in attrs:
475 attrs['uidNumber'] = UIDAllocator.alloc()
476
4bec261 username validation fixes, state changes
Root authored
477 if 'uid' not in attrs or attrs['uid'] == "":
9f088fe better user creation/deletion/state changes
Root authored
478 attrs['uid'] = "user%d" % attrs['uidNumber']
479
480 if 'tcdnetsoc_membership_year' not in attrs:
481 attrs['tcdnetsoc_membership_year'] = [current_session()]
a0f8cd4 user creation
root authored
482 if User(attrs['uid']).exists():
483 raise Exception("Uid %s is taken" % attrs['uid'])
31e7d2c more flexible username validation
Root authored
484 if not User.username_is_valid(attrs['uid']):
a0f8cd4 user creation
root authored
485 raise Exception("Invalid username %s" % attrs['uid'])
9f088fe better user creation/deletion/state changes
Root authored
486
487 attrs['tcdnetsoc_saved_password'] = '***newmember***'
488 u = super(User,cls).create(**attrs)
489
490 return u
491
492 def create_personal_group(self):
493 '''Create the personal group for this user, a group containing only them.
494 Used as the default group for their files.
495
496 (You should never need to call this directly)'''
497
498 if self.get_attribute('gidNumber') is None:
499 self.gidNumber = self.uidNumber
500 if self.get_personal_group().exists():
501 return # no need to create it
502 g = PersonalGroup.create(cn = self.uid,
503 member = [self],
504 gidNumber = self.gidNumber)
505 g.sambaSID = g.gen_samba_sid()
506 g.sambaGroupType = 2
507 g.objectClass += 'sambaGroupMapping'
508
509 def setup_samba_account(self):
510 '''Set up a Samba account for this user (samba cannot use standard Posix account
511 info for reasons best known to the "designers" of the SMB protocol).
512
513 Note: the password must be changed, even to the same value, after this is called.
514
515 (You should never need to call this directly)'''
516 assert 'posixAccount' in self.objectClass
517 if self.get_attribute('sambaSID') is None:
518 self.sambaSID = self.gen_samba_sid()
519 if self.get_attribute('sambaPrimaryGroupSID') is None:
520 self.sambaPrimaryGroupSID = self.get_personal_group().gen_samba_sid()
521 if 'sambaSamAccount' not in self.objectClass:
522 self.objectClass += 'sambaSamAccount'
523
524
525 def addshell(self, **attrs):
526 if self.get_attribute('gidNumber') is None:
a0f8cd4 user creation
root authored
527 mkgrp = True
9f088fe better user creation/deletion/state changes
Root authored
528 self.gidNumber = self.uidNumber
529 if self.get_attribute('homeDirectory') is None:
530 self.homeDirectory = '/home/' + self.uid
531
532 if mkgrp:
533 self.create_personal_group()
534
535
536 if 'loginShell' not in attrs or "special_shell" in attrs['loginShell']:
a0f8cd4 user creation
root authored
537 attrs['loginShell'] = User.first_login_shell
9f088fe better user creation/deletion/state changes
Root authored
538
539
540 self.objectClass += 'posixAccount'
541
542 self.setup_samba_account()
543
544
a0f8cd4 user creation
root authored
545 if 'userPassword' in attrs:
546 password = attrs['userPassword']
547 del attrs['userPassword']
548 else:
549 password = generate_password()
550
9f088fe better user creation/deletion/state changes
Root authored
551 print "Password for %s set to %s" % (self.uid, password)
552 self.passwd(password)
a0f8cd4 user creation
root authored
553
554
555
556 u.memberOf += Privilege("shell")
557 u.memberOf += Privilege("webspace")
558 for fs, q in User.default_quotas.iteritems():
559 u.quota(fs).set(q)
0e671f2 create mysql dbs for new users
root authored
560
561 u.reset_mysql_pw()
562
a0f8cd4 user creation
root authored
563 return u
1f6ed3e iteration over class instances and Samba support
root authored
564
15b2b67 @stedolan disk quotas
authored
565 # Disk quotas
566 class fs:
567 home = "cuberoot.netsoc.tcd.ie:/srv/userhome"
a0f8cd4 user creation
root authored
568 webspace = "cuberoot.netsoc.tcd.ie:/srv/userweb"
569 default_quotas = {
570 fs.home: "4G",
571 fs.webspace: "1G"
572 }
15b2b67 @stedolan disk quotas
authored
573
574 def quota(self, fs):
575 return User.Quota(self, fs)
576
577 class Quota:
578 def __init__(self, user, fs):
579 self.user = user
580 self.fs = fs
581
582 _sizes = {'T': 1024 ** 4, 'G': 1024 ** 3, 'M': 1024 ** 2, 'K': 1024}
583 # bytes <-> human-readable size conversions
584 @staticmethod
585 def parse_size(sz):
586 if sz == "unlimited": return 0
587 sz = str(sz)
588 m=1
589 for s in User.Quota._sizes:
590 if sz.endswith(s):
591 m = User.Quota._sizes[s]
592 sz = sz[0:-1]
593 break
594 return int(float(sz) * m)
595 @staticmethod
596 def write_size(sz):
597 if sz == 0: return "unlimited"
598 sz = float(sz)
599 suffix = ""
600 for name,s in reversed(sorted(User.Quota._sizes.iteritems(), key=lambda (a,b):b)):
601 if sz > 0.9 * s:
602 suffix = name
603 sz /= float(s)
604 break
605 return "%.1f%s" % (sz, name)
606
607 def _get_quota(self):
608 for i in self.user.tcdnetsoc_diskquota:
609 if i.startswith(self.fs + ":"):
610 return [int(x) for x in i.split(":")[2:6]]
611 return None, None, None, None
612 def _set_quota(self, l):
613 for i in self.user.tcdnetsoc_diskquota:
614 if i.startswith(self.fs + ":"):
615 self.user.tcdnetsoc_diskquota -= i
616 self.user.tcdnetsoc_diskquota += ":".join([self.fs] + [str(x) for x in l])
617 def _get_usage(self):
618 for i in self.user.tcdnetsoc_diskusage:
619 if i.startswith(self.fs + ":"):
620 return [int(x) for x in i.split(":")[2:]]
621 return None, None, None, None, None, None
622
623 def set(self, sz, extra_size=10, bytes_per_inode=10*1024, inode_extra_size=10):
624 sz = self.parse_size(sz)
625 szlimit = sz / 1024 # max size in 1k blocks
626 inodelimit = float(sz) / float(bytes_per_inode) # inode limit
627 self._set_quota([
628 szlimit, # size in 1k blocks
629 int(float(szlimit) * (1 + 0.01 * extra_size)), # hardlimit
630 int(inodelimit), # max no. of inodes
631 int(inodelimit * (1 + 0.01 * inode_extra_size)) # inode hardlimit
632 ])
633
634 def __repr__(self):
635 blocksoft, blockhard, inodesoft, inodehard = self._get_quota()
636 blockused, xblocksoft, xblockhard, inodeused, xinodesoft, xinodehard = self._get_usage()
637 if blocksoft is None:
638 return "no quota set"
639 if blockused is None:
f760808 @stedolan minor quota bugfix
authored
640 return "%s [no usage data]" % self.write_size(blocksoft*1024)
15b2b67 @stedolan disk quotas
authored
641 s = "%s of %s (%d%%)" % (
f760808 @stedolan minor quota bugfix
authored
642 self.write_size(blockused*1024 if blockused > 0 else "0"),
15b2b67 @stedolan disk quotas
authored
643 self.write_size(blocksoft*1024),
644 100.0 * blockused / blocksoft)
645 if xblocksoft != blocksoft or xinodesoft != inodesoft or \
646 xblockhard != blockhard or xinodehard != inodehard:
647 s += " [with changes not yet applied]"
648 return s
649
650
651
cf7d132 @stedolan added user states, ability to create new objects, and user privileges
authored
652
81e3981 @stedolan rewrote everything :D (I should really learn to make smaller, more fr…
authored
653 class Group(NDObject):
654 '''A group of users. Groups may contain any number of users, including zero'''
655 rdn_attr = 'cn'
a0f8cd4 user creation
root authored
656 default_objectclass = ['tcdnetsoc-group']
81e3981 @stedolan rewrote everything :D (I should really learn to make smaller, more fr…
authored
657
658 # Allow "user in group" and "for user in group" as shorthands for
659 # "user in group.member" and "for user in group.member"
660 def __contains__(self, obj):
661 return obj in self.member
662 def __iter__(self):
663 return iter(self.member)
664
1f6ed3e iteration over class instances and Samba support
root authored
665 def gen_samba_sid(self):
666 return "%s-%s" % (_get_samba_domain_sid(), self.gidNumber * 2 + 1001)
667
668
669 def check(self):
670 if 'sambaGroupMapping' in self.objectClass:
671 assert self.sambaGroupType == 2
672 assert self.sambaSID == self.gen_samba_sid()
673
84d0d30 better creation of groups/privileges (auto-alloc GID)
Root authored
674 @classmethod
675 def create(cls, **attrs):
676 if 'gidNumber' not in attrs:
677 attrs['gidNumber'] = GIDAllocator.alloc()
678 return super(Group, cls).create(**attrs)
679
1f6ed3e iteration over class instances and Samba support
root authored
680
81e3981 @stedolan rewrote everything :D (I should really learn to make smaller, more fr…
authored
681 class PersonalGroup(Group):
682 '''A PersonalGroup is a group with the same name as a user having only that user
683 as a member. Its GID is the UID of the user and its name is the username of the user'''
684 rdn_attr = 'cn'
a0f8cd4 user creation
root authored
685 default_objectclass = ['tcdnetsoc-group']
81e3981 @stedolan rewrote everything :D (I should really learn to make smaller, more fr…
authored
686
687 def get_user(self):
688 return User(self.cn)
689
690
691 def check(self):
692 assert 'tcdnetsoc-group' in self.objectClass
693 user = self.get_user()
e6e07c1 @stedolan minor fix
authored
694 assert user.exists()
81e3981 @stedolan rewrote everything :D (I should really learn to make smaller, more fr…
authored
695 assert user.gidNumber == self.gidNumber
696 assert len(self.member) == 1
697 assert user in self
1f6ed3e iteration over class instances and Samba support
root authored
698 assert 'sambaGroupMapping' in self.objectClass
a0f8cd4 user creation
root authored
699
81e3981 @stedolan rewrote everything :D (I should really learn to make smaller, more fr…
authored
700
cf7d132 @stedolan added user states, ability to create new objects, and user privileges
authored
701 class Privilege(Group):
702 '''Groups controlling access to specific services, for instance webspace or
703 filestorage'''
704 rdn_attr = 'cn'
84d0d30 better creation of groups/privileges (auto-alloc GID)
Root authored
705 default_objectclass = ['tcdnetsoc-privilege']
cc3c311 @stedolan added Privilege and Service classes
authored
706 def check(self):
707 assert 'tcdnetsoc-privilege' in self.objectClass
81e3981 @stedolan rewrote everything :D (I should really learn to make smaller, more fr…
authored
708
709
cc3c311 @stedolan added Privilege and Service classes
authored
710 class Service(NDObject):
711 rdn_attr = 'cn'
a0f8cd4 user creation
root authored
712 default_objectclass = ['tcdnetsoc-service']
cc3c311 @stedolan added Privilege and Service classes
authored
713 def get_password(self):
714 return self.get_attribute("userPassword")
1a98aa1 @stedolan access control improvements
authored
715
716 def has_access(self, user):
717 return len(list(Privilege.search(SearchFilter.all(tcdnetsoc_service_granted=self,
718 member=user)))) != 0
f554f36 autogeneration of passwords for service accounts
root authored
719 @classmethod
720 def create(cls, **attrs):
721 if 'userPassword' not in attrs:
722 attrs['userPassword'] = generate_password()
723 o = super(Service,cls).create(**attrs)
724 print "Generated password '%s' for %s" % (attrs['userPassword'], o.cn)
725 return o
726
cc3c311 @stedolan added Privilege and Service classes
authored
727
728
81e3981 @stedolan rewrote everything :D (I should really learn to make smaller, more fr…
authored
729 class IDNumber(NDObject):
730 """Allocator for new ID numbers such as UID and GID.
731 The next ID is stored in the allocator object, and when a new one is requested
732 the field is atomically incremented and the old value is returned"""
733 rdn_attr = 'cn'
a0f8cd4 user creation
root authored
734 default_objectclass = ['tcdnetsoc-idnum']
81e3981 @stedolan rewrote everything :D (I should really learn to make smaller, more fr…
authored
735 def _setnum(self, old, new):
736 # Minor hack: we use _raw_modattrs to ensure atomicity
737 # Without it, there's a race condition
738 self._raw_modattrs([
739 (ldap.MOD_DELETE, 'serialNumber', str(old)),
740 (ldap.MOD_ADD, 'serialNumber', str(new))])
741
742 def alloc(self):
743 # try to atomically allocate a new number (UID, GID, etc)
744 # attempt it 3 times in case it fails because someone else
745 # is also allocating numbers
746 for attempt in range(3):
747 currid = self.serialNumber
748 try:
749 self._setnum(currid, currid+1)
750 except ldap.NO_SUCH_ATTRIBUTE, e:
751 time.sleep(random.random() * 0.1)
752 continue
753 return currid
754 raise e
755
756 def check(self):
757 assert 'tcdnetsoc-idnum' in self.objectClass
758
759
760 UIDAllocator = IDNumber('next-uid')
761 GIDAllocator = IDNumber('next-gid')
762
763
a0513d5 storage of arbitrary key-value settings in ldap (currently the regex …
Root authored
764 class Setting(NDObject):
765 """Arbitrary configuration-style key-value setting, stored in LDAP to be accessible from all Netsoc machines"""
766 rdn_attr = 'cn'
767 default_objectclass = ['tcdnetsoc-setting']
768 def _setnum(self, old, new):
769 # Minor hack: we use _raw_modattrs to ensure atomicity
770 # Without it, there's a race condition
771 self._raw_modattrs([
772 (ldap.MOD_DELETE, 'tcdnetsoc-value', str(old)),
773 (ldap.MOD_ADD, 'tcdnetsoc-value', str(new))])
774
775 def alloc(self):
776 # try to atomically allocate a new number (UID, GID, etc)
777 # attempt it 3 times in case it fails because someone else
778 # is also allocating numbers
779 for attempt in range(3):
780 currid = int(self.tcdnetsoc_value.first())
781 try:
782 self._setnum(currid, currid+1)
783 except ldap.NO_SUCH_ATTRIBUTE, e:
784 time.sleep(random.random() * 0.1)
785 continue
786 return currid
787 raise e
788
789 def check(self):
790 assert 'tcdnetsoc-setting' in self.objectClass
791
792
793
81e3981 @stedolan rewrote everything :D (I should really learn to make smaller, more fr…
authored
794 Attribute('objectClass', [str])
795 Attribute('serialNumber', int)
796 Attribute('tcdnetsoc_membership_year', [str])
db9810f @stedolan added ldapi:/// support
authored
797 Attribute('tcdnetsoc_ISS_username', str)
cf7d132 @stedolan added user states, ability to create new objects, and user privileges
authored
798 Attribute('loginShell', str)
7dad23b search improvements
Root authored
799 Attribute('uid', str, match_like)
81e3981 @stedolan rewrote everything :D (I should really learn to make smaller, more fr…
authored
800 Attribute('uidNumber', int)
801 Attribute('gidNumber', int)
802 Attribute('homeDirectory', str)
7dad23b search improvements
Root authored
803 Attribute('cn', str, match_like)
cc3c311 @stedolan added Privilege and Service classes
authored
804 Attribute('userPassword', str)
805 Attribute('mail', str)
806 Attribute('tcdnetsoc_admin_comment', [str])
81e3981 @stedolan rewrote everything :D (I should really learn to make smaller, more fr…
authored
807 Attribute('member', [User])
808 Attribute('memberOf', [Group], backlink='member')
cc3c311 @stedolan added Privilege and Service classes
authored
809 Attribute('tcdnetsoc_service_granted', [Service])
810 Attribute('tcdnetsoc_granted_by_privilege', [Privilege], backlink='tcdnetsoc_service_granted')
15b2b67 @stedolan disk quotas
authored
811 Attribute('tcdnetsoc_diskquota', [str])
812 Attribute('tcdnetsoc_diskusage', [str])
a0513d5 storage of arbitrary key-value settings in ldap (currently the regex …
Root authored
813 Attribute('tcdnetsoc_value', [str])
1f6ed3e iteration over class instances and Samba support
root authored
814 Attribute('sambaSID', str)
815 Attribute('sambaPrimaryGroupSID', str)
816 Attribute('sambaGroupType', int)
dc16486 MySQL accounts
root authored
817 Attribute('tcdnetsoc_mysql_pw', str)
4bec261 username validation fixes, state changes
Root authored
818 Attribute('tcdnetsoc_saved_password', str)
Something went wrong with that request. Please try again.