1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
|
#!/usr/bin/python
import os
import pysvn
import subprocess
import sys
import urllib
from functools import partial
import traceback
import logging
import errno
import conf
import common.db
import common.chat
import common.makeuser
import common.studpath
# usage:
# usrmgt-server <port> <magic>
# User management operations:
# - Create local user
# - [Re]Create jail for a user
# - Create a svn repository for a user
# - create repository
# - svn config
# - svn auth
# - Checkout repository as home directory
# - /etc/passwd entry
# - Disable a user's account
# - Enable a user's account
# - Remove a user
# - Rebuild svn config
# - Rebuild svn auth file
# - Rebuild passwd + push to nodes.
def create_user(props):
"""Create the database record for the given user.
Expected properties:
login - used as a unix login name and svn repository name.
STRING REQUIRED
unixid - the unix uid under which execution will take place
on the behalf of the user. Don't use 0! If not specified
or None, one will be allocated from the configured
numeric range.
INT OPTIONAL
password - the clear-text password for the user. If this property is
absent or None, this is an indication that external
authentication should be used (i.e. LDAP).
STRING OPTIONAL
email - the user's email address.
STRING OPTIONAL
nick - the display name to use.
STRING REQUIRED
fullname - The name of the user for results and/or other official
purposes.
STRING REQUIRED
rolenm - The user's role. Must be one of "anyone", "student",
"tutor", "lecturer", "admin".
STRING/ENUM REQUIRED
studentid - If supplied and not None, the student id of the user for
results and/or other official purposes.
STRING OPTIONAL
Return Value: the uid associated with the user. INT
"""
common.makeuser.make_user_db(**props)
user = common.db.get_user(props["login"])
return user["unixid"]
def update_user(props):
"""Create the database record for the given user.
Expected properties:
login - user who is making the change (not necessarily the one
being updated).
update - dict of fields to be updated. The fields are all those
in the login table of the db.
'login' required.
Omitted fields will not be set.
"""
update = props['update']
# Note: "login" is special - it is not part of the kwargs to
# db.update_user, but the first arg - used to find the user to update.
# However it doesn't need any special treatment here.
db = common.db.DB()
db.update_user(**update)
db.close()
def get_login(login, passwd, realm, existing_login, _may_save):
"""Callback function used by pysvn for authentication.
login, passwd: Credentials of the user attempting to log in.
passwd in clear (this should be the user's svn_pass, a
randomly-generated permanent password for this user, not their main
IVLE password).
realm, existing_login, _may_save: The 3 arguments passed by pysvn to
callback_get_login.
The following has been determined empirically, not from docs:
existing_login will be the name of the user who owns the process on
the first attempt, "" on subsequent attempts. We use this fact.
"""
logging.debug("Getting password for %s (realm %s)" % (login, realm))
# Only provide credentials on the _first_ attempt.
# If we're being asked again, then it means the credentials failed for
# some reason and we should just fail. (This is not desirable, but it's
# better than being asked an infinite number of times).
if existing_login == "":
logging.warning("Could not authenticate SVN for %s" % login)
return (False, login, passwd, False)
else:
return (True, login, passwd, False)
def activate_user(props):
"""Create the on-disk stuff for the given user.
Sets the state of the user in the db from pending to enabled.
Expected properties:
login - the user name for the jail
STRING REQUIRED
Return Value: None
"""
os.umask(0022) # Bad, but start_server sets it worse.
login = props['login']
db = common.db.DB()
try:
# FIXME: check we're pending
details = db.get_user(login)
# make svn config/auth
logging.debug("Creating repo")
common.makeuser.make_svn_repo(login, throw_on_error=False)
logging.debug("Creating svn config")
common.makeuser.make_svn_config(login, throw_on_error=False)
logging.debug("Creating svn auth")
passwd = common.makeuser.make_svn_auth(login, throw_on_error=False)
logging.debug("passwd: %s" % passwd)
svn = pysvn.Client()
svn.callback_get_login = partial(get_login, login, passwd)
if conf.svn_addr[-1] != '/':
conf.svn_addr = conf.svn_addr + "/"
# FIXME: This should be a loop over enrolements.
# We're not going to fix this now because it requires
# a large amount of admin console work to manage subject
# offerings.
# Instead, we're just going to use a single offering with
# a "short" name of "info1".
logging.debug("Creating /info1")
try:
svn.mkdir(conf.svn_addr + login + "/info1",
"Initial creation of work directory for Informatics 1")
except Exception, exc:
logging.warning("While mkdiring info1: %s" % str(exc))
pass
logging.debug("Creating /stuff")
try:
svn.mkdir(conf.svn_addr + login + "/stuff",
"Initial creation of directory for miscellania")
except Exception, exc:
logging.warning("While mkdiring stuff: %s" % str(exc))
pass
logging.debug("Creating jail")
common.makeuser.make_jail(login, details.unixid, svn_pass=passwd)
common.makeuser.mount_jail(login)
do_checkout(props, svn_pass=passwd)
# FIXME: should this be nicer?
os.system("chown -R %d:%d %s" \
% (details.unixid, details.unixid,
common.studpath.url_to_local(login)[1]))
logging.info("Enabling user")
db.update_user(login, state='enabled')
return {"response": "okay"}
finally:
db.close()
def do_checkout(props, svn_pass=None):
"""Create the default contents of a user's jail by checking out from the
repositories.
If any directory already exists, just fail silently (so this is not a
"wipe-and-checkout", merely a checkout if it failed or something).
Expected properties:
login - the user name for the jail
STRING REQUIRED
Return Value: None
"""
login = props['login']
db = common.db.DB()
user = db.get_user(login)
# If svn_pass isn't supplied, grab it from the DB
if svn_pass is None:
svn_pass = user.svn_pass
db.close()
svn = pysvn.Client()
svn.callback_get_login = partial(get_login, login, svn_pass)
if conf.svn_addr[-1] != os.sep:
conf.svn_addr += os.sep
# FIXME: <hack>
logging.debug("Checking out directories in the jail")
try:
svn.checkout(conf.svn_addr + login + "/stuff",
common.studpath.url_to_local(login + "/stuff")[1])
os.system("chown -R %d:%d %s" \
% (user.unixid, user.unixid,
common.studpath.url_to_local(login + "/stuff")[1]))
except Exception, exc:
logging.warning("While mkdiring stuff: %s" % str(exc))
pass
try:
svn.checkout(conf.svn_addr + login + "/info1",
common.studpath.url_to_local(login + "/info1")[1])
os.system("chown -R %d:%d %s" \
% (user.unixid, user.unixid,
common.studpath.url_to_local(login + "/info1")[1]))
except Exception, exc:
logging.warning("While mkdiring info1: %s" % str(exc))
pass
# FIXME: </hack>
return {"response": "okay"}
actions = {
'create_user':create_user,
'update_user':update_user,
'activate_user':activate_user,
'do_checkout':do_checkout,
}
def initializer():
try:
pidfile = open('/var/run/usrmgt-server.pid', 'w')
pidfile.write('%d\n' % os.getpid())
pidfile.close()
except IOError, (errno, strerror):
print "Couldn't write PID file. IO error(%s): %s" % (errno, strerror)
sys.exit(1)
def dispatch(props):
logging.debug(repr(props))
action = props.keys()[0]
return actions[action](props[action])
if __name__ == "__main__":
if len(sys.argv) <3:
print >>sys.stderr, "Usage: usrmgt-server <port> <magic>"
sys.exit(1)
port = int(sys.argv[1])
magic = sys.argv[2]
pid = os.getpid()
logging.basicConfig(filename="/var/log/usrmgt.log", level=logging.INFO)
logging.info("Starting usrmgt server on port %d (pid = %d)" % (port, pid))
common.chat.start_server(port, magic, True, dispatch, initializer)
|