#!/usr/bin/python
#
# SWAMI - Simple Web Authentication Management Interface
#
# Currently, .htaccess files provide simple directory-based auth using 
# the 'Basic Auth' technique.  This is nice, as far as it goes.  The only
# problem is that writing .htaccess files... sucks.  So the idea here is to
# 1) allow the user (presumably the site owner/admin) to create and destroy
#    .htaccess files in any subdir of the managed tree
# 2) let the user manage associated htpasswd/htgroup files
#
# Now with a nice RESTful API:
#   /swami.cgi/~
#        gets the list of users, shows the form to add/remove/modify them
#     POST action=post, username=<username>, password=<password>[, confirm-password=<pass>]
#        adds username/password to the list; fail if password != confirm-password
#     POST action=put, username=<username, password=<password>, ...
#        replace the list of username/passwords with the ones in username and password
#        (preserve ordering)
#   /swami.cgi/~<username>
#        show the form to chnage the user's password
#     POST action=put/post, password=<password>[, confirm-password=<pass>]
#        sets password, possibly creating user too; fail if password != confirm-password
#     POST action=delete
#        deletes the user
#  /swami.cgi/:
#        gets the list of groups, shows the form to add/remove/modify them
#     POST action=post, groupname=<groupname>, usernames=<space separated member list>
#        add the groupname with usernames to the list
#     POST action=put, groupname=<groupname>, usernames=<space separated member list>, ...
#        replace the list with the ones in groupname/members
#  /swami.cgi/:<groupname>
#        show users of this group.  Show the form to add/remove them
#     POST action=post, usernames=<space-separted user list>
#     POST action=post, username=<username>, ...
#        add usernames to this group
#     POST action=put, usernames=<space-separted user list>
#     POST action=put, username=<username>, ...
#        set the usernames in this group
#     POST action=delete
#        delete the group
#  /swami.cgi/:<groupname>/<username>
#        show form to add/remove this user from this group
#     POST action=put/post
#        add the username to the group
#     POST action=delete
#        delete the username from the group
#

import os
import cgi
import sys
import crypt
import random
import string
from xreadlines import xreadlines

# You might want to modify these if you're running swami in more than one place
HTPASSWD = '/home/pj/.swami.htpasswd'
HTGROUP = '/home/pj/.swami.htgroup'

###### Code starts here

class HTPasswd(object):
    def __init__(self, filename):
        self.filename = filename
        self._data = {}
        if not os.path.exists(filename):
            # doesn't exist, so create it with some defaults
            self.set('admin','admin')
            self.save()
        else:
            datafile = open(filename, "r")
            for line in xreadlines(datafile):
                line = line.strip()
                # skip blank lines
                if not line: continue
        	# skip trash lines
                try:
                    user, epass = line.split(':')
                    self._data[user] = epass
        	except:
        	    sys.stderr.write("Bad line in %s: %s" % (filename, line))
            datafile.close()
            if not os.access(filename, os.W_OK):
                raise Exception("No write access to %s" % filename)

    def clear(self):
        self._data = {}

    def users(self):
        return self._data.keys()

    def set(self, username, password):
        # set username's password
        ## first generate a decent salt
        saltchoice = string.ascii_letters + string.digits
        salt = random.choice(saltchoice) + random.choice(saltchoice)
        # now crypt() things
        self._data[username] = crypt.crypt(password, salt)

    def remove(self, username):
        del self._data[username]

    def save(self):
        datafile = open(self.filename, 'w')
        for user in self._data.keys():
            epass = self._data[user]
            datafile.write(user+':'+epass+'\n')
        datafile.close() 


class HTGroup(object):
    def __init__(self, filename):
        self.filename = filename
        self._data = {}
        if not os.path.exists(filename):
            # doesn't exist, so create it with some defaults
            self.set('swami-admin',['admin'])
            self.save()
        else:
            datafile = open(filename, 'r')
            for line in xreadlines(datafile):
                line = line.strip()
        	# skip blank lines
        	if not line: continue
                (group, users) = line.split(':')
                self._data[group] = users.split()
            datafile.close()
            if not os.access(filename, os.W_OK):
                raise Exception("No write access to %s" % filename)

    def groups(self):
        return self._data.keys()

    def set(self, group, users):
        if type(users) != type([]):
 	    raise Exception("wrong argtype to HTGroup.set()")
        self._data[group] = users

    def get(self, group):
        return self._data[group]

    def getUser(self, user):
        return [g for g in self._data.keys() if user in self._data[g]]

    def add(self, group, users):
        for u in users:
            if u not in self._data[group]:
                self._data[group].append(u)

    def save(self):
        datafile = open(self.filename, 'w')
        for group in self._data.keys():
	    if self._data[group]:
                datafile.write(group+': '+' '.join(self._data[group])+'\n')
        datafile.close()

