#!/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 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):
        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()


## Library code


def dispatch(RootNode, path):
    ## FIXME:  this might do the wrong thing if there're /s in the
    ##         query string.  
    # break up the path
    pieces = [ p for p in path.split('/') if p != '']
    ## parse cgi input
    ## put GET data (ie. 'query string' stuff) into getform
    ## and POST data into postform
    if os.environ['REQUEST_METHOD'].upper() == 'POST':
        postform = cgi.FieldStorage()
        # this is a cheat b/c most browsers don't PUT or DELETE too well
        action = postform.getfirst('action', 'POST').upper()
        os.environ['REQUEST_METHOD'] = 'GET'
    else:
        postform = {}
        action = 'GET'
    getform = cgi.FieldStorage()
    # my RESTful promise (also keeps people from causing trouble)
    if not action in ['GET', 'PUT', 'POST', 'DELETE']:
        action = 'GET'
    # dispatch
    sys.stderr.write("%s %s\n" % (action, repr(pieces)))
    RootNode._dispatch(action, pieces, getform, postform)


class RESTNode(object):

    # default way to map a child URI-space onto a RESTNode-descended class
    childClasses = {} 

    def makeChild(self, childname):
        # override this for dynamically-named resources
        try:
            return self.childClasses[childname]()
        except KeyError:
            return ErrorNotFound(childname)

    # these all get thier 'piece' as an arg to them
    def GET(self, query, body):
        pass

    POST = GET
    PUT = POST
    DELETE = PUT

    def _isAllowedTo(self, action):
        # authorization hook
        # action is always one of GET/PUT/POST/DELETE
        return 1

    # this is the internal dispatch mechanism
    def _dispatch(self, action, pieces, query, body):
        if len(pieces) < 1:
            # get the correct method
            if self._isAllowedTo(action):
                method = getattr(self, action)
            else:
                method = ErrorPermission(action, path).GET
            # call the method
            result = method(query, body)
            # make up blank headers if none specified
            if type(result) != type([]):
                result = ['', result]
            self._display(result[0], result[1])
        else:
            # find the correct child
            child = self.makeChild(pieces[0])
            # their turn to dispatch
            child._dispatch(action, pieces[1:], query, body)

    def _display(self, headers, bodytext):
        # make sure the results are displayable
        if headers.find("Content-type: ") < 0 :
            # supply a default Content-type if there's not one
            headers = 'Content-type: text/html\n' + headers
        # display the results
        print headers
        print '\n\n'
        print bodytext


class ErrorNotFound(RESTNode):

    def __init__(self, piece):
        self.piece = piece

    def GET(self, query, post):
        return ['Status: 404 Not Found', 
                "<h1>No Resource named %s Found.</h1>\n" % self.piece ]


class ErrorPermission(RESTNode):

    def __init__(self, action, path):
        return ['Status: 300',
                "<h1>Not allowed to %s %s. </h1>\n" % (action, path) ]


### End Library
### Start Instantiation


# 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 node
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

#
# 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')
# where were they going?
pathName = os.environ.get('PATH_INFO','/')
# global flag
IsAdminUser = User in HTGroup(HTGROUP).get('swami-admin')

dispatch(Root(), pathName)


