~azzar1/unity/add-show-desktop-key

« back to all changes in this revision

Viewing changes to lib/common/makeuser.py

  • Committer: dcoles
  • Date: 2008-07-03 04:38:30 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:804
Setup: To go with last revision - Now just a front end for the setup package

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# IVLE - Informatics Virtual Learning Environment
 
2
# Copyright (C) 2007-2008 The University of Melbourne
 
3
#
 
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.
 
8
#
 
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.
 
13
#
 
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
 
17
 
 
18
# Module: MakeUser
 
19
# Author: Matt Giuca
 
20
# Date:   1/2/2008
 
21
 
 
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
 
27
# * Unix user account
 
28
 
 
29
# TODO: Sanitize login name and other fields.
 
30
# Users must not be called "temp" or "template".
 
31
 
 
32
# TODO: When creating a new home directory, chown it to its owner
 
33
 
 
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).
 
37
 
 
38
import md5
 
39
import os
 
40
import stat
 
41
import shutil
 
42
import time
 
43
import uuid
 
44
import warnings
 
45
import filecmp
 
46
import conf
 
47
import db
 
48
 
 
49
def chown_to_webserver(filename):
 
50
    """
 
51
    Chowns a file so the web server user owns it.
 
52
    (This is useful in setting up Subversion conf files).
 
53
    Assumes root.
 
54
    """
 
55
    try:
 
56
        os.system("chown -R www-data:www-data %s" % filename)
 
57
    except:
 
58
        pass
 
59
 
 
60
def make_svn_repo(login, throw_on_error=True):
 
61
    """Create a repository for the given user.
 
62
    """
 
63
    path = os.path.join(conf.svn_repo_path, login)
 
64
    try:
 
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:
 
69
        print repr(exc)
 
70
        if throw_on_error:
 
71
            raise
 
72
 
 
73
    chown_to_webserver(path)
 
74
 
 
75
def rebuild_svn_config():
 
76
    """Build the complete SVN configuration file.
 
77
    """
 
78
    conn = db.DB()
 
79
    res = conn.query("SELECT login, rolenm FROM login;").dictresult()
 
80
    groups = {}
 
81
    for r in res:
 
82
        role = r['rolenm']
 
83
        if role not in groups:
 
84
            groups[role] = []
 
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())
 
89
    f.write("\n")
 
90
    f.write("[groups]\n")
 
91
    for (g,ls) in groups.iteritems():
 
92
        f.write("%s = %s\n" % (g, ",".join(ls)))
 
93
    f.write("\n")
 
94
    for r in res:
 
95
        login = r['login']
 
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")
 
101
        f.write("\n")
 
102
    f.close()
 
103
    os.rename(conf.svn_conf + ".new", conf.svn_conf)
 
104
    chown_to_webserver(conf.svn_conf)
 
105
 
 
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.
 
109
    """
 
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")
 
116
    f.write("\n")
 
117
    f.close()
 
118
    chown_to_webserver(conf.svn_conf)
 
119
 
 
120
def make_svn_auth(login, throw_on_error=True):
 
121
    """Setup svn authentication for the given user.
 
122
       FIXME: create local.auth entry
 
123
    """
 
124
    passwd = md5.new(uuid.uuid4().bytes).digest().encode('hex')
 
125
    if os.path.exists(conf.svn_auth_ivle):
 
126
        create = ""
 
127
    else:
 
128
        create = "c"
 
129
 
 
130
    db.DB().update_user(login, svn_pass=passwd)
 
131
 
 
132
    res = os.system("htpasswd -%smb %s %s %s" % (create,
 
133
                                              conf.svn_auth_ivle,
 
134
                                              login, passwd))
 
135
    if res != 0 and throw_on_error:
 
136
        raise Exception("Unable to create ivle-auth for %s" % login)
 
137
 
 
138
    # Make sure the file is owned by the web server
 
139
    if create == "c":
 
140
        chown_to_webserver(conf.svn_auth_ivle)
 
141
 
 
142
    return passwd
 
143
 
 
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 
 
147
    to be removed.
 
148
    
 
149
    parent: This is used for the recursive call to track the relative paths 
 
150
    that we have decended.
 
151
    """
 
152
    
 
153
    cmp = filecmp.dircmp(basedir, targetdir)
 
154
 
 
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)]
 
157
 
 
158
    # Remove files that are redundant
 
159
    to_remove = [os.path.join(parent,x) for x in cmp.right_only]
 
160
    
 
161
    # Recurse
 
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)
 
167
        to_add += sadd
 
168
        to_remove += sremove
 
169
 
 
170
    return (to_add, to_remove)
 
171
 
 
172
 
 
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
 
175
    conf.py.
 
176
 
 
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.
 
181
 
 
182
    Returns the path to the user's home directory.
 
183
 
 
184
    Chowns the user's directory within the jail to the given UID.
 
185
 
 
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.
 
190
 
 
191
    force: If false, exception if jail already exists for this user.
 
192
    If true (default), overwrites it, but preserves home directory.
 
193
 
 
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.
 
196
 
 
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.
 