def setadd(l1,l2):
    result = []
    for i in l1+l2:
        if not i in result:
            result.append(i)
    return result

#
# see who it is; if no auth, then it must be admin! (and we should set up auth!)
#
User = os.environ.get('REMOTE_USER', 'admin');
BaseURL = os.environ.get('SCRIPT_NAME')

print "Content-type: text/html\n\n"
print '<a href="%s/~">users</a>' % (BaseURL)
print ' - <a href="%s/:">groups</a>' % (BaseURL)
print ' - <a href="%s/">htaccess</a>' % (BaseURL)
print '<hr>\n'
try:
    #
    # create HTPASSWD and HTGROUP files and populate with
    # user admin, password admin and group swami-admin, user admin
    PassDB = HTPasswd(HTPASSWD)
    GroupDB = HTGroup(HTGROUP)
except:
    print "Sorry, couldn't create some config files.  You lose."
    sys.exit(1)

# parse cgi input
if os.environ['REQUEST_METHOD'].upper() == 'POST':
    postform = cgi.FieldStorage()
    os.environ['REQUEST_METHOD'] = 'GET'
    action = postform.getfirst('action', 'POST').upper()
else:
    postform = {}
    action = 'GET'
getform = cgi.FieldStorage()

# my RESTful promise
if not action in ['GET', 'PUT', 'POST', 'DELETE']:
   print "Unknown action specified.\n"
   sys.exit(0)

# basic perms checking
def hasPermsTo(action, pathName):
    # swami-admins can do anything
    if User in GroupDB.get('swami-admin'):
        return 1
    # users can modify their own stuff
    if pathName[1:2] == '/~':
        return pathName[3:] == User; 
    return 0

# where were they going?
pathName = os.environ.get('PATH_INFO','/')
# normalize it: start with a slash, but don't end with one
if pathName[0] != '/': pathName = '/' + pathName
if len(pathName) > 1 and pathName[-1] == '/': pathName = pathName[:-1]

# dispatch
if len(pathName) > 1 and pathName[1] == '~':
    ## user management
    # dig out the username
    username = pathName[2:].split('/')[0]
    if not username:
        # operate on the list of users
	## handle put/post/delete
        if action == 'PUT':
            if hasPermsto('delete', "/~"):
                   PassDB.clear() 
        if action in ['POST', 'PUT']:
            users = postform.getlist('username')
            passwds = postform.getlist('password')
            confirms = postform.getlist('confirm_password')
            for i in range(len(users)):
                if not hasPermsTo(action, "/~"+users[i]):
	            print "No perms to modify %s.<p>" % users[i]
                elif (i < len(confirms)) and (confirms[i] != passwds[i]):
        	    print "Passwords don't match for user %s!  Try again.<p>" % users[i]
		else:
                    PassDB.set(users[i], passwds[i])
            PassDB.save()
        ## begin display (get)
        print """
	Current users:<p>"""
	for u in PassDB.users():
	    print '<a href="%s/~%s">%s</a> ' % (BaseURL, u, u)
        print """
	<hr>
        <form method="post">
	Add a user named <input type="text" name="username">
	with password <input type="text" name="password">
	and again <input type="text" name="confirm_password">.
	<input type="submit" value="create">
	</form>
        """
    else:
        # per-user commands
	## handle put/post/delete
        if not hasPermsTo(action, pathName):
            print "No perms to modify %s.<p>" % username
        elif action in ['POST', 'PUT']:
            password = postform.getfirst('password')
    	    confirm = postform.getfirst('confirm_password', password)
	    if password != confirm:
        	print "Passwords don't match for user %s!  Try again.<p>" % username
            else:
		PassDB.set(username, password)
                PassDB.save()
		print "Password set.<p>"
        elif action == "DELETE":
	    PassDB.remove(username)
	    PassDB.save()
        ## begin display (get)
	print '<a href="%s/~">Up</a><p />' % (BaseURL)
	if username in PassDB.users():
	    print """
	    <form method="post">
	    <input type="hidden" name="action" value="delete">
	    <input type="submit" value="Delete this user">
	    </form>"""
	    buttontext = "change"
	else:
	    print 'This user currently does not exist.'
	    buttontext = "create"
        print """
	<p>
	<form method="post">
	Set password to <input type="text" name="password">
	and again <input type="text" name="confirm_password">.
	<input type="submit" value="%s">
	</form>
        """ % buttontext

