#!/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 more RESTful wrt groups/users
#   /swami.cgi/user
#        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/user/<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/group/<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/group/<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 sys
import crypt
import random
import string
import pyward
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):
        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 rmusers(self, group, users):
        for u in users:
            try:
                self._data[group].remove(u)
            except ValueError:
                pass

    def remove(self, group):
        try:
            del self._data[group]
        except ValueError:
            pass

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


### Web framework stuff


# Some defaults: basic security and put a header on every page
class MyRESTNode(RESTNode):

    def __init__(self):
        self.user = None

    def _isAllowedTo(self, action):
        return action == 'GET' or self.user == User or IsAdminUser

    def _display(self, headers, bodytext):
        Header = '<a href="%s/user">users</a>' % (BaseURL)
        Header += ' - <a href="%s/group">groups</a>' % (BaseURL)
        # FIXME: Later
        #Header += ' - <a href="%s/">htaccess</a>' % (BaseURL)
        Header += '\n<hr>\n'
        return RESTNode._display(self, headers, Header+bodytext)


# Actual nodes
class UserNode(MyRESTNode):

    def __init__(self, user, passdb):
	MyRESTNode.__init__(self)
        self.user = user
        self.passdb = passdb

    def GET(self, query, body):
        if self.user in self.users():
            output = """
            <form method="post">
            <input type="hidden" name="action" value="delete">
            <input type="submit" value="Delete this user">
            </form>"""
            buttontext = "change"
        else:
            output = 'This user currently does not exist.'
            buttontext = "create"
        output += """\n<p>\n<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>\n""" % buttontext
        return output

    def PUT(self, query, body):
        password = body.getfirst('password')
        confirm = body.getfirst('confirm_password', password)
        if password != confirm:
            output = "Passwords don't match for user %s!  Try again.<p>" % self.user
        else:
            passdb.set(self.user, password)
            passdb.save()
            output = "Password set.<p>"
        return output + self.GET(query, body)

    POST = PUT

    def DELETE(self, query, body):
        self.passdb.remove(self.user)
        self.passdb.save()
        return self.GET(query, body)


class UserListNode(MyRESTNode, HTPasswd):

    def __init__(self):
	MyRESTNode.__init__(self)
        HTPasswd.__init__(self, HTPASSWD)

    def makeChild(self, childname):
        return UserNode(childname, self)

    def GET(self, query, body):
        output = """
        Current users:<p>"""
        for u in self.users():
            output += '<a href="%s/user/%s">%s</a> ' % (BaseURL, u, u)
        output += """<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>"""
        return output

    def PUT(self, query, body):
        self.clear() 
        return self.POST(query, body)

    def POST(self, query, body):
        users = body.getlist('username')
        passwds = body.getlist('password')
        confirms = body.getlist('confirm_password')
        for i in range(len(users)):
            if not hasPermsTo('PUT', "/user/"+users[i]):
                output = "No perms to modify %s.<p>" % users[i]
            elif (i < len(confirms)) and (confirms[i] != passwds[i]):
                output = "Passwords don't match for user %s!  Try again.<p>" % users[i]
            else:
                self.set(users[i], passwds[i])
        self.save()
        return output + self.GET(query, body)

    DELETE = GET


class GroupUserNode(MyRESTNode):

    def __init__(self, db, group, user):
	MyRESTNode.__init__(self)
        self.db = db
        self.group = group
        self.user = user

    def mod(self, query, body, how):
        how(self.group, [self.user])
        self.db.save()
        return self.GET(query, body)

    def PUT(self, query, body):
        return self.mod(query, body, self.db.set)

    POST = PUT

    def DELETE(self, query, body):
        return self.mod(query, body, self.db.rmusers)

    def GET(self, query, body):
        userlink = '<a href="%s/user/%s">user</a> ' % (BaseURL, self.user)
        output = '<form method="post">'
        if not self.user in self.db.get(self.group):
            output += '<input type="submit" value="Add"> %s to ' % userlink
        else:
            output += """<input type="hidden" name="action" value="delete">
            <input type="submit" value="Remove"> %s from """ % userlink
        output += '<a href="%s/group/%s">this group</a>.\n</form>' % (BaseURL, self.group)
        return output


class GroupNode(MyRESTNode):

    def __init__(self, group, db):
	MyRESTNode.__init__(self)
        self.group = group
        self.db = db

    def makeChild(self, piece):
        return GroupUserNode(self.db, self.group, piece)

    def mod(self, query, body, how):
        newuserlist = body.getfirst('usernames','').split()
        newuserlist += body.getlist('username')
        mod(self.group, newuserlist)
        self.db.save()
        return self.GET(query, body)

    def PUT(self, query, body):
        return self.mod(query, body, self.db.set)

    def POST(self, query, body):
        return self.mod(query, body, self.db.add)

    def DELETE(self, query, body):
        self.db.remove(group)
        self.db.save()
        return self.GET(query, body)

    def GET(self, query, body):
        if self.group not in self.db.groups():
            output = "This group does not exist."
        else:
            output = "Members of this group:<p>"
            for u in self.db.get(self.group):
                output +=  '<a href="%s/user/%s">%s</a> ' % (BaseURL, u, u)
        output +=  """<br />
        <form method="post">
        Add a member: <select name="username">
        """
        for u in self.db.users():
            output +=  "<option>",u,"</option>\n"
        output +=  """</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>
        """
        return output


class GroupListNode(MyRESTNode, HTGroup):

    def __init__(self):
        HTGroup.__init__(self, HTGROUP)
	MyRESTNode.__init__(self)

    def makeChild(self, piece):
        return GroupNode(piece, self)

    def PUT(self, query, body):
        self.clear()
        return self.POST(query, body)

    def POST(self, query, body):
        groups = body.getlist('groupname')
        userlists = body.getlist('usernames')
        for i in range(len(groups)):
            group = groups[i]
            users = userlists[i].strip().split()
            self.set(group, users)
        self.save()
        return self.GET(query, body)

    def GET(self, query, body):
        output = "Groups:<p />"
        for g in self.groups():
           output += '<a href="%s/group/%s">%s</a>: ' % (BaseURL, g, g)
           for u in self.get(g):
               output += '<a href="%s/group/%s/%s">%s</a> ' % (BaseURL, g, u, u)
           output += '<br />'
        output += """
        <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>
        """
        return output


class Root(MyRESTNode):
    childClasses = { 'user': UserListNode,
                     'group': GroupListNode
                   }

    def GET(self, query, body):
        return ''

## Actual execution path

# global values used above
User = os.environ.get('REMOTE_USER', 'admin');
BaseURL = os.environ.get('SCRIPT_NAME')
IsAdminUser = User in HTGroup(HTGROUP).get('swami-admin')

dispatch_cgi(Root())


