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

465 by mattgiuca
Added new app: userservice, which is an ajax service for user management
1
# IVLE
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
# App: userservice
19
# Author: Matt Giuca
20
# Date: 14/2/2008
21
478 by mattgiuca
scripts/usrmgr-server: Renamed actions from dashes to underscores.
22
# Provides an Ajax service for handling user management requests.
23
# This includes when a user logs in for the first time.
24
25
# NOTE: This app does NOT require authentication. This is because otherwise it
26
# would be blocked from receiving requests to activate when the user is trying
27
# to accept the TOS.
28
29
# It must do its own authentication and authorization.
30
486 by mattgiuca
caps: Added a few new capabilities.
31
### Actions ###
32
33
# The one-and-only path segment to userservice determines the action being
34
# undertaken.
35
# All actions require that you are logged in.
36
# All actions require method = POST, unless otherwise stated.
37
38
# userservice/activate_me
39
# Required cap: None
40
# declaration = "I accept the IVLE Terms of Service"
41
# Activate the currently-logged-in user's account. Requires that "declaration"
42
# is as above, and that the user's state is "no_agreement".
43
44
# userservice/create_user
45
# Required cap: CAP_CREATEUSER
46
# Arguments are the same as the database columns for the "login" table.
47
# Required:
48
#   login, fullname, rolenm
49
# Optional:
50
#   password, nick, email, studentid
51
52
# userservice/get_user
53
# method: May be GET
54
# Required cap: None to see yourself.
55
#   CAP_GETUSER to see another user.
56
# Gets the login details of a user. Returns as a JSON object.
57
# login = Optional login name of user to get. If omitted, get yourself.
58
59
# userservice/update_user
60
# Required cap: None to update yourself.
61
#   CAP_UPDATEUSER to update another user (and also more fields).
62
#   (This is all-powerful so should be only for admins)
63
# login = Optional login name of user to update. If omitted, update yourself.
64
# Other fields are optional, and will set the given field of the user.
65
# Without CAP_UPDATEUSER, you may change the following fields of yourself:
66
#   password, nick, email
67
# With CAP_UPDATEUSER, you may also change the following fields of any user:
68
#   password, nick, email, login, rolenm, unixid, fullname, studentid
69
# (You can't change "state", but see userservice/[en|dis]able_user).
70
71
# TODO
72
# userservice/enable_user
73
# Required cap: CAP_UPDATEUSER
74
# Enable a user whose account has been disabled. Does not work for
75
# no_agreement or pending users.
76
# login = Login name of user to enable.
77
78
# TODO
79
# userservice/disable_user
80
# Required cap: CAP_UPDATEUSER
81
# Disable a user's account. Does not work for no_agreement or pending users.
82
# login = Login name of user to disable.
83
465 by mattgiuca
Added new app: userservice, which is an ajax service for user management
84
import os
85
import sys
86
87
import cjson
88
478 by mattgiuca
scripts/usrmgr-server: Renamed actions from dashes to underscores.
89
import common
552 by mattgiuca
userservice: Small fixes to update_user.
90
import common.db
478 by mattgiuca
scripts/usrmgr-server: Renamed actions from dashes to underscores.
91
from common import (util, chat, caps)
465 by mattgiuca
Added new app: userservice, which is an ajax service for user management
92
import conf
93
487 by mattgiuca
setup.py: Added code to convert usrmgt_port into an int before writing to conf
94
from conf import (usrmgt_host, usrmgt_port, usrmgt_magic)
465 by mattgiuca
Added new app: userservice, which is an ajax service for user management
95
96
# The user must send this declaration message to ensure they acknowledge the
97
# TOS
98
USER_DECLARATION = "I accept the IVLE Terms of Service"
99
100
def handle(req):
101
    """Handler for the Console Service AJAX backend application."""
506 by mattgiuca
dispatch.__init__, dispatch.request, cgirequest:
102
    if req.user is None:
478 by mattgiuca
scripts/usrmgr-server: Renamed actions from dashes to underscores.
103
        # Not logged in
550 by mattgiuca
userservice: Added error messages.
104
        req.throw_error(req.HTTP_FORBIDDEN,
105
        "You are not logged in to IVLE.")