elif len(pathName) > 1 and pathName[1] == ':':
    ## group management
    # dig out the group and username (if they exist)
    group = pathName[2:].split('/')[0]
    try:
        username = pathName[2:].split('/')[1]
    except IndexError:
        username = ''
    if not hasPermsTo(action, pathName):
        print "No perms to %s %s.<p>" % (action, pathName) 
    elif not group:
        # Manipulate the entire set of groups
	## handle put/post/delete
        if action == 'PUT':
	    GroupDB.clear() 
        if action in ['PUT', 'POST']:
	    groups = postform.getlist('groupname')
	    userlists = postform.getlist('usernames')
	    for i in range(len(groups)):
	        group = groups[i]
		users = userlists[i].strip().split()
	        GroupDB.set(group, users)
	    GroupDB.save()
        ## begin display (get)
        print "Groups:<p />"
        for g in GroupDB.groups():
	   print '<a href="%s/:%s">%s</a>: ' % (BaseURL, g, g)
	   for u in GroupDB.get(g):
	       print '<a href="%s/:%s/%s">%s</a> ' % (BaseURL, g, u, u)
           print '<br />'
        print """
        <form method="post">
	Add a group named <input type="text" name="groupname"> with
	(space separated) members <input type="text" name="usernames"><br />
	<input type="submit" value="create">
	</form>
	"""
    elif not username:
        # one group
	## handle put/post/delete
        if action in ['PUT', 'POST']:
            newuserlist = postform.getfirst('usernames','').split()
	    newuserlist += postform.getlist('username')
        if action == 'PUT':
	    GroupDB.set(group, newuserlist)
	elif action == 'POST':
	    oldusers = GroupDB.get(group)
	    print "<!-- setadding %s to %s -->" % (repr(oldusers), repr(newuserlist))
	    GroupDB.set(group, setadd(oldusers,newuserlist))
        elif action == 'DELETE':
	    GroupDB.remove(group)
	if action != 'GET':
	    GroupDB.save()
        ## begin display (get)
	if group not in GroupDB.groups():
	    print "This group does not exist."
	else:
	    print "Members of this group:<p>"
	    for u in GroupDB.get(group):
	        print '<a href="%s/~%s">%s</a> ' % (BaseURL, u, u)
        print """<br />
        <form method="post">
	Add a member: <select name="username">
	"""
        for u in PassDB.users():
            print "<option>",u,"</option>\n"
        print """</select>
        <input type="submit" value="add">
	</form>
	<form method="post">
	<input type="hidden" name="action" value="delete">
	<input type="submit" value="Delete this group">
	</form>
        """
    else:
        # got group and username, just deal with action
	oldusers = GroupDB.get(group)
        if action in ['PUT', 'POST']:
	    GroupDB.set(group, setadd(oldusers,[username]))
        elif action == 'DELETE':
	    GroupDB.set(group, [u for u in oldusers if u != username])
        print '<a href="%s/~%s">This user</a> ' % (BaseURL, username)
	if username in PassDB.users():
	    print "exists"
	else:
	    print "does not exist.<p \>"
	print '<form method="post">'
	if not username in GroupDB.get(group):
	    print '<input type="submit" value="Add"> user to '
	else:
	    print """
	    <input type="hidden" name="action" value="delete">
	    <input type="submit" value="Remove"> user from """
	print '<a href="%s/:%s">this group</a>.' % (BaseURL, group)
	print '</form>'

else:
    pass
    # deal with .htaccess files
    #print """
    #(this is bogus, just to brainstorm ideas)
    #<br>
    #&lt;Limit <input type="checkbox" name="limit_get" value="y"> GET
    #<input type="checkbox" name="limit_get" value="y"> POST
    #<input type="checkbox" name="limit_get" value="y"> PUT
    #<input type="checkbox" name="limit_get" value="y"> DELETE
    #<input type="checkbox" name="limit_get" value="y"> DELETE 
    #&gt;
    #<p>
    #<input type="checkbox" name="require_valid" value="y"> require valid-user
    ##<input type="something" name="new_require_group" value="y"> require <select name="require_group">
    #"""
    #for g in GroupDB.groups():
    #    print '<option>%s</option>\n' % g
    #print """
    #</select>
    #"""

