#!/usr/bin/env python # # Copyright (c) 2006 - 2010 Benjamin Schweizer. All rights reserved. # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # # # Abstract # ~~~~~~~~ # htpasswd_editor provides a text user interface for htpasswd(1) files. # It can be used as a companion with pam_pwdfile(x) or Apache. # It required the python-newt library. # # Authors # ~~~~~~~ # Benjamin Schweizer, http://benjamin-schweizer.de/contact # # Changes # ~~~~~~~ # 2010-04-28, benjamin: added monkeypatch for broken debian newt library # 2010-04-27, benjamin: updated docs, changed license from BSD to ISC. # 2008-09-17, benjamin: bugfixs and error handling for broken files # 2008-06-09, benjamin: code cleanups and generalization # 2006-06-02, benjamin: initial release # # Bugs # ~~~~ # - the umask is hard-coded # - the monkeypatch is always applied; check for actual bug # try: from snack import * except ImportError: raise SystemExit("cannot find the python-newt library.") import os import sys import crypt import random, string # the debian 4/5 newt library is broken and bug reports are # open since ages; i've given up on this issue: # - http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=340366 # - https://bugzilla.redhat.com/show_bug.cgi?id=248878 # below is a monkey patch; it cannot reliable check if the # bug is there, so newer/fixed versions get patched also. # TODO: ask upstream if they add __version__ to snack.py def EntryWindow(screen, title, text, prompts, allowCancel = 1, width = 40, entryWidth = 20, buttons = [ 'Ok', 'Cancel' ], help = None): """ EntryWindow(screen, title, text, prompts, allowCancel = 1, width = 40, entryWidth = 20, buttons = [ 'Ok', 'Cancel' ], help = None): """ bb = ButtonBar(screen, buttons); t = TextboxReflowed(width, text) count = 0 for n in prompts: count = count + 1 sg = Grid(2, count) count = 0 entryList = [] for n in prompts: if (type(n) == types.TupleType): (n, e) = n #e = Entry(entryWidth, e) else: e = Entry(entryWidth) sg.setField(Label(n), 0, count, padding = (0, 0, 1, 0), anchorLeft = 1) sg.setField(e, 1, count, anchorLeft = 1) count = count + 1 entryList.append(e) g = GridFormHelp(screen, title, help, 1, 3) g.add(t, 0, 0, padding = (0, 0, 0, 1)) g.add(sg, 0, 1, padding = (0, 0, 0, 1)) g.add(bb, 0, 2, growx = 1) result = g.runOnce() entryValues = [] count = 0 for n in prompts: entryValues.append(entryList[count].value()) count = count + 1 return (bb.buttonPressed(result), tuple(entryValues)) def usage(): print """ usage: htpasswd_editor FILENAME FILENAME := some htpasswd file """ class Htpasswd(dict): """handles htpasswd(1) files as python dict""" def __init__(self, filename): self.filename = filename try: fh = open(self.filename, 'rb') line = fh.readline() while line: username, password_crypt = line[:-1].split(':') #self[username] = password_crypt dict.__setitem__(self, username, password_crypt) line = fh.readline() except IOError, e: pass def save(self): fh = open(self.filename, 'wb') for key in self: value = self[key] fh.write("%s:%s\n" % (key, value)) fh.close() def __setitem__(self, key, value): salt = random.choice(string.letters + string.digits) + random.choice(string.letters + string.digits) dict.__setitem__(self, key, crypt.crypt(value, salt)) class PasswordEditor: def __init__(self, screen, htpasswd_filename): self.screen = screen try: self.htpasswd = Htpasswd(htpasswd_filename) except ValueError: raise SystemExit('cannot parse password file.') button = False while True: dialog = ListboxChoiceWindow( self.screen, 'Account Overview', 'Please select an user account:', list(self.htpasswd) or [''], scroll=True, height=screen.height-18, buttons=['Add', 'Modify', 'Delete', 'Done'] ) (button, selection) = dialog if self.htpasswd: username = list(self.htpasswd)[selection] if button == 'modify': self.modify(username) elif button == 'add': self.add() elif button == 'delete': self.delete(username) elif button == 'done': break def add(self): dialog = EntryWindow( self.screen, 'Account Creation', 'Please enter the account name:', ['Account Name'], ) (button, selection) = dialog if button != 'ok': return username = selection[0] self.modify(username) def modify(self, username): dialog = EntryWindow( self.screen, 'Account Details', 'Modify the settings for "%s"' % username, [ ('Password', Entry(50, '', hidden=True)), ('Password (again)', Entry(50, '', hidden=True)) ] ) (button, selection) = dialog if button != 'ok': return password = selection[0] password2 = selection[1] if password == '': dialog = ButtonChoiceWindow(screen, 'Error', 'Password is empty', buttons=['Ok']) self.modify(username) if password != password2: dialog = ButtonChoiceWindow(screen, 'Error', 'Passwords mismatch', buttons=['Ok']) self.modify(username) self.htpasswd[username] = password self.htpasswd.save() def delete(self, username): dialog = ButtonChoiceWindow( self.screen, 'Account Deletion', 'Are you sure that you want to delete "%s"?' % username, buttons=['Yes', 'No'] ) if dialog != 'yes': return del self.htpasswd[username] self.htpasswd.save() if __name__ == '__main__': if len(sys.argv) != 2: usage() sys.exit(1) os.umask(0002) screen = SnackScreen() screen.drawRootText(0, 0, 'Htpasswd Editor') screen.drawRootText(screen.width - 28, 0, '(c) 2010 Benjamin Schweizer.') try: passwordeditor = PasswordEditor(screen, htpasswd_filename=sys.argv[1]) except: screen.finish() raise screen.finish() # eof.