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

« back to all changes in this revision

Viewing changes to ivle/makeuser.py

  • Committer: William Grant
  • Date: 2009-04-29 04:58:42 UTC
  • Revision ID: grantw@unimelb.edu.au-20090429045842-upijb1ybcbz1zg6u
Kill --nosvnrevno from setup.install - we don't use Subversion now.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
# IVLE - Informatics Virtual Learning Environment
2
 
# Copyright (C) 2007-2009 The University of Melbourne
 
2
# Copyright (C) 2007-2008 The University of Melbourne
3
3
#
4
4
# This program is free software; you can redistribute it and/or modify
5
5
# it under the terms of the GNU General Public License as published by
15
15
# along with this program; if not, write to the Free Software
16
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
17
17
 
18
 
"""User and group filesystem management helpers."""
 
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).
19
37
 
20
38
import hashlib
21
39
import os
24
42
import time
25
43
import uuid
26
44
import warnings
 
45
import filecmp
27
46
import logging
28
 
import subprocess
29
 
 
30
 
from storm.expr import Select, Max
31
 
 
32
 
import ivle.config
33
 
from ivle.database import (User, ProjectGroup, Assessed, ProjectSubmission,
34
 
        Project, ProjectSet, Offering, Enrolment, Subject, Semester)
 
47
import ivle.pulldown_subj
 
48
 
 
49
from ivle.database import ProjectGroup
35
50
 
36
51
def chown_to_webserver(filename):
37
 
    """chown a directory and its contents to the web server.
38
 
 
39
 
    Recursively chowns a file or directory so the web server user owns it.
 
52
    """
 
53
    Chowns a file so the web server user owns it.
 
54
    (This is useful in setting up Subversion conf files).
40
55
    Assumes root.
41
56
    """
42
 
    subprocess.call(['chown', '-R', 'www-data:www-data', filename])
 
57
    try:
 
58
        os.system("chown -R www-data:www-data %s" % filename)
 
59
    except:
 
60
        pass
43
61
 
44
62
def make_svn_repo(path, throw_on_error=True):
45
 
    """Create a Subversion repository at the given path."""
 
63
    """Create a Subversion repository at the given path.
 
64
    """
46
65
    try:
47
 
        res = subprocess.call(['svnadmin', 'create', path])
 
66
        res = os.system("svnadmin create '%s'" % path)
48
67
        if res != 0 and throw_on_error:
49
68
            raise Exception("Cannot create repository: %s" % path)
50
69
    except Exception, exc:
56
75
 
57
76
def rebuild_svn_config(store, config):
58
77
    """Build the complete SVN configuration file.
59
 
 
60
78
    @param config: An ivle.config.Config object.
61
79
    """
62
 
    users = store.find(User)
 
80
    users = store.find(ivle.database.User)
 
81
    groups = {}
 
82
    # TODO: Populate groups with per-offering tutors/lecturers/etc.
63
83
    conf_name = config['paths']['svn']['conf']
64
84
    temp_name = conf_name + ".new"
65
85
    f = open(temp_name, "w")
66
 
    f.write("""\
67
 
# IVLE SVN repository authorisation configuration
68
 
# Generated: %(time)s
69
 
""" % {'time': time.asctime()})
70
 
 
 
86
    f.write("# IVLE SVN Repositories Configuration\n")
 
87
    f.write("# Auto-generated on %s\n" % time.asctime())
 
88
    f.write("\n")
 
89
    f.write("[groups]\n")
 
90
    for (g,ls) in groups.iteritems():
 
91
        f.write("%s = %s\n" % (g, ",".join(ls)))
 
92
    f.write("\n")
71
93
    for u in users:
72
 
        f.write("""
73
 
[%(login)s:/]
74
 
%(login)s = rw
75
 
