Author: blackbird Date: 2007-05-25 10:45:15 +0200 (Fri, 25 May 2007) New Revision: 5138 Added: trunk/sandbox/py-rest-doc/sphinx/templates/admin/manage_users.html Modified: trunk/sandbox/py-rest-doc/sphinx-web.py trunk/sandbox/py-rest-doc/sphinx/style/admin.css trunk/sandbox/py-rest-doc/sphinx/templates/admin/index.html trunk/sandbox/py-rest-doc/sphinx/web/application.py trunk/sandbox/py-rest-doc/sphinx/web/userdb.py trunk/sandbox/py-rest-doc/sphinx/web/util.py Log: added user control panel Modified: trunk/sandbox/py-rest-doc/sphinx/style/admin.css =================================================================== --- trunk/sandbox/py-rest-doc/sphinx/style/admin.css 2007-05-25 06:32:45 UTC (rev 5137) +++ trunk/sandbox/py-rest-doc/sphinx/style/admin.css 2007-05-25 08:45:15 UTC (rev 5138) @@ -21,6 +21,10 @@ color: white; } +div.admin form form { + display: inline; +} + div.admin input, div.admin textarea { font-family: 'Bitstream Vera Sans', 'Arial', sans-serif; font-size: 13px; @@ -69,20 +73,20 @@ padding: 5px 10px 5px 10px; } -div.admin table.comments { +div.admin table.mapping { width: 100%; border: 1px solid #092835; border-collapse: collapse; background-color: #05171e; } -div.admin table.comments th { +div.admin table.mapping th { background-color: #092835; border-bottom: 1px solid #05171e; padding: 5px; } -div.admin table.comments td { +div.admin table.mapping td { border: 1px solid #092835; border-left: none; border-right: none; @@ -91,44 +95,52 @@ padding: 0 5px 0 5px; } -div.admin table.comments tr:hover { +div.admin table.mapping tr:hover { background-color: #081c25; } -div.admin table.comments td.pub_date { +div.admin table.mapping td.username { + width: 180px; +} + +div.admin table.mapping td.pub_date { font-style: italic; text-align: right; } -div.admin table.comments td.actions input { +div.admin table.mapping td.groups input { + width: 100%; +} + +div.admin table.mapping td.actions input { padding: 0; } -div.admin table.comments td.actions { +div.admin table.mapping .actions { text-align: right; width: 70px; } -div.admin table.comments span.meta { +div.admin table.mapping span.meta { font-size: 11px; color: #aaa; } -div.admin table.comments span.meta a { +div.admin table.mapping span.meta a { color: #aaa; } -div.admin div.comment_form dl { +div.admin div.detail_form dl { color: white; } -div.admin div.comment_form dt { +div.admin div.detail_form dt { clear: both; float: left; width: 110px; } -div.admin div.comment_form textarea { +div.admin div.detail_form textarea { width: 98%; height: 160px; } Modified: trunk/sandbox/py-rest-doc/sphinx/templates/admin/index.html =================================================================== --- trunk/sandbox/py-rest-doc/sphinx/templates/admin/index.html 2007-05-25 06:32:45 UTC (rev 5137) +++ trunk/sandbox/py-rest-doc/sphinx/templates/admin/index.html 2007-05-25 08:45:15 UTC (rev 5138) @@ -8,6 +8,9 @@ <ul> <li><a href="moderate_comments/">Moderate Comments</a></li> <li><a href="change_password/">Change Password</a></li> + {%- if is_master_admin %} + <li><a href="manage_users/">Manage Users</a></li> + {%- endif %} <li><a href="../">Back To Documentation</a></li> <li><a href="logout/">Logout</a></li> </ul> Added: trunk/sandbox/py-rest-doc/sphinx/templates/admin/manage_users.html =================================================================== --- trunk/sandbox/py-rest-doc/sphinx/templates/admin/manage_users.html 2007-05-25 06:32:45 UTC (rev 5137) +++ trunk/sandbox/py-rest-doc/sphinx/templates/admin/manage_users.html 2007-05-25 08:45:15 UTC (rev 5138) @@ -0,0 +1,75 @@ +{% extends "admin/layout.html" %} +{% block admin_body %} + <h1>Manage Users</h1> + <form action="" method="post"> + {% if ask_confirmation %} + <div class="dialog"> + <h2>Confirm</h2> + <div class="text"> + {% trans amount=to_delete|length, first_user=to_delete[0]|e, + all_users=to_delete|join(', ')|e %} + Do you really want to delete the user {{ first_user }}? + {% pluralize %} + Do you really want to delete the {{ amount }} users + {{ all_users }}? + {% endtrans %} + </div> + <div class="buttons"> + <input type="hidden" name="update" value="yes"> + <input type="submit" name="confirmated" value="Yes"> + <input type="submit" name="aborted" value="No"> + </div> + </div> + {% endif %} + {% if generated_user and generated_password %} + <div class="dialog"> + <h2>User Generated</h2> + <div class="text"> + The user <strong>{{ generated_user|e }}</strong> was generated successfully + with the password <strong>{{ generated_password|e }}</strong>. + </div> + </div> + {% endif %} + {% if self_destruction %} + <div class="dialog"> + <h2>Error</h2> + <div class="text"> + You can't delete your own user or remove your own master privileges. + </div> + </div> + {% endif %} + {% if add_user_mode %} + <div class="dialog detail_form"> + <h2>Add User</h2> + <div class="text"> + Username <input type="text" size="24" name="username" value="{{ + form.username|e(true) }}"> + </div> + <div class="buttons"> + <input type="submit" name="add_user" value="Add"> + <input type="submit" name="aborted" value="Cancel"> + </div> + </div> + {% endif %} + <table class="mapping"> + <tr> + <th>Username</th> + <th>Privileges</th> + <th class="actions">Delete</th> + </tr> + {%- for user, privileges in users|dictsort %} + <tr> + <td class="username">{{ user|e }}</td> + <td class="groups"><input type="text" name="privileges-{{ user|e }}" value="{{ privileges|join(', ') }}"></td> + <td class="actions"><input type="checkbox" name="delete" value="{{ user|e + }}"{% if user in to_delete %} checked{% endif %}></td> + </tr> + {%- endfor %} + </table> + <div class="actions"> + <input type="submit" name="update" value="Update"> + <input type="submit" name="add_user" value="Add User"> + <input type="submit" name="cancel" value="Cancel"> + </div> + </form> +{% endblock %} Modified: trunk/sandbox/py-rest-doc/sphinx/web/application.py =================================================================== --- trunk/sandbox/py-rest-doc/sphinx/web/application.py 2007-05-25 06:32:45 UTC (rev 5137) +++ trunk/sandbox/py-rest-doc/sphinx/web/application.py 2007-05-25 08:45:15 UTC (rev 5138) @@ -210,6 +210,11 @@ """ Get some administration pages. """ + is_master_admin = False + is_logged_in = req.user is not None + if is_logged_in: + is_master_admin = 'master' in self.userdb.privileges[req.user] + if page == 'login': if req.user is not None: return RedirectResponse('admin/') @@ -226,7 +231,7 @@ return Response(render_template(req, 'admin/login.html', { 'login_failed': login_failed })) - elif req.user is None: + elif not is_logged_in: return RedirectResponse('admin/login/') elif page == 'logout': req.logout() @@ -251,11 +256,6 @@ details_for = page[18:] + '.rst' or None to_delete = set() edit_detail = None - for item in req.form.getlist('delete'): - try: - to_delete.add(int(item)) - except ValueError: - pass if 'edit' in req.args: try: @@ -264,6 +264,11 @@ pass if req.method == 'POST': + for item in req.form.getlist('delete'): + try: + to_delete.add(int(item)) + except ValueError: + pass if req.form.get('cancel'): return RedirectResponse('admin/') elif req.form.get('confirmated'): @@ -275,7 +280,7 @@ return RedirectResponse('admin/' + page) elif req.form.get('aborted'): return RedirectResponse('admin/' + page) - elif req.form.get('edit'): + elif req.form.get('edit') and not to_delete: try: edit_detail = Comment.get(int(req.args['edit'])) except ValueError: @@ -300,10 +305,79 @@ 'ask_confirmation': req.method == 'POST' and to_delete, 'edit_detail': edit_detail })) + elif page == 'manage_users' and is_master_admin: + add_user_mode = False + user_privileges = {} + users = sorted((user, []) for user in self.userdb.users) + to_delete = set() + generated_user = generated_password = None + + if req.method == 'POST': + for item in req.form.getlist('delete'): + try: + to_delete.add(item) + except ValueError: + pass + for name, item in req.form.iteritems(): + if name.startswith('privileges-'): + user_privileges[name[11:]] = [x.strip() for x + in item.split(',')] + if req.form.get('cancel'): + return RedirectResponse('admin/') + elif req.form.get('add_user'): + username = req.form.get('username') + if username: + generated_password = self.userdb.add_user(username) + self.userdb.save() + generated_user = username + else: + add_user_mode = True + elif req.form.get('aborted'): + return RedirectResponse('admin/manage_users/') + + users = {} + for user in self.userdb.users: + if not user in user_privileges: + users[user] = sorted(self.userdb.privileges[user]) + else: + users[user] = user_privileges[user] + + new_users = users.copy() + for user in to_delete: + new_users.pop(user, None) + + self_destruction = not req.user in new_users or \ + 'master' not in new_users[req.user] + + if req.method == 'POST' and (not to_delete or + (to_delete and req.form.get('confirmated'))) and \ + req.form.get('update'): + old_users = self.userdb.users.copy() + for user in old_users: + if user not in new_users: + del self.userdb.users[user] + else: + self.userdb.privileges[user].clear() + self.userdb.privileges[user].update(new_users[user]) + self.userdb.save() + return RedirectResponse('admin/manage_users/') + + return Response(render_template(req, 'admin/manage_users.html', { + 'users': users, + 'add_user_mode': add_user_mode, + 'to_delete': to_delete, + 'ask_confirmation': req.method == 'POST' and to_delete \ + and not self_destruction, + 'generated_user': generated_user, + 'generated_password': generated_password, + 'self_destruction': self_destruction + })) elif page == '': - return Response(render_template(req, 'admin/index.html')) + return Response(render_template(req, 'admin/index.html', { + 'is_master_admin': is_master_admin + })) else: - raise ValueError() + raise RedirectResponse('admin/') pretty_type = { 'data': 'module data', Modified: trunk/sandbox/py-rest-doc/sphinx/web/userdb.py =================================================================== --- trunk/sandbox/py-rest-doc/sphinx/web/userdb.py 2007-05-25 06:32:45 UTC (rev 5137) +++ trunk/sandbox/py-rest-doc/sphinx/web/userdb.py 2007-05-25 08:45:15 UTC (rev 5138) @@ -13,33 +13,69 @@ from __future__ import with_statement from os import path from hashlib import sha1 +from random import choice, randrange +from collections import defaultdict +def gen_password(length=8, add_numbers=True, mix_case=True, + add_special_char=True): + """ + Generate a pronounceable password. + """ + if length <= 0: + raise ValueError('requested password of length <= 0') + consonants = 'bcdfghjklmnprstvwz' + vowels = 'aeiou' + if mix_case: + consonants = consonants * 2 + consonants.upper() + vowels = vowels * 2 + vowels.upper() + pw = ''.join([choice(consonants) + + choice(vowels) + + choice(consonants + vowels) for _ + in xrange(length // 3 + 1)])[:length] + if add_numbers: + n = length // 3 + if n > 0: + pw = pw[:-n] + for _ in xrange(n): + pw += choice('0123456789') + if add_special_char: + tmp = randrange(0, len(pw)) + l1 = pw[:tmp] + l2 = pw[tmp:] + if max(len(l1), len(l2)) == len(l1): + l1 = l1[:-1] + else: + l2 = l2[:-1] + return l1 + choice('#$&%?!') + l2 + return pw + + class UserDatabase(object): def __init__(self, filename): self.filename = filename self.users = {} + self.privileges = defaultdict(set) if path.exists(filename): with file(filename) as f: for line in f: - if line.strip(): - user, password = line.split(':', 1) - self.users[user.strip()] = password.strip() + line = line.strip() + if line and line[0] != '#': + parts = line.split(':') + self.users[parts[0]] = parts[1] + self.privileges[parts[0]].update(parts[2].split(',')) - def add_user(self, user, password): - if user in self.users: - raise ValueError('user %r already exists' % user) + def set_password(self, user, password): + """Encode the password for a user (also adds users).""" self.users[user] = sha1('%s|%s' % (user, password)).hexdigest() - def remove_user(self, user): - del self.users[user] + def add_user(self, user): + """Add a new user and return the generated password.""" + pw = gen_password(8, add_special_char=False) + self.set_password(user, pw) + return pw - def set_password(self, user, password): - if user not in self.users: - raise ValueError('unknown user %r' % user) - self.users[user] = sha1('%s|%s' % (user, password)).hexdigest() - def check_password(self, user, password): return user in self.users and \ self.users[user] == sha1('%s|%s' % (user, password)).hexdigest() @@ -47,4 +83,5 @@ def save(self): with file(self.filename, 'w') as f: for username, password in self.users.iteritems(): - f.write('%s:%s\n' % (username, password)) + privileges = ','.join(self.privileges.get(username, ())) + f.write('%s:%s:%s\n' % (username, password, privileges)) Modified: trunk/sandbox/py-rest-doc/sphinx/web/util.py =================================================================== --- trunk/sandbox/py-rest-doc/sphinx/web/util.py 2007-05-25 06:32:45 UTC (rev 5137) +++ trunk/sandbox/py-rest-doc/sphinx/web/util.py 2007-05-25 08:45:15 UTC (rev 5138) @@ -683,8 +683,7 @@ mime_type = guessed_type[0] start_response('200 OK', [('Content-Type', mime_type)]) with file(filename, 'rb') as f: - result = f.read() - return iter([result]) + return [f.read()] def __call__(self, environ, start_response): p = environ.get('PATH_INFO', '') Modified: trunk/sandbox/py-rest-doc/sphinx-web.py =================================================================== --- trunk/sandbox/py-rest-doc/sphinx-web.py 2007-05-25 06:32:45 UTC (rev 5137) +++ trunk/sandbox/py-rest-doc/sphinx-web.py 2007-05-25 08:45:15 UTC (rev 5138) @@ -35,8 +35,7 @@ srv.serve_forever() except KeyboardInterrupt: pass - + if __name__ == '__main__': sys.exit(main(sys.argv)) - |