200
    """
 
201
    # MUST run as root or some of this may fail
 
202
    if os.getuid() != 0:
 
203
        raise Exception("Must run make_jail as root")
 
204
    
 
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: " +
 
208
            stagingdir)
 
209
    # tempdir is for putting backup homes in
 
210
    tempdir = os.path.join(conf.jail_base, '__temp__')
 
211
    if not os.path.exists(tempdir):
 
212
        os.makedirs(tempdir)
 
213
    elif not os.path.isdir(tempdir):
 
214
        os.unlink(tempdir)
 
215
        os.mkdir(tempdir)
 
216
    userdir = os.path.join(conf.jail_base, username)
 
217
    homedir = os.path.join(userdir, 'home')
 
218
 
 
219
    if os.path.exists(userdir):
 
220
        if not force:
 
221
            raise Exception("User's jail already exists")
 
222
        # User jail already exists. Blow it away but preserve their home
 
223
        # directory.
 
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)
 
232
        try:
 
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.
 
237
            if manifest:
 
238
                (to_add, to_remove) = manifest
 
239
                # Remove redundant files and directories
 
240
                for d in to_remove:
 
241
                    dst = os.path.join(userdir, d)
 
242
                    src = os.path.join(stagingdir, d)
 
243
                    if os.path.isdir(dst):
 
244
                        shutil.rmtree(dst)
 
245
                    elif os.path.isfile(dst):
 
246
                        os.remove(dst)
 
247
                # Add new files
 
248
                for d in to_add:
 
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):
 
253
                        shutil.rmtree(dst)
 
254
                    elif os.path.isfile(dst):
 
255
                        os.remove(dst)
 
256
                    # Link the file/dirs
 
257
                    if os.path.isdir(src):
 
258
                        linktree(src, dst)
 
259
                    elif os.path.isfile(src):
 
260
                        os.link(src, dst)
 
261
                    
 
262
            else:
 
263
                # No manifest, do a full rebuild
 
264
                shutil.rmtree(userdir)
 
265
                # Hard-link (copy aliasing) the entire tree over
 
266
                linktree(stagingdir, userdir)
 
267
        finally:
 
268
            # Set up the user's home directory (restore backup)
 
269
            # First make sure the directory is empty and its parent exists
 
270
            try:
 
271
                shutil.rmtree(homedir)
 
272
            except:
 
273
                pass
 
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.
 
276
            os.makedirs(homedir)
 
277
            shutil.move(homebackup, homedir)
 
278
        userhomedir = os.path.join(homedir, username)   # Return value
 
279
    else:
 
280
        # No user jail exists
 
281
        # Hard-link (copy aliasing) the entire tree over
 
282
        linktree(stagingdir, userdir)
 
283
 
 
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)
 
291
 
 
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)
 
297
 
 
298
    return userhomedir
 
299
 
 
300
def make_conf_py(username, user_jail_dir, staging_dir, svn_pass=None):
 
301
    """
 
302
    Creates (overwriting any existing file) a file /opt/ivle/lib/conf/conf.py
 
303
    in a given user's jail.
 
304
    username: Username.
 
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.
 
310
    """
 
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
 
313
    # simply appending.
 
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")
 
319
 
 
320
    # If svn_pass isn't supplied, grab it from the DB
 
321
    if svn_pass is None:
 
322
        dbconn = db.DB()
 
323
        svn_pass = dbconn.get_user(username).svn_pass
 
324
        dbconn.close()
 
325
 
 
326
    # Read the contents of the template conf file
 
327
    try:
 
328
        template_conf_file = open(template_conf_path, "r")
 
329
        template_conf_data = template_conf_file.read()
 
330
        template_conf_file.close()
 
331
    except:
 
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")
 
336
 
 
337
    # Remove the target conf file if it exists
 
338
    try:
 
339
        os.remove(conf_path)
 
340
    except OSError:
 
341
        pass
 
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 "
 
348
        "the jail\n")
 
349
    conf_file.write("svn_pass = %s\n" % repr(svn_pass))
 
350
    conf_file.close()
 
351
 
 
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
 
355
                        | stat.S_IROTH)
 
356
 
 
357
def linktree(src, dst):
 
358
    """Recursively hard-link a directory tree using os.link().
 
359
 
 
360
    The destination directory must not already exist.
 
361
    If exception(s) occur, an Error is raised with a list of reasons.
 
362
 
 
363
    Symlinks are preserved (in fact, hard links are created which point to the
 
364
    symlinks).
 
365
 
 
366
    Code heavily based upon shutil.copytree from Python 2.5 library.
 
367
    """
 
368
    names = os.listdir(src)
 
369
    os.makedirs(dst)
 
370
    errors = []
 
371
    for name in names:
 
372
        srcname = os.path.join(src, name)
 
373
        dstname = os.path.join(dst, name)
 
374
        try:
 
375
            if os.path.isdir(srcname):
 
376
                linktree(srcname, dstname)
 
377
            else:
 
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])
 
386
    try:
 
387
        shutil.copystat(src, dst)
 
388
    except WindowsError:
 
389
        # can't copy file access times on Windows
 
390
        pass
 
391
    except OSError, why:
 
392
        errors.extend((src, dst, str(why)))
 
393
    if errors:
 
394
        raise Exception, errors
 
395
 
 
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"
 
402
    state.
 
403
    Throws an exception if the user already exists.
 
404
    """
 
405
    dbconn = db.DB()
 
406
    dbconn.create_user(**kwargs)
 
407
    dbconn.close()
 
408
 
 
409
    if kwargs['password']:
 
410
        if os.path.exists(conf.svn_auth_local):
 
411
            create = ""
 
412
        else:
 
413
            create = "c"
 
414
        res = os.system("htpasswd -%smb %s %s %s" % (create,
 
415
                                                     conf.svn_auth_local,
 
416
                                                     kwargs['login'],
 
417
                                                     kwargs['password']))
 
418
        if res != 0 and throw_on_error:
 
419
            raise Exception("Unable to create local-auth for %s" % kwargs['login'])
 
420
 
 
421
    # Make sure the file is owned by the web server
 
422
    if create == "c":
 
423
        chown_to_webserver(conf.svn_auth_local)