""" % {'login': u.login.encode('utf-8')})
76
 
 
77
 
    # Now we need to grant offering tutors and lecturers access to the latest
78
 
    # submissions in their offerings. There are much prettier ways to do this,
79
 
    # but a lot of browser requests call this function, so it needs to be
80
 
    # fast. We can grab all of the paths needing authorisation directives with
81
 
    # a single query, and we cache the list of viewers for each offering.
82
 
    offering_viewers_cache = {}
83
 
    for (login, psid, pspath, offeringid) in store.find(
84
 
        (User.login, ProjectSubmission.id, ProjectSubmission.path,
85
 
         Offering.id),
86
 
            Assessed.id == ProjectSubmission.assessed_id,
87
 
            User.id == Assessed.user_id,
88
 
            Project.id == Assessed.project_id,
89
 
            ProjectSet.id == Project.project_set_id,
90
 
            Offering.id == ProjectSet.id,
91
 
            ProjectSubmission.date_submitted == Select(
92
 
                    Max(ProjectSubmission.date_submitted),
93
 
                    ProjectSubmission.assessed_id == Assessed.id,
94
 
                    tables=ProjectSubmission
95
 
            )
96
 
        ):
97
 
 
98
 
        # Do we already have the list of logins authorised for this offering
99
 
        # cached? If not, get it.
100
 
        if offeringid not in offering_viewers_cache:
101
 
            offering_viewers_cache[offeringid] = list(store.find(
102
 
                    User.login,
103
 
                    User.id == Enrolment.user_id,
104
 
                    Enrolment.offering_id == offeringid,
105
 
                    Enrolment.role.is_in((u'tutor', u'lecturer')),
106
 
                    Enrolment.active == True,
107
 
                )
108
 
            )
109
 
 
110
 
        f.write("""
111
 
# Submission %(id)d
112
 
[%(login)s:%(path)s]
113
 
""" % {'login': login.encode('utf-8'), 'id': psid,
114
 
       'path': pspath.encode('utf-8')})
115
 
 
116
 
        for viewer_login in offering_viewers_cache[offeringid]:
117
 
            # We don't want to override the owner's write privilege,
118
 
            # so we don't add them to the read-only ACL.
119
 
            if login != viewer_login:
120
 
                f.write("%s = r\n" % viewer_login.encode('utf-8'))
121
 
 
 
94
        f.write("[%s:/]\n" % u.login)
 
95
        f.write("%s = rw\n" % u.login)
 
96
        #f.write("@tutor = r\n")
 
97
        #f.write("@lecturer = rw\n")
 
98
        #f.write("@admin = rw\n")
 
99
        f.write("\n")
122
100
    f.close()
123
101
    os.rename(temp_name, conf_name)
124
102
    chown_to_webserver(conf_name)
125
103
 
126
104
def rebuild_svn_group_config(store, config):
127
105
    """Build the complete SVN configuration file for groups
128
 
 
129
106
    @param config: An ivle.config.Config object.
130
107
    """
131
108
    conf_name = config['paths']['svn']['group_conf']
132
109
    temp_name = conf_name + ".new"
133
110
    f = open(temp_name, "w")
134
 
 
135
 
    f.write("""\
136
 
# IVLE SVN group repository authorisation configuration
137
 
# Generated: %(time)s
138
 
 
139
 