465 by mattgiuca
Added new app: userservice, which is an ajax service for user management
106
    if len(req.path) > 0 and req.path[-1] == os.sep:
107
        path = req.path[:-1]
108
    else:
109
        path = req.path
110
    # The path determines which "command" we are receiving
486 by mattgiuca
caps: Added a few new capabilities.
111
    fields = req.get_fieldstorage()
536 by mattgiuca
apps/userservice: Generalised searching for actions (dict mapping action names to
112
    try:
113
        func = actions_map[req.path]
114
    except KeyError:
550 by mattgiuca
userservice: Added error messages.
115
        req.throw_error(req.HTTP_BAD_REQUEST,
116
        "%s is not a valid userservice action." % repr(req.path))
536 by mattgiuca
apps/userservice: Generalised searching for actions (dict mapping action names to
117
    func(req, fields)
465 by mattgiuca
Added new app: userservice, which is an ajax service for user management
118
486 by mattgiuca
caps: Added a few new capabilities.
119
def handle_activate_me(req, fields):
465 by mattgiuca
Added new app: userservice, which is an ajax service for user management
120
    """Create the jail, svn, etc, for the currently logged in user (this is
121
    put in the queue for usermgt to do).
122
    This will block until usermgt returns, which could take seconds to minutes
123
    in the extreme. Therefore, it is designed to be called by Ajax, with a
124
    nice "Please wait" message on the frontend.
125
126
    This will signal that the user has accepted the terms of the license
127
    agreement, and will result in the user's database status being set to
128
    "enabled". (Note that it will be set to "pending" for the duration of the
129
    handling).
130
131
    As such, it takes a single POST field, "declaration", which
132
    must have the value, "I accept the IVLE Terms of Service".
133
    (Otherwise users could navigate to /userservice/createme without
134
    "accepting" the terms - at least this way requires them to acknowledge
135
    their acceptance). It must only be called through a POST request.
136
    """
497 by mattgiuca
userservice: activate_me is now database enabled. It checks the state from the
137
    db = common.db.DB()
486 by mattgiuca
caps: Added a few new capabilities.
138
    try:
497 by mattgiuca
userservice: activate_me is now database enabled. It checks the state from the
139
        if req.method != "POST":
550 by mattgiuca
userservice: Added error messages.
140
            req.throw_error(req.HTTP_METHOD_NOT_ALLOWED,
141
            "Only POST requests are valid methods to activate_me.")
497 by mattgiuca
userservice: activate_me is now database enabled. It checks the state from the
142
        try:
143
            declaration = fields.getfirst('declaration')
144
        except AttributeError:
550 by mattgiuca
userservice: Added error messages.
145
            declaration = None      # Will fail next test
497 by mattgiuca
userservice: activate_me is now database enabled. It checks the state from the
146
        if declaration != USER_DECLARATION:
550 by mattgiuca
userservice: Added error messages.
147
            req.throw_error(req.HTTP_BAD_REQUEST,
148
            "Please use the Terms of Service form instead of talking to "
149
            "this service directly.")
497 by mattgiuca
userservice: activate_me is now database enabled. It checks the state from the
150
151
        # Make sure the user's status is "no_agreement", and set status to
152
        # pending, within the one transaction. This ensures we only do this
153
        # one time.
154
        db.start_transaction()
155
        try:
156
506 by mattgiuca
dispatch.__init__, dispatch.request, cgirequest:
157
            user_details = db.get_user(req.user.login)
497 by mattgiuca
userservice: activate_me is now database enabled. It checks the state from the
158
            # Check that the user's status is "no_agreement".
159
            # (Both to avoid redundant calls, and to stop disabled users from
160
            # re-enabling their accounts).
500 by mattgiuca
db: get_user and get_users now return User objects instead of dictionaries.
161
            if user_details.state != "no_agreement":
550 by mattgiuca
userservice: Added error messages.
162
                req.throw_error(req.HTTP_BAD_REQUEST,
163
                "You have already agreed to the terms.")
497 by mattgiuca
userservice: activate_me is now database enabled. It checks the state from the
164
            # Write state "pending" to ensure we don't try this again
506 by mattgiuca
dispatch.__init__, dispatch.request, cgirequest:
165
            db.update_user(req.user.login, state="pending")
497 by mattgiuca
userservice: activate_me is now database enabled. It checks the state from the
166
        except:
167
            db.rollback()
168
            raise
169
        db.commit()
170
171
        # Get the arguments for usermgt.activate_user from the session
172
        # (The user must have already logged in to use this app)
173
        args = {
505 by mattgiuca
dispatch.html, consoleservice, userservice, interpret:
174
            "login": req.user.login,
497 by mattgiuca
userservice: activate_me is now database enabled. It checks the state from the
175
        }
176
        msg = {'activate_user': args}
177
178
        response = chat.chat(usrmgt_host, usrmgt_port, msg, usrmgt_magic,
179
            decode = False)
180
        # Write to the user's session to allow them to be activated
505 by mattgiuca
dispatch.html, consoleservice, userservice, interpret:
181
        req.user.state = "enabled"
498 by mattgiuca
apps/userservice: Now writes to req.session to avoid having to log out.
182
        session = req.get_session()
505 by mattgiuca
dispatch.html, consoleservice, userservice, interpret:
183
        session['user'] = req.user
498 by mattgiuca
apps/userservice: Now writes to req.session to avoid having to log out.
184
        session.save()
497 by mattgiuca
userservice: activate_me is now database enabled. It checks the state from the
185
        # Write the response
186
        req.content_type = "text/plain"
187
        req.write(response)
188
    finally:
189
        db.close()
486 by mattgiuca
caps: Added a few new capabilities.
190
676 by mattgiuca
usrmgt-server: Moved SVN checkout code from activate_me into a separate
191
def handle_do_checkout(req, fields):
192
    """Check out the user's repositories into their home directory, failing
193
    silently for any that already exist.
194
    This can be done in a limited form by any user, on their own account,
195
    or with full powers by a user with CAP_UPDATEUSER on any account.
196
    """
197
    if req.method != "POST":
198
        req.throw_error(req.HTTP_METHOD_NOT_ALLOWED,
677 by mattgiuca
userservice: Fix error message text.
199
        "Only POST requests are valid methods to do_checkout.")
676 by mattgiuca
usrmgt-server: Moved SVN checkout code from activate_me into a separate
200
201
    # Only give full powers if this user has CAP_UPDATEUSER
202
    fullpowers = req.user.hasCap(caps.CAP_UPDATEUSER)
203
    # List of fields that may be changed
204
    fieldlist = (update_user_fields_admin if fullpowers
205
        else update_user_fields_anyone)
206
207
    try:
208
        login = fields.getfirst('login')
209
        if login is None:
210
            raise AttributeError()
211
        if not fullpowers and login != req.user.login:
212
            # Not allowed to edit other users
213
            req.throw_error(req.HTTP_FORBIDDEN,
214
            "You do not have permission to check out another user's "
215
            "repository.")
216
    except AttributeError:
217
        # If login not specified, update yourself
218
        login = req.user.login
219
220
    msg = {'do_checkout': {"login": login}}
221
222
    response = chat.chat(usrmgt_host, usrmgt_port, msg, usrmgt_magic,
223
        decode = False)
224
    req.content_type = "text/plain"
225
    req.write(response)
226
486 by mattgiuca
caps: Added a few new capabilities.
227
create_user_fields_required = [
228
    'login', 'fullname', 'rolenm'
229
]
230
create_user_fields_optional = [
548 by mattgiuca
www/apps/userservice: Allows unixid as an argument to create_user.
231
    'password', 'nick', 'email', 'studentid', 'unixid'
486 by mattgiuca
caps: Added a few new capabilities.
232
]
233
def handle_create_user(req, fields):
234
    """Create a new user, whose state is no_agreement.
235
    This does not create the user's jail, just an entry in the database which
236
    allows the user to accept an agreement.
237
    """
238
    if req.method != "POST":
550 by mattgiuca
userservice: Added error messages.
239
        req.throw_error(req.HTTP_METHOD_NOT_ALLOWED,
240
            "Only POST requests are valid methods to create_user.")
505 by mattgiuca
dispatch.html, consoleservice, userservice, interpret:
241
    # Check if this user has CAP_UPDATEUSER
242
    if not req.user.hasCap(caps.CAP_UPDATEUSER):
550 by mattgiuca
userservice: Added error messages.
243
        req.throw_error(req.HTTP_FORBIDDEN,
244
        "You do not have permission to create users.")
486 by mattgiuca
caps: Added a few new capabilities.
245
246
    # Make a dict of fields to create
247
    create = {}
248
    for f in create_user_fields_required:
536 by mattgiuca
apps/userservice: Generalised searching for actions (dict mapping action names to
249
        val = fields.getfirst(f)
250
        if val is not None:
251
            create[f] = val
252
        else:
550 by mattgiuca
userservice: Added error messages.
253
            req.throw_error(req.HTTP_BAD_REQUEST,
254
            "Required field %s missing." % repr(f))
486 by mattgiuca
caps: Added a few new capabilities.
255
    for f in create_user_fields_optional:
536 by mattgiuca
apps/userservice: Generalised searching for actions (dict mapping action names to
256
        val = fields.getfirst(f)
257
        if val is not None:
258
            create[f] = val
259
        else:
486 by mattgiuca
caps: Added a few new capabilities.
260
            pass
261
262
    # Get the arguments for usermgt.create_user from the session
263
    # (The user must have already logged in to use this app)
264
    msg = {'create_user': create}
265
487 by mattgiuca
setup.py: Added code to convert usrmgt_port into an int before writing to conf
266
    response = chat.chat(usrmgt_host, usrmgt_port, msg, usrmgt_magic,
486 by mattgiuca
caps: Added a few new capabilities.
267
        decode = False)
268
    req.content_type = "text/plain"
269
    req.write(response)
270
271
update_user_fields_anyone = [
272
    'password', 'nick', 'email'
273
]
274
update_user_fields_admin = [
552 by mattgiuca
userservice: Small fixes to update_user.
275
    'password', 'nick', 'email', 'rolenm', 'unixid', 'fullname',
486 by mattgiuca
caps: Added a few new capabilities.
276
    'studentid'
277
]
278
def handle_update_user(req, fields):
279
    """Update a user's account details.
280
    This can be done in a limited form by any user, on their own account,
281
    or with full powers by a user with CAP_UPDATEUSER on any account.
282
    """
283
    if req.method != "POST":
550 by mattgiuca
userservice: Added error messages.
284
        req.throw_error(req.HTTP_METHOD_NOT_ALLOWED,
677 by mattgiuca
userservice: Fix error message text.
285
        "Only POST requests are valid methods to update_user.")
486 by mattgiuca
caps: Added a few new capabilities.
286
287
    # Only give full powers if this user has CAP_UPDATEUSER
505 by mattgiuca
dispatch.html, consoleservice, userservice, interpret:
288
    fullpowers = req.user.hasCap(caps.CAP_UPDATEUSER)
486 by mattgiuca
caps: Added a few new capabilities.
289
    # List of fields that may be changed
290
    fieldlist = (update_user_fields_admin if fullpowers
291
        else update_user_fields_anyone)
292
293
    try:
294
        login = fields.getfirst('login')
552 by mattgiuca
userservice: Small fixes to update_user.
295
        if login is None:
296
            raise AttributeError()
506 by mattgiuca
dispatch.__init__, dispatch.request, cgirequest:
297
        if not fullpowers and login != req.user.login:
486 by mattgiuca
caps: Added a few new capabilities.
298
            # Not allowed to edit other users
550 by mattgiuca
userservice: Added error messages.
299
            req.throw_error(req.HTTP_FORBIDDEN,
300
            "You do not have permission to update another user.")
486 by mattgiuca
caps: Added a few new capabilities.
301
    except AttributeError:
302
        # If login not specified, update yourself
506 by mattgiuca
dispatch.__init__, dispatch.request, cgirequest:
303
        login = req.user.login
486 by mattgiuca
caps: Added a few new capabilities.
304
305
    # Make a dict of fields to update
306
    update = {}
307
    for f in fieldlist:
536 by mattgiuca
apps/userservice: Generalised searching for actions (dict mapping action names to
308
        val = fields.getfirst(f)
309
        if val is not None:
310
            update[f] = val
311
        else:
486 by mattgiuca
caps: Added a few new capabilities.
312
            pass
552 by mattgiuca
userservice: Small fixes to update_user.
313
    update['login'] = login
486 by mattgiuca
caps: Added a few new capabilities.
314
315
    # Get the arguments for usermgt.create_user from the session
316
    # (The user must have already logged in to use this app)
317
    args = {
505 by mattgiuca
dispatch.html, consoleservice, userservice, interpret:
318
        "login": req.user.login,
486 by mattgiuca
caps: Added a few new capabilities.
319
        "update": update,
320
    }
321
    msg = {'update_user': args}
322
487 by mattgiuca
setup.py: Added code to convert usrmgt_port into an int before writing to conf
323
    response = chat.chat(usrmgt_host, usrmgt_port, msg, usrmgt_magic,
486 by mattgiuca
caps: Added a few new capabilities.
324
        decode = False)
591 by mattgiuca
userservice: Added code to update req.session after updating the user who is
325
326
    # Re-read the user's details from the DB so we can update their session
327
    # XXX potentially-unsafe session write
328
    if login == req.user.login:
329
        db = common.db.DB()
330
        user = db.get_user(login)
331
        session = req.get_session()
332
        session['user'] = user
333
        session.save()
334
        db.close()
335
486 by mattgiuca
caps: Added a few new capabilities.
336
    req.content_type = "text/plain"
337
    req.write(response)
536 by mattgiuca
apps/userservice: Generalised searching for actions (dict mapping action names to
338
552 by mattgiuca
userservice: Small fixes to update_user.
339
def handle_get_user(req, fields):
340
    """
341
    Retrieve a user's account details. This returns all details which the db
554 by mattgiuca
userservice: Does not return svn_pass to the user.
342
    module is willing to give up, EXCEPT the following fields:
343
        svn_pass
552 by mattgiuca
userservice: Small fixes to update_user.
344
    """
345
    # Only give full powers if this user has CAP_GETUSER
346
    fullpowers = req.user.hasCap(caps.CAP_GETUSER)
347
348
    try:
349
        login = fields.getfirst('login')
350
        if login is None:
351
            raise AttributeError()
352
        if not fullpowers and login != req.user.login:
353
            # Not allowed to edit other users
354
            req.throw_error(req.HTTP_FORBIDDEN,
355
            "You do not have permission to see another user.")
356
    except AttributeError:
357
        # If login not specified, update yourself
358
        login = req.user.login
359
360
    # Just talk direct to the DB
361
    db = common.db.DB()
362
    user = db.get_user(login)
363
    db.close()
364
    user = dict(user)
365
    if 'role' in user:
366
        user['rolenm'] = str(user['role'])
367
        del user['role']
554 by mattgiuca
userservice: Does not return svn_pass to the user.
368
    try:
369
        del user['svn_pass']
370
    except KeyError:
371
        pass
674 by mattgiuca
userservice: Fixed encoding of User objects into JSON
372
    # Convert time stamps to nice strings
373
    try:
374
        if user['pass_exp'] is not None:
375
            user['pass_exp'] = str(user['pass_exp'])
376
    except KeyError:
377
        pass
378
    try:
379
        if user['acct_exp'] is not None:
380
            user['acct_exp'] = str(user['acct_exp'])
381
    except KeyError:
382
        pass
383
    try:
384
        if user['last_login'] is not None:
385
            user['last_login'] = str(user['last_login'])
386
    except KeyError:
387
        pass
554 by mattgiuca
userservice: Does not return svn_pass to the user.
388
    response = cjson.encode(user)
552 by mattgiuca
userservice: Small fixes to update_user.
389
    req.content_type = "text/plain"
390
    req.write(response)
391
536 by mattgiuca
apps/userservice: Generalised searching for actions (dict mapping action names to
392
# Map action names (from the path)
393
# to actual function objects
394
actions_map = {
395
    "activate_me": handle_activate_me,
676 by mattgiuca
usrmgt-server: Moved SVN checkout code from activate_me into a separate
396
    "do_checkout": handle_do_checkout,
536 by mattgiuca
apps/userservice: Generalised searching for actions (dict mapping action names to
397
    "create_user": handle_create_user,
398
    "update_user": handle_update_user,
552 by mattgiuca
userservice: Small fixes to update_user.
399
    "get_user": handle_get_user,
536 by mattgiuca
apps/userservice: Generalised searching for actions (dict mapping action names to
400
}