1
# IVLE - Informatics Virtual Learning Environment
2
# Copyright (C) 2007-2008 The University of Melbourne
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
22
# Allows creation of users. This sets up the following:
23
# * User's jail and home directory within the jail.
24
# * Subversion repository (TODO)
25
# * Check out Subversion workspace into jail (TODO)
26
# * Database details for user
29
# TODO: Sanitize login name and other fields.
30
# Users must not be called "temp" or "template".
32
# TODO: When creating a new home directory, chown it to its owner
34
# TODO: In chown_to_webserver:
35
# Do not call os.system("chown www-data") - use Python lib
36
# and use the web server uid given in conf. (Several places).
49
def chown_to_webserver(filename):
51
Chowns a file so the web server user owns it.
52
(This is useful in setting up Subversion conf files).
56
os.system("chown -R www-data:www-data %s" % filename)
60
def make_svn_repo(login, throw_on_error=True):
61
"""Create a repository for the given user.
63
path = os.path.join(conf.svn_repo_path, login)
65
res = os.system("svnadmin create '%s'" % path)
66
if res != 0 and throw_on_error:
67
raise Exception("Cannot create repository for %s" % login)
68
except Exception, exc:
73
chown_to_webserver(path)
75
def rebuild_svn_config():
76
"""Build the complete SVN configuration file.
79
res = conn.query("SELECT login, rolenm FROM login;").dictresult()
83
if role not in groups:
85
groups[role].append(r['login'])
86
f = open(conf.svn_conf + ".new", "w")
87
f.write("# IVLE SVN Repositories Configuration\n")
88
f.write("# Auto-generated on %s\n" % time.asctime())
91
for (g,ls) in groups.iteritems():
92
f.write("%s = %s\n" % (g, ",".join(ls)))
96
f.write("[%s:/]\n" % login)
97
f.write("%s = rw\n" % login)
98
#f.write("@tutor = r\n")
99
#f.write("@lecturer = rw\n")
100
#f.write("@admin = rw\n")
103
os.rename(conf.svn_conf + ".new", conf.svn_conf)
104
chown_to_webserver(conf.svn_conf)
106
def make_svn_config(login, throw_on_error=True):
107
"""Add an entry to the apache-svn config file for the given user.
108
Assumes the given user is either a guest or a student.
110
f = open(conf.svn_conf, "a")
111
f.write("[%s:/]\n" % login)
112
f.write("%s = rw\n" % login)
113
#f.write("@tutor = r\n")
114
#f.write("@lecturer = rw\n")
115
#f.write("@admin = rw\n")
118
chown_to_webserver(conf.svn_conf)
120
def make_svn_auth(login, throw_on_error=True):
121
"""Setup svn authentication for the given user.
122
FIXME: create local.auth entry
124
passwd = md5.new(uuid.uuid4().bytes).digest().encode('hex')
125
if os.path.exists(conf.svn_auth_ivle):
130
db.DB().update_user(login, svn_pass=passwd)
132
res = os.system("htpasswd -%smb %s %s %s" % (create,
135
if res != 0 and throw_on_error:
136
raise Exception("Unable to create ivle-auth for %s" % login)
138
# Make sure the file is owned by the web server
140
chown_to_webserver(conf.svn_auth_ivle)
144
def generate_manifest(basedir, targetdir, parent=''):
145
""" From a basedir and a targetdir work out which files are missing or out
146
of date and need to be added/updated and which files are redundant and need
149
parent: This is used for the recursive call to track the relative paths
150
that we have decended.
153
cmp = filecmp.dircmp(basedir, targetdir)
155
# Add all new files and files that have changed
156
to_add = [os.path.join(parent,x) for x in (cmp.left_only + cmp.diff_files)]
158
# Remove files that are redundant
159
to_remove = [os.path.join(parent,x) for x in cmp.right_only]
162
for d in cmp.common_dirs:
163
newbasedir = os.path.join(basedir, d)
164
newtargetdir = os.path.join(targetdir, d)
165
newparent = os.path.join(parent, d)
166
(sadd,sremove) = generate_manifest(newbasedir, newtargetdir, newparent)
170
return (to_add, to_remove)
173
def make_jail(username, uid, force=True, manifest=None, svn_pass=None):
174
"""Creates a new user's jail space, in the jail directory as configured in
177
This expects there to be a "staging" directory within the jail root which
178
contains all the files for a sample student jail. It creates the student's
179
directory in the jail root, by making a hard-link copy of every file in the
180
staging directory, recursively.
182
Returns the path to the user's home directory.
184
Chowns the user's directory within the jail to the given UID.
186
Note: This takes separate username and uid arguments. The UID need not
187
*necessarily* correspond to a Unix username at all, if all you are
188
planning to do is setuid to it. This allows the caller the freedom of
189
deciding the binding between username and uid, if any.
191
force: If false, exception if jail already exists for this user.
192
If true (default), overwrites it, but preserves home directory.
194
manifest: If provided this will be a pair (to_add, to_remove) of files or
195
directories to add or remove from the jail.
197
svn_pass: If provided this will be a string, the randomly-generated
198
Subversion password for this user (if you happen to already have it).
199
If not provided, it will be read from the database.
201
# MUST run as root or some of this may fail
203
raise Exception("Must run make_jail as root")
205
stagingdir = os.path.join(conf.jail_base, '__staging__')
206
if not os.path.isdir(stagingdir):
207
raise Exception("Staging jail directory does not exist: " +
209
# tempdir is for putting backup homes in
210
tempdir = os.path.join(conf.jail_base, '__temp__')
211
if not os.path.exists(tempdir):
213
elif not os.path.isdir(tempdir):
216
userdir = os.path.join(conf.jail_base, username)
217
homedir = os.path.join(userdir, 'home')
219
if os.path.exists(userdir):
221
raise Exception("User's jail already exists")
222
# User jail already exists. Blow it away but preserve their home
224
# Ignore warnings about the use of tmpnam
225
warnings.simplefilter('ignore')
226
homebackup = os.tempnam(tempdir)
227
warnings.resetwarnings()
228
# Note: shutil.move does not behave like "mv" - it does not put a file
229
# into a directory if it already exists, just fails. Therefore it is
230
# not susceptible to tmpnam symlink attack.
231
shutil.move(homedir, homebackup)
233
# Any errors that occur after making the backup will be caught and
234
# the backup will be un-made.
235
# XXX This will still leave the user's jail in an unusable state,
236
# but at least they won't lose their files.
238
(to_add, to_remove) = manifest
239
# Remove redundant files and directories
241
dst = os.path.join(userdir, d)
242
src = os.path.join(stagingdir, d)
243
if os.path.isdir(dst):
245
elif os.path.isfile(dst):
249
dst = os.path.join(userdir, d)
250
src = os.path.join(stagingdir, d)
251
# Clear the previous file/dir
252
if os.path.isdir(dst):
254
elif os.path.isfile(dst):
257
if os.path.isdir(src):
259
elif os.path.isfile(src):
263
# No manifest, do a full rebuild
264
shutil.rmtree(userdir)
265
# Hard-link (copy aliasing) the entire tree over
266
linktree(stagingdir, userdir)
268
# Set up the user's home directory (restore backup)
269
# First make sure the directory is empty and its parent exists
271
shutil.rmtree(homedir)
274
# XXX If this fails the user's directory will be lost (in the temp
275
# directory). But it shouldn't fail as homedir should not exist.
277
shutil.move(homebackup, homedir)
278
userhomedir = os.path.join(homedir, username) # Return value
280
# No user jail exists
281
# Hard-link (copy aliasing) the entire tree over
282
linktree(stagingdir, userdir)
284
# Set up the user's home directory
285
userhomedir = os.path.join(homedir, username)
286
os.mkdir(userhomedir)
287
# Chown (and set the GID to the same as the UID).
288
os.chown(userhomedir, uid, uid)
289
# Chmod to rwxr-xr-x (755)
290
os.chmod(userhomedir, 0755)
292
# There is 1 special file which should not be hard-linked, but instead
293
# generated specific to this user: /opt/ivle/lib/conf/conf.py.
294
# "__" username "__" users are exempt (special)
295
if not (username.startswith("__") and username.endswith("__")):
296
make_conf_py(username, userdir, stagingdir, svn_pass)
300
def make_conf_py(username, user_jail_dir, staging_dir, svn_pass=None):
302
Creates (overwriting any existing file) a file /opt/ivle/lib/conf/conf.py
303
in a given user's jail.
305
user_jail_dir: User's jail dir, ie. conf.jail_base + username
306
staging_dir: The dir with the staging copy of the jail. (With the
307
template conf.py file).
308
svn_pass: As with make_jail. User's SVN password, but if not supplied,
309
will look up in the DB.
311
# Note: It is important to delete this file and recreate it (somewhat
312
# ironically beginning by pasting the same contents in again), rather than
314
# Note that all files initially are aliased, so appending would result
315
# in a massive aliasing problem. Deleting and recreating ensures that
316
# the conf.py files are unique to each jail.
317
template_conf_path = os.path.join(staging_dir,"opt/ivle/lib/conf/conf.py")
318
conf_path = os.path.join(user_jail_dir, "opt/ivle/lib/conf/conf.py")
320
# If svn_pass isn't supplied, grab it from the DB
323
svn_pass = dbconn.get_user(username).svn_pass
326
# Read the contents of the template conf file
328
template_conf_file = open(template_conf_path, "r")
329
template_conf_data = template_conf_file.read()
330
template_conf_file.close()
332
# Couldn't open template conf.py for some reason
333
# Just treat it as empty file
334
template_conf_data = ("# Warning: Problem building config script.\n"
335
"# Could not find template conf.py file.\n")
337
# Remove the target conf file if it exists
342
conf_file = open(conf_path, "w")
343
conf_file.write(template_conf_data)
344
conf_file.write("\n# The login name for the owner of the jail\n")
345
conf_file.write("login = %s\n" % repr(username))
346
conf_file.write("\n")
347
conf_file.write("# The subversion-only password for the owner of "
349
conf_file.write("svn_pass = %s\n" % repr(svn_pass))
352
# Make this file world-readable
353
# (chmod 644 conf_path)
354
os.chmod(conf_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP
357
def linktree(src, dst):
358
"""Recursively hard-link a directory tree using os.link().
360
The destination directory must not already exist.
361
If exception(s) occur, an Error is raised with a list of reasons.
363
Symlinks are preserved (in fact, hard links are created which point to the
366
Code heavily based upon shutil.copytree from Python 2.5 library.
368
names = os.listdir(src)
372
srcname = os.path.join(src, name)
373
dstname = os.path.join(dst, name)
375
if os.path.isdir(srcname):
376
linktree(srcname, dstname)
378
os.link(srcname, dstname)
379
# XXX What about devices, sockets etc.?
380
except (IOError, os.error), why:
381
errors.append((srcname, dstname, str(why)))
382
# catch the Error from the recursive copytree so that we can
383
# continue with other files
384
except Exception, err:
385
errors.append(err.args[0])
387
shutil.copystat(src, dst)
389
# can't copy file access times on Windows
392
errors.extend((src, dst, str(why)))
394
raise Exception, errors
396
def make_user_db(throw_on_error = True, **kwargs):
397
"""Creates a user's entry in the database, filling in all the fields.
398
All arguments must be keyword args. They are the fields in the table.
399
However, instead of supplying a "passhash", you must supply a
400
"password" argument, which will be hashed internally.
401
Also do not supply a state. All users are created in the "no_agreement"
403
Throws an exception if the user already exists.
406
dbconn.create_user(**kwargs)
409
if kwargs['password']:
410
if os.path.exists(conf.svn_auth_local):
414
res = os.system("htpasswd -%smb %s %s %s" % (create,
418
if res != 0 and throw_on_error:
419
raise Exception("Unable to create local-auth for %s" % kwargs['login'])
421
# Make sure the file is owned by the web server
423
chown_to_webserver(conf.svn_auth_local)