""" % {'time': time.asctime()})
140
 
 
141
 
    group_members_cache = {}
 
111
    f.write("# IVLE SVN Group Repositories Configuration\n")
 
112
    f.write("# Auto-generated on %s\n" % time.asctime())
 
113
    f.write("\n")
142
114
    for group in store.find(ProjectGroup):
143
115
        offering = group.project_set.offering
144
116
        reponame = "_".join([offering.subject.short_name,
145
117
                             offering.semester.year,
146
118
                             offering.semester.semester,
147
119
                             group.name])
148
 
 
149
 
        f.write("[%s:/]\n" % reponame.encode('utf-8'))
150
 
        if group.id not in group_members_cache:
151
 
            group_members_cache[group.id] = set()
 
120
        f.write("[%s:/]\n"%reponame)
152
121
        for user in group.members:
153
 
            group_members_cache[group.id].add(user.login)
154
 
            f.write("%s = rw\n" % user.login.encode('utf-8'))
 
122
            f.write("%s = rw\n" % user.login)
155
123
        f.write("\n")
156
 
 
157
 
    # Now we need to grant offering tutors and lecturers access to the latest
158
 
    # submissions in their offerings. There are much prettier ways to do this,
159
 
    # but a lot of browser requests call this function, so it needs to be
160
 
    # fast. We can grab all of the paths needing authorisation directives with
161
 
    # a single query, and we cache the list of viewers for each offering.
162
 
    offering_viewers_cache = {}
163
 
    for (ssn, year, sem, name, psid, pspath, gid, offeringid) in store.find(
164
 
        (Subject.short_name, Semester.year, Semester.semester,
165
 
         ProjectGroup.name, ProjectSubmission.id, ProjectSubmission.path,
166
 
         ProjectGroup.id, Offering.id),
167
 
            Assessed.id == ProjectSubmission.assessed_id,
168
 
            ProjectGroup.id == Assessed.project_group_id,
169
 
            Project.id == Assessed.project_id,
170
 
            ProjectSet.id == Project.project_set_id,
171
 
            Offering.id == ProjectSet.offering_id,
172
 
            Subject.id == Offering.subject_id,
173
 
            Semester.id == Offering.semester_id,
174
 
            ProjectSubmission.date_submitted == Select(
175
 
                    Max(ProjectSubmission.date_submitted),
176
 
                    ProjectSubmission.assessed_id == Assessed.id,
177
 
                    tables=ProjectSubmission
178
 
            )
179
 
        ):
180
 
 
181
 
        reponame = "_".join([ssn, year, sem, name])
182
 
 
183
 
        # Do we already have the list of logins authorised for this offering
184
 
        # cached? If not, get it.
185
 
        if offeringid not in offering_viewers_cache:
186
 
            offering_viewers_cache[offeringid] = list(store.find(
187
 
                    User.login,
188
 
                    User.id == Enrolment.user_id,
189
 
                    Enrolment.offering_id == offeringid,
190
 
                    Enrolment.role.is_in((u'tutor', u'lecturer')),
191
 
                    Enrolment.active == True,
192
 
                )
193
 
            )
194
 
 
195
 
        f.write("""
196
 
# Submission %(id)d
197
 
[%(repo)s:%(path)s]
198
 
""" % {'repo': reponame.encode('utf-8'), 'id': psid,
199
 
       'path': pspath.encode('utf-8')})
200
 
 
201
 
        for viewer_login in offering_viewers_cache[offeringid]:
202
 
            # Skip existing group members, or they can't write to it any more.
203
 
            if viewer_login not in group_members_cache[gid]:
204
 
                f.write("%s = r\n" % viewer_login)
205
 
 
206
124
    f.close()
207
125
    os.rename(temp_name, conf_name)
208
126
    chown_to_webserver(conf_name)
209
127
 
210
128
def make_svn_auth(store, login, config, throw_on_error=True):
211
 
    """Create a Subversion password for a user.
212
 
 
213
 
    Generates a new random Subversion password, and assigns it to the user.
214
 
    The password is added to Apache's Subversion authentication file.
 
129
    """Setup svn authentication for the given user.
 
130
       Uses the given DB store object. Does not commit to the db.
215
131
    """
216
132
    # filename is, eg, /var/lib/ivle/svn/ivle.auth
217
133
    filename = config['paths']['svn']['auth_ivle']
 
134
    passwd = hashlib.md5(uuid.uuid4().bytes).hexdigest()
218
135
    if os.path.exists(filename):
219
136
        create = ""
220
137
    else:
221
138
        create = "c"
222
139
 
223
 
    user = User.get_by_login(store, login)
224
 
 
225
 
    if user.svn_pass is None:
226
 
        passwd = hashlib.md5(uuid.uuid4().bytes).hexdigest()
227
 
        user.svn_pass = unicode(passwd)
228
 
 
229
 
    res = subprocess.call(['htpasswd', '-%smb' % create,
230
 
                           filename, login, user.svn_pass])
 
140
    user = ivle.database.User.get_by_login(store, login)
 
141
    user.svn_pass = unicode(passwd)
 
142
 
 
143
    res = os.system("htpasswd -%smb %s %s %s" % (create, filename,
 
144
                                              login, passwd))
231
145
    if res != 0 and throw_on_error:
232
146
        raise Exception("Unable to create ivle-auth for %s" % login)
233
147
 
235
149
    if create == "c":
236
150
        chown_to_webserver(filename)
237
151
 
238
 
    return user.svn_pass
 
152
    return passwd
 
153
 
 
154
def generate_manifest(basedir, targetdir, parent=''):
 
155
    """ From a basedir and a targetdir work out which files are missing or out 
 
156
    of date and need to be added/updated and which files are redundant and need 
 
157
    to be removed.
 
158
    
 
159
    parent: This is used for the recursive call to track the relative paths 
 
160
    that we have decended.
 
161
    """
 
162
    
 
163
    cmp = filecmp.dircmp(basedir, targetdir)
 
164
 
 
165
    # Add all new files and files that have changed
 
166
    to_add = [os.path.join(parent,x) for x in (cmp.left_only + cmp.diff_files)]
 
167
 
 
168
    # Remove files that are redundant
 
169
    to_remove = [os.path.join(parent,x) for x in cmp.right_only]
 
170
    
 
171
    # Recurse
 
172
    for d in cmp.common_dirs:
 
173
        newbasedir = os.path.join(basedir, d)
 
174
        newtargetdir = os.path.join(targetdir, d)
 
175
        newparent = os.path.join(parent, d)
 
176
        (sadd,sremove) = generate_manifest(newbasedir, newtargetdir, newparent)
 
177
        to_add += sadd
 
178
        to_remove += sremove
 
179
 
 
180
    return (to_add, to_remove)
 
181
 
239
182
 
240
183
def make_jail(user, config, force=True):
241
 
    """Create or update a user's jail.
 
184
    """Creates a new user's jail space, in the jail directory as configured in
 
185
    conf.py.
242
186
 
243
 
    Only the user-specific parts of the jail are created here - everything
244
 
    else is expected to be part of another aufs branch.
 
187
    This only creates things within /home - everything else is expected to be
 
188
    part of another UnionFS branch.
245
189
 
246
190
    Returns the path to the user's home directory.
247
191
 
248
192
    Chowns the user's directory within the jail to the given UID.
249
193
 
250
 
    @param force: If False, raise an exception if the user already has a jail.
251
 
                  If True (default), rebuild the jail preserving /home.
 
194
    force: If false, exception if jail already exists for this user.
 
195
    If true (default), overwrites it, but preserves home directory.
252
196
    """
253
197
    # MUST run as root or some of this may fail
254
198
    if os.getuid() != 0:
264
208
        os.mkdir(tempdir)
265
209
    userdir = os.path.join(jail_src_base, user.login)
266
210
    homedir = os.path.join(userdir, 'home')
267
 
    tmpdir = os.path.join(userdir, 'tmp')
268
211
    userhomedir = os.path.join(homedir, user.login)   # Return value
269
212
 
270
213
    if os.path.exists(userdir):
273
216
        # User jail already exists. Blow it away but preserve their home
274
217
        # directory. It should be all that is there anyway, but you never
275
218
        # know!
276
 
        # Ignore warnings about the use of tempnam
 
219
        # Ignore warnings about the use of tmpnam
277
220
        warnings.simplefilter('ignore')
278
221
        homebackup = os.tempnam(tempdir)
279
222
        warnings.resetwarnings()
282
225
        # NOTE that shutil.move changed in Python 2.6, it now moves a
283
226
        # directory INTO the target (like `mv`), which it didn't use to do.
284
227
        # This code works regardless.
285
 
        shutil.move(userhomedir, homebackup)
 
228
        shutil.move(homedir, homebackup)
286
229
        shutil.rmtree(userdir)
287
 
        os.makedirs(homedir)
288
 
        shutil.move(homebackup, userhomedir)
 
230
        os.makedirs(userdir)
 
231
        shutil.move(homebackup, homedir)
289
232
        # Change the ownership of all the files to the right unixid
290
233
        logging.debug("chown %s's home directory files to uid %d"
291
234
            %(user.login, user.unixid))
303
246
    make_ivle_conf(user.login, userdir, user.svn_pass, config)
304
247
    make_etc_passwd(user.login, userdir, config['paths']['jails']['template'],
305
248
                    user.unixid)
306
 
    os.makedirs(tmpdir)
307
 
    os.chmod(tmpdir, 01777)
308
249
 
309
250
    return userhomedir
310
251
 
311
252
def make_ivle_conf(username, user_jail_dir, svn_pass, sys_config):
312
 
    """Generate an ivle.conf for a user's jail.
313
 
 
 
253
    """
314
254
    Creates (overwriting any existing file, and creating directories) a
315
255
    file /etc/ivle/ivle.conf in a given user's jail.
316
 
 
317
256
    @param username: Username.
318
257
    @param user_jail_dir: User's jail dir, ie. ['jails']['src'] + username
319
258
    @param svn_pass: User's SVN password.
320
259
    @param sys_config: An ivle.config.Config object (the system-wide config).
321
260
    """
322
 
    conf_path = os.path.join(user_jail_dir, "home/.ivle.conf")
323
 
    if not os.path.exists(os.path.dirname(conf_path)):
324
 
        os.makedirs(os.path.dirname(conf_path))
 
261
    conf_path = os.path.join(user_jail_dir, "etc/ivle/ivle.conf")
 
262
    os.makedirs(os.path.dirname(conf_path))
325
263
 
326
264
    # In the "in-jail" version of conf, we don't need MOST of the details
327
265
    # (it would be a security risk to have them here).
328
266
    # So we just write root_dir.
329
267
    conf_obj = ivle.config.Config(blank=True)
330
268
    conf_obj.filename = conf_path
331
 
    conf_obj['urls'] = {}
332
269
    conf_obj['urls']['root'] = sys_config['urls']['root']
333
270
    conf_obj['urls']['public_host'] = sys_config['urls']['public_host']
334
271
    conf_obj['urls']['svn_addr'] = sys_config['urls']['svn_addr']
335
 
    conf_obj['user_info'] = {}
336
272
    conf_obj['user_info']['login'] = username
337
273
    conf_obj['user_info']['svn_pass'] = svn_pass
338
274
    conf_obj.write()
343
279
                        | stat.S_IROTH)
344
280
 
345
281
def make_etc_passwd(username, user_jail_dir, template_dir, unixid):
346
 
    """Create a passwd file for a user's jail.
347
 
 
 
282
    """
348
283
    Creates /etc/passwd in the given user's jail. This will be identical to
349
284
    that in the template jail, except for the added entry for this user.
350
285
    """
351
 
    template_passwd_path = os.path.join(template_dir, "home/.passwd")
352
 
    passwd_path = os.path.join(user_jail_dir, "home/.passwd")
 
286
    template_passwd_path = os.path.join(template_dir, "etc/passwd")
 
287
    passwd_path = os.path.join(user_jail_dir, "etc/passwd")
353
288
    passwd_dir = os.path.dirname(passwd_path)
354
289
    if not os.path.exists(passwd_dir):
355
290
        os.makedirs(passwd_dir)
358
293
    passwd_file.write('%s:x:%d:%d::/home/%s:/bin/bash'
359
294
                      % (username, unixid, unixid, username))
360
295
    passwd_file.close()
 
296
 
 
297
def mount_jail(login, config):
 
298
    # This is where we'll mount to...
 
299
    destdir = os.path.join(config['paths']['jails']['mounts'], login)
 
300
    # ... and this is where we'll get the user bits.
 
301
    srcdir = os.path.join(config['paths']['jails']['src'], login)
 
302
    try:
 
303
        if not os.path.exists(destdir):
 
304
            os.mkdir(destdir)
 
305
        if os.system('/bin/mount -t aufs -o dirs=%s:%s=ro none %s'
 
306
                     % (srcdir, config['paths']['jails']['template'],
 
307
                        destdir)) == 0:
 
308
            logging.info("mounted user %s's jail." % login)
 
309
        else:
 
310
            logging.error("failed to mount user %s's jail!" % login)
 
311
    except Exception, message:
 
312
        logging.warning(str(message))