Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 335 lines (274 sloc) 11.38 kb
81e3981 Stephen Dolan rewrote everything :D (I should really learn to make smaller, more frequ...
authored
1 # to be renamed...
094710d Stephen Dolan initial commit
authored
2
81e3981 Stephen Dolan rewrote everything :D (I should really learn to make smaller, more frequ...
authored
3 from ldapobject import *
4 import pwd, grp, posix, os, stat, time
5 import re
094710d Stephen Dolan initial commit
authored
6
7 def current_session():
8 '''Current session of Netsoc, e.g. "2008-2009"
9
10 The next session starts at the beginning of August, to give us a
11 month or two to fix things. FIXME: should it?
12 '''
13 year, month = time.gmtime()[0:2]
14 if month >= 8:
15 year += 1
16 return "%4d-%4d" % (year-1, year)
17
18
81e3981 Stephen Dolan rewrote everything :D (I should really learn to make smaller, more frequ...
authored
19 def read_small_file(file):
20 '''Read a small file (e.g. ~/.plan), carefully'''
21 try:
22 f = open(file, "r")
23 # make sure it's actually a file, not a pipe or somesuch
24 st = os.fstat(f.fileno())
25 if stat.S_ISREG(st[stat.ST_MODE]):
26 # fixed upper limit in case someone creates a huge ~/.plan
27 return f.read(1024)
28 else:
29 return None
30 except:
31 # if it doesn't exist, or it's somewhere invalid, etc., then
32 # don't let the exception propagate
33 return None
094710d Stephen Dolan initial commit
authored
34
35
81e3981 Stephen Dolan rewrote everything :D (I should really learn to make smaller, more frequ...
authored
36 class NDObject(LDAPObject):
37 base_dn='dc=netsoc,dc=tcd,dc=ie'
51d089f Stephen Dolan minor changes to noshell logic
authored
38 def can_bind(self):
39 return self.get_attribute("userPassword") is not None
094710d Stephen Dolan initial commit
authored
40
81e3981 Stephen Dolan rewrote everything :D (I should really learn to make smaller, more frequ...
authored
41 class User(NDObject):
42 '''A member of Netsoc, past or present. Every member corresponds to a User, even the ones
43 without active shell accounts. If a shell account exists for a user (even if it is disabled)
44 user.has_account() will return True. For those users who have an account, their gidNumber
45 refers to their PersonalGroup (see below)'''
46 rdn_attr = 'uid'
47
48 valid_username = re.compile("^[a-z_][a-z0-9_-]*$")
49 root_DN = "cn=root,dc=netsoc,dc=tcd,dc=ie"
50
cf7d132 Stephen Dolan added user states, ability to create new objects, and user privileges
authored
51
81e3981 Stephen Dolan rewrote everything :D (I should really learn to make smaller, more frequ...
authored
52 def __init__(self, uid=None, obj_dn=None):
53 if uid == "root":
54 NDObject.__init__(self, obj_dn = root_DN)
55 else:
56 NDObject.__init__(self, uid, obj_dn = obj_dn)
57
58 @property
59 def project(self):
60 """Read a user's ~/.project file"""
61 return read_small_file(self.homeDirectory + "/.project")
62 @property
63 def plan(self):
64 """Read a user's ~/.plan file"""
65 return read_small_file(self.homeDirectory + "/.plan")
66
cf7d132 Stephen Dolan added user states, ability to create new objects, and user privileges
authored
67
68 def get_full_name(self):
69 gecos = self.get_attribute("gecos")
70 if gecos is None: return None
71 if "," in gecos: return gecos.split(",")[0]
72 return gecos
73
81e3981 Stephen Dolan rewrote everything :D (I should really learn to make smaller, more frequ...
authored
74 def has_account(self):
01d6324 Stephen Dolan minor fixes
authored
75 return 'posixAccount' in self.objectClass and self.can_bind()
81e3981 Stephen Dolan rewrote everything :D (I should really learn to make smaller, more frequ...
authored
76
db9810f Stephen Dolan added ldapi:/// support
authored
77
81e3981 Stephen Dolan rewrote everything :D (I should really learn to make smaller, more frequ...
authored
78 def destroy(self):
79 if self.has_account() and os.access(self.homeDirectory, os.F_OK):
80 raise Exception("Cannot destroy user %s since home directory still exists" % self)
81 NDObject.destroy(self)
82
83 def get_personal_group(self):
84 if self.has_account():
85 return PersonalGroup(self.uid)
094710d Stephen Dolan initial commit
authored
86 else:
81e3981 Stephen Dolan rewrote everything :D (I should really learn to make smaller, more frequ...
authored
87 return None
88
87f819e Stephen Dolan added password-changing support
authored
89 def passwd(self, old, new):
90 self._raw_passwd(old, new)
91
cf7d132 Stephen Dolan added user states, ability to create new objects, and user privileges
authored
92 def has_priv(self, name):
93 return self in Privilege(name)
94
95 @staticmethod
96 def with_priv(self, name):
97 return Privilege(name).member
98
81e3981 Stephen Dolan rewrote everything :D (I should really learn to make smaller, more frequ...
authored
99 def info(self):
100 name = self.cn
101 isCurrentMember = current_session() in self.tcdnetsoc_membership_year
102 hasShellAcct = 'posixAccount' in self.objectClass
51d089f Stephen Dolan minor changes to noshell logic
authored
103 canBind = self.can_bind()
81e3981 Stephen Dolan rewrote everything :D (I should really learn to make smaller, more frequ...
authored
104 groups = list(self.memberOf)
105 membershipYears = self.tcdnetsoc_membership_year
106 username = self.uid
107 def has(priv):
cf7d132 Stephen Dolan added user states, ability to create new objects, and user privileges
authored
108 if self.has_priv(priv):
81e3981 Stephen Dolan rewrote everything :D (I should really learn to make smaller, more frequ...
authored
109 return priv
110 else:
111 return "no " + priv
112 info = "User #%s: %s (%s), %s\n" % (self.uidNumber, username, name, "current member" if isCurrentMember else "not current member")
51d089f Stephen Dolan minor changes to noshell logic
authored
113 if canBind:
114 if hasShellAcct:
115 info += "has shell account, "+has('webspace')+", "+has('filestorage')+"\n"
116 info += "in groups: " + ", ".join(g.cn for g in self.memberOf) + "\n"
117 else:
118 info += "no shell account\n"
094710d Stephen Dolan initial commit
authored
119 else:
51d089f Stephen Dolan minor changes to noshell logic
authored
120 info += "Disabled account\n"
81e3981 Stephen Dolan rewrote everything :D (I should really learn to make smaller, more frequ...
authored
121 info += "Member of netsoc in " + ", ".join(self.tcdnetsoc_membership_year) + "\n"
122 return info
094710d Stephen Dolan initial commit
authored
123
124 def __repr__(self):
81e3981 Stephen Dolan rewrote everything :D (I should really learn to make smaller, more frequ...
authored
125 return "<User %s (%s)>" % (self.uid, self.cn)
126
127
128 @staticmethod
129 def myself():
130 return User(pwd.getpwuid(posix.getuid())[0])
131
cf7d132 Stephen Dolan added user states, ability to create new objects, and user privileges
authored
132
133
134 disabled_shells = ['renew','bold','expired','dead']
01d6324 Stephen Dolan minor fixes
authored
135 disabled_shells_base = "/usr/local/special_shells/"
136 first_login_shell = "/usr/local/special_shells/accept_AUP"
cf7d132 Stephen Dolan added user states, ability to create new objects, and user privileges
authored
137 homedir_pattern = "/home/%s"
138 default_login_shell = "/bin/bash"
51d089f Stephen Dolan minor changes to noshell logic
authored
139 states = ['active','disabled','renew','bold','expired','dead']
cf7d132 Stephen Dolan added user states, ability to create new objects, and user privileges
authored
140
8265da7 Stephen Dolan user states
authored
141 def _has_disabled_shell(self):
142 sh = self.get_attribute("loginShell")
143 return sh is not None and sh != User.first_login_shell and sh.startswith(User.disabled_shells_base)
144
cf7d132 Stephen Dolan added user states, ability to create new objects, and user privileges
authored
145 def get_state(self):
146 if self.has_account():
8265da7 Stephen Dolan user states
authored
147 disabled_shell = self._has_disabled_shell()
cf7d132 Stephen Dolan added user states, ability to create new objects, and user privileges
authored
148 if self.has_priv("shell"):
149 if disabled_shell:
8265da7 Stephen Dolan user states
authored
150 lerr(repr(self) + " is active, but has shell " + self.loginShell)
cf7d132 Stephen Dolan added user states, ability to create new objects, and user privileges
authored
151 return "active"
152 else:
153 if not disabled_shell:
8265da7 Stephen Dolan user states
authored
154 lerr(repr(self) + " is disabled, but has shell " + self.loginShell)
cf7d132 Stephen Dolan added user states, ability to create new objects, and user privileges
authored
155 return "bold" # abitrary default, this shouldn't happen
156 else:
157 return sh[len(User.disabled_shells_base):]
158 else:
51d089f Stephen Dolan minor changes to noshell logic
authored
159 return "disabled"
cf7d132 Stephen Dolan added user states, ability to create new objects, and user privileges
authored
160
161 def set_state(self, newst):
162 assert newst in User.states
163 st = self.get_state()
164 if st == newst:
165 return
51d089f Stephen Dolan minor changes to noshell logic
authored
166 if newst == "disabled":
cf7d132 Stephen Dolan added user states, ability to create new objects, and user privileges
authored
167 self.objectClass -= "posixAccount"
8265da7 Stephen Dolan user states
authored
168 # FIXME: remove other privileges as well??
cf7d132 Stephen Dolan added user states, ability to create new objects, and user privileges
authored
169 if self.has_priv("shell"):
170 Privilege("shell").member -= self
51d089f Stephen Dolan minor changes to noshell logic
authored
171 del self.userPassword
cf7d132 Stephen Dolan added user states, ability to create new objects, and user privileges
authored
172 return
173
51d089f Stephen Dolan minor changes to noshell logic
authored
174 if st == "disabled":
e6e07c1 Stephen Dolan minor fix
authored
175 if self._has_disabled_shell():
8265da7 Stephen Dolan user states
authored
176 prevstate = self.loginShell[len(User.disabled_shells_base):]
177 if newst != prevstate:
51d089f Stephen Dolan minor changes to noshell logic
authored
178 raise Exception("Trying to change state of %s from disabled to %s, although account was %s" % (self, newst, prevstate))
8265da7 Stephen Dolan user states
authored
179
cf7d132 Stephen Dolan added user states, ability to create new objects, and user privileges
authored
180 assert not self.has_priv("shell")
181 if not PersonalGroup(self.uid).exists():
182 PersonalGroup.create(cn=self.uid,
183 objectClass=["tcdnetsoc-group"],
184 gidNumber=self.uidNumber,
185 member=[self])
186 self.gidNumber = self.uidNumber
187 self.homeDirectory = User.homedir_pattern % self.uid
188 self.objectClass += "posixAccount"
189 Privilege("shell").member += self
190
191 sh = self.get_attribute("loginShell")
192 if newst == "active":
193 if sh is None:
194 newsh = User.first_login_shell
195 else:
196 newsh = User.default_login_shell
197 else:
198 newsh = User.disabled_shells_base + newst
199
200 self.loginShell = newsh
201
202 def get_correct_state(self):
203 # Does this person automatically get a shell?
51d089f Stephen Dolan minor changes to noshell logic
authored
204 noexpire = self.has_priv("noexpire")
cf7d132 Stephen Dolan added user states, ability to create new objects, and user privileges
authored
205
206 # Can this person sign up even if they've left college?
207 alwaysrenewable = self.has_priv("alwaysrenewable")
208
209 # Is this person a current TCD student/staff member?
210 current_tcd = True # FIXME
211
212 # Has this person paid the membership fee this year?
213 current_member = current_session() in self.tcdnetsoc_membership_year
214
51d089f Stephen Dolan minor changes to noshell logic
authored
215 entitled_to_renew = noexpire or alwaysrenewable or current_tcd
216 entitled_to_shell = noexpire or (current_member and current_tcd)
cf7d132 Stephen Dolan added user states, ability to create new objects, and user privileges
authored
217
218 st = self.get_state()
219 if st in ["active", "renew", "expired"]:
8265da7 Stephen Dolan user states
authored
220 if entitled_to_shell:
221 s = "active"
222 else:
cf7d132 Stephen Dolan added user states, ability to create new objects, and user privileges
authored
223 if entitled_to_renew:
224 s = "renew"
225 else:
226 s = "expired"
51d089f Stephen Dolan minor changes to noshell logic
authored
227 elif st == "disabled":
01d6324 Stephen Dolan minor fixes
authored
228 if not self._has_disabled_shell():
8265da7 Stephen Dolan user states
authored
229 s = "active"
230 else:
51d089f Stephen Dolan minor changes to noshell logic
authored
231 s = "disabled"
cf7d132 Stephen Dolan added user states, ability to create new objects, and user privileges
authored
232 elif st == "bold":
233 s = "bold"
234 elif st == "dead":
235 s = "dead"
236 return s
237
81e3981 Stephen Dolan rewrote everything :D (I should really learn to make smaller, more frequ...
authored
238 def check(self):
239 assert 'tcdnetsoc-person' in self.objectClass
8265da7 Stephen Dolan user states
authored
240 st = self.get_state()
51d089f Stephen Dolan minor changes to noshell logic
authored
241 if st == "disabled":
8265da7 Stephen Dolan user states
authored
242 assert not self.has_account()
243 assert not self.has_priv("shell")
51d089f Stephen Dolan minor changes to noshell logic
authored
244 assert self.get_attribute('userPassword') is None
8265da7 Stephen Dolan user states
authored
245 else:
246 assert self.has_account()
81e3981 Stephen Dolan rewrote everything :D (I should really learn to make smaller, more frequ...
authored
247 assert self.gidNumber == self.uidNumber
248 assert 'posixAccount' in self.objectClass
249 assert self.get_personal_group() is not None
250
cf7d132 Stephen Dolan added user states, ability to create new objects, and user privileges
authored
251
81e3981 Stephen Dolan rewrote everything :D (I should really learn to make smaller, more frequ...
authored
252 class Group(NDObject):
253 '''A group of users. Groups may contain any number of users, including zero'''
254 rdn_attr = 'cn'
255
256 # Allow "user in group" and "for user in group" as shorthands for
257 # "user in group.member" and "for user in group.member"
258 def __contains__(self, obj):
259 return obj in self.member
260 def __iter__(self):
261 return iter(self.member)
262
263 class PersonalGroup(Group):
264 '''A PersonalGroup is a group with the same name as a user having only that user
265 as a member. Its GID is the UID of the user and its name is the username of the user'''
266 rdn_attr = 'cn'
267
268 def get_user(self):
269 return User(self.cn)
270
271
272 def check(self):
273 assert 'tcdnetsoc-group' in self.objectClass
274 user = self.get_user()
e6e07c1 Stephen Dolan minor fix
authored
275 assert user.exists()
81e3981 Stephen Dolan rewrote everything :D (I should really learn to make smaller, more frequ...
authored
276 assert user.gidNumber == self.gidNumber
277 assert len(self.member) == 1
278 assert user in self
279
cf7d132 Stephen Dolan added user states, ability to create new objects, and user privileges
authored
280 class Privilege(Group):
281 '''Groups controlling access to specific services, for instance webspace or
282 filestorage'''
283 rdn_attr = 'cn'
81e3981 Stephen Dolan rewrote everything :D (I should really learn to make smaller, more frequ...
authored
284
285
286 class IDNumber(NDObject):
287 """Allocator for new ID numbers such as UID and GID.
288 The next ID is stored in the allocator object, and when a new one is requested
289 the field is atomically incremented and the old value is returned"""
290 rdn_attr = 'cn'
291 def _setnum(self, old, new):
292 # Minor hack: we use _raw_modattrs to ensure atomicity
293 # Without it, there's a race condition
294 self._raw_modattrs([
295 (ldap.MOD_DELETE, 'serialNumber', str(old)),
296 (ldap.MOD_ADD, 'serialNumber', str(new))])
297
298 def alloc(self):
299 # try to atomically allocate a new number (UID, GID, etc)
300 # attempt it 3 times in case it fails because someone else
301 # is also allocating numbers
302 for attempt in range(3):
303 currid = self.serialNumber
304 try:
305 self._setnum(currid, currid+1)
306 except ldap.NO_SUCH_ATTRIBUTE, e:
307 time.sleep(random.random() * 0.1)
308 continue
309 return currid
310 raise e
311
312 def check(self):
313 assert 'tcdnetsoc-idnum' in self.objectClass
314
315
316 UIDAllocator = IDNumber('next-uid')
317 GIDAllocator = IDNumber('next-gid')
318
319
320 Attribute('objectClass', [str])
321 Attribute('serialNumber', int)
322 Attribute('tcdnetsoc_membership_year', [str])
db9810f Stephen Dolan added ldapi:/// support
authored
323 Attribute('tcdnetsoc_ISS_username', str)
cf7d132 Stephen Dolan added user states, ability to create new objects, and user privileges
authored
324 Attribute('loginShell', str)
db9810f Stephen Dolan added ldapi:/// support
authored
325 Attribute('sn', str)
81e3981 Stephen Dolan rewrote everything :D (I should really learn to make smaller, more frequ...
authored
326 Attribute('uid', str, match_exact)
327 Attribute('uidNumber', int)
328 Attribute('gidNumber', int)
329 Attribute('homeDirectory', str)
330 Attribute('cn', str)
331 Attribute('member', [User])
332 Attribute('memberOf', [Group], backlink='member')
333
334
Something went wrong with that request. Please try again.