added web interface code
added basic templates
This commit is contained in:
parent
f611eac8ad
commit
0cfc0ebb44
4 changed files with 326 additions and 0 deletions
204
htman.py
Normal file
204
htman.py
Normal file
|
@ -0,0 +1,204 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
$Id$
|
||||
|
||||
A web interface for managing htpasswd files.
|
||||
|
||||
Copyright 2010 Lars Kruse <devel@sumpfralle.de>
|
||||
|
||||
This module is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This module is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with This module. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import htpasswd
|
||||
import bobo
|
||||
import genshi.template
|
||||
import ConfigParser
|
||||
import re
|
||||
import sys
|
||||
import os
|
||||
|
||||
BASE_DIR = os.path.dirname(__file__)
|
||||
CONFIG_FILE_LOCATIONS = [os.path.join(BASE_DIR, "htman.conf"), '/etc/htman/htman.conf']
|
||||
TEMPLATES_DIR = os.path.join(BASE_DIR, 'templates')
|
||||
REGEX = {
|
||||
"username": r"[a-zA-Z0-9_\-\.]+$",
|
||||
"password": r"[a-zA-Z0-9_\-\.%\$_\;,<>=+\[\]\{\}\(\)#\'\"\/\&\*@]+$",
|
||||
"mapping": r"[a-zA-Z0-9_\-\.]+$",
|
||||
}
|
||||
# use the environment variable HTMAN_CONFIG as the config file location if the
|
||||
# variable is defined
|
||||
if "HTMAN_CONFIG" in os.environ:
|
||||
CONFIG_FILE_LOCATIONS = [os.environ["HTMAN_CONFIG"]]
|
||||
|
||||
@bobo.query('')
|
||||
@bobo.query('/')
|
||||
def show_frontpage():
|
||||
values = web_defaults.copy()
|
||||
return render("frontpage.html", **values)
|
||||
|
||||
@bobo.query('/admin')
|
||||
def show_files():
|
||||
values = web_defaults.copy()
|
||||
# The template expects a list of tuples: (mapping name, admin-url).
|
||||
# We assume, that the admin-url is just below the main admin URL. Thus
|
||||
# there is no need for generating a specific URL.
|
||||
values["mapping"] = [(mapping, mapping) for mapping in mapping.keys()]
|
||||
return render("list_mappings.html", **values)
|
||||
|
||||
@bobo.query('/manage')
|
||||
def show_htpasswd(htname=None):
|
||||
if (htname is None) or (not htname in mapping) or (not re.match(REGEX["mapping"], htname)):
|
||||
return bobo.redirect(web_defaults["base_url"])
|
||||
else:
|
||||
# do a redirect: this allows the webserver to check the permissions for this URL
|
||||
return bobo.redirect("%smanage/%s" % (web_defaults["base_url"], str(htname)))
|
||||
|
||||
# the alternative "/admin" URL allows to define super-user htaccess rules via
|
||||
# Apache's "Location" directive
|
||||
@bobo.query('/admin/:htname')
|
||||
@bobo.query('/manage/:htname')
|
||||
def manage_htpasswd(htname=None, action=None, username=None, password=None):
|
||||
values = web_defaults.copy()
|
||||
values["error"] = None
|
||||
values["success"] = None
|
||||
if (htname is None) or (not htname in mapping) or (not re.match(REGEX["mapping"], htname)):
|
||||
return bobo.redirect(web_defaults["base_url"])
|
||||
values["htname"] = htname
|
||||
htpasswd_file = mapping[htname]
|
||||
do_create_file = not os.path.isfile(htpasswd_file)
|
||||
try:
|
||||
htdb = htpasswd.HtpasswdFile(htpasswd_file, create=do_create_file)
|
||||
except IOError:
|
||||
values["error"] = "Failed to read htpasswd file"
|
||||
htdb = None
|
||||
else:
|
||||
if action is None:
|
||||
# just show the current state
|
||||
pass
|
||||
elif not username:
|
||||
values["error"] = "The username may not be empty!"
|
||||
elif not re.match(REGEX["username"], username):
|
||||
values["error"] = "The username contains invalid characters!"
|
||||
elif action == "del":
|
||||
if username in htdb.get_usernames():
|
||||
htdb.delete(username)
|
||||
htdb.save()
|
||||
values["success"] = "User removed"
|
||||
else:
|
||||
values["error"] = "The user does not exist!"
|
||||
elif not password:
|
||||
values["error"] = "The password may not be empty!"
|
||||
elif not re.match(REGEX["password"], password):
|
||||
values["error"] = "The password contains invalid characters!"
|
||||
elif action == "update":
|
||||
if username in htdb.get_usernames():
|
||||
htdb.update(username, password)
|
||||
htdb.save()
|
||||
values["success"] = "Password changed"
|
||||
else:
|
||||
values["error"] = "The user does not exist!"
|
||||
elif action == "new":
|
||||
if not username in htdb.get_usernames():
|
||||
htdb.update(username, password)
|
||||
htdb.save()
|
||||
values["success"] = "User added"
|
||||
else:
|
||||
values["error"] = "The user exists already!"
|
||||
else:
|
||||
values["error"] = "Invalid action"
|
||||
if not htdb is None:
|
||||
values["usernames"] = htdb.get_usernames()
|
||||
else:
|
||||
values["usernames"] = []
|
||||
# show the current htpasswd file
|
||||
return render("manage_users.html", **values)
|
||||
|
||||
def get_config():
|
||||
config = ConfigParser.SafeConfigParser()
|
||||
for filename in CONFIG_FILE_LOCATIONS:
|
||||
if os.path.isfile(filename):
|
||||
try:
|
||||
config.read(filename)
|
||||
except IOError:
|
||||
# ignore errors
|
||||
pass
|
||||
else:
|
||||
return config
|
||||
print >>sys.stderr, "Failed to load config file from %s" % str(CONFIG_FILE_LOCATIONS)
|
||||
sys.exit(1)
|
||||
|
||||
def get_mapping(config):
|
||||
try:
|
||||
mapping_file = config.get("Locations", "mapping")
|
||||
except ConfigParser.NoOptionError:
|
||||
print >>sys.stderr, "The location of the mapping file is not " \
|
||||
"defined in the config file: [Locations] -> mapping"
|
||||
sys.exit(2)
|
||||
if not os.path.isfile(mapping_file):
|
||||
print >>sys.stderr, "The mapping file does not exist: %s" \
|
||||
% str(mapping_file)
|
||||
sys.exit(2)
|
||||
# read the mapping file
|
||||
try:
|
||||
input_data = open(mapping_file, "r").readlines()
|
||||
except IOError:
|
||||
print >>sys.stderr, "Failed to open mapping file: %s" % mapping_file
|
||||
sys.exit(4)
|
||||
mapping = {}
|
||||
for line in input_data:
|
||||
if line.startswith("#"):
|
||||
continue
|
||||
if "=" in line:
|
||||
name, location = line.split("=", 1)
|
||||
name = name.strip()
|
||||
location = location.strip()
|
||||
# ignore invalid mapping names
|
||||
if re.match(REGEX["mapping"], name):
|
||||
mapping[name] = location
|
||||
return mapping
|
||||
|
||||
def get_templates_dir(config):
|
||||
try:
|
||||
templates_dir = config.get("Locations", "templates")
|
||||
except ConfigParser.NoOptionError:
|
||||
# use the default
|
||||
templates_dir = TEMPLATES_DIR
|
||||
if not os.path.isdir(templates_dir):
|
||||
print >>sys.stderr, "Templates directory not found: %s" % templates_dir
|
||||
sys.exit(3)
|
||||
return os.path.abspath(templates_dir)
|
||||
|
||||
def render(filename, input_data=None, **values):
|
||||
stream = loader.load(filename).generate(**values)
|
||||
if not input_data is None:
|
||||
stream |= genshi.filters.HTMLFormFiller(data=input_data)
|
||||
return stream.render("html", doctype="html")
|
||||
|
||||
|
||||
# *** Initialization ***
|
||||
config = get_config()
|
||||
# defaults to the standard TEMPLATES_DIR
|
||||
mapping = get_mapping(config)
|
||||
templates_dir = get_templates_dir(config)
|
||||
web_defaults = dict(config.items("Web"))
|
||||
loader = genshi.template.TemplateLoader(templates_dir, auto_reload=False)
|
||||
|
||||
|
||||
# this line allows to use mod_wsgi
|
||||
# see: http://groups.google.com/group/bobo-web/msg/2ba55fc381658cd1
|
||||
# see: http://blog.dscpl.com.au/2009/08/using-bobo-on-top-of-modwsgi.html
|
||||
if __name__ == "__main__":
|
||||
application = bobo.Application(bobo_resources=__name__)
|
||||
|
18
templates/frontpage.html
Normal file
18
templates/frontpage.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
<!DOCTYPE html>
|
||||
<html xml:lang="de" lang="de"
|
||||
xmlns="http://www.w3.org/1999/xhtml"
|
||||
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||
xmlns:py="http://genshi.edgewall.org/">
|
||||
<xi:include href="layout.html" />
|
||||
|
||||
<head/>
|
||||
|
||||
<body>
|
||||
<form name="show_htpasswd" action="manage" method="POST">
|
||||
<label for="name">Access zone:</label>
|
||||
<input type="text" size="30" name="htname" id="name" />
|
||||
<input type="submit" name="submit" value="Show" />
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
|
44
templates/layout.html
Normal file
44
templates/layout.html
Normal file
|
@ -0,0 +1,44 @@
|
|||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||
xmlns:py="http://genshi.edgewall.org/" py:strip="">
|
||||
|
||||
<py:match path="head" once="true">
|
||||
<head py:attrs="select('@*')">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||
<title>${title}</title>
|
||||
<link rel="stylesheet" href="${css_url}" type="text/css" />
|
||||
<link rel="shortcut icon" href="${favicon_url}" type="image/vnd.microsoft.icon" />
|
||||
<link rel="icon" href="${favicon_url}" type="image/vnd.microsoft.icon" />
|
||||
</head>
|
||||
</py:match>
|
||||
|
||||
<py:match path="body" once="true">
|
||||
<body py:attrs="select('@*')">
|
||||
|
||||
<div id="header">
|
||||
<table border="0">
|
||||
<tr><td>
|
||||
<a href="/index.html"><img src="/logo.png" alt="sl-logo" style="border:none"/></a>
|
||||
</td>
|
||||
<td><h1><a href="/index.html">systemausfall.org</a> </h1></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="content">${select('*|text()')}</div>
|
||||
|
||||
<div id="footer">
|
||||
<table width="100%">
|
||||
<tr><td style="width:70%"><div class="text">
|
||||
"A designer knows he has achieved perfection not when there is nothing left to add, but when there is nothing left to take away." -- Antoine de Saint-Exupéry
|
||||
</div></td>
|
||||
<td style="width:10%"><div class="link">
|
||||
<a href="/packungsbeilage.html">impressum</a>
|
||||
</div></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</py:match>
|
||||
</html>
|
||||
|
60
templates/manage_users.html
Normal file
60
templates/manage_users.html
Normal file
|
@ -0,0 +1,60 @@
|
|||
<!DOCTYPE html>
|
||||
<html xml:lang="de" lang="de"
|
||||
xmlns="http://www.w3.org/1999/xhtml"
|
||||
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||
xmlns:py="http://genshi.edgewall.org/">
|
||||
<xi:include href="layout.html" />
|
||||
|
||||
<head/>
|
||||
|
||||
<body>
|
||||
<h2>Manage user access for <i>${htname}</i></h2>
|
||||
|
||||
<div py:if="error" class="error">${error}</div>
|
||||
|
||||
<h3>Add user</h3>
|
||||
<form name="add-passwd" action="" method="POST">
|
||||
<label for="username_add">User name:</label>
|
||||
<input type="text" size="20" name="username" id="username_add" />
|
||||
<br/>
|
||||
<label for="password_add">Password:</label>
|
||||
<input type="text" size="20" name="password" id="password_add" />
|
||||
<input type="hidden" name="action" value="new" />
|
||||
<input type="hidden" name="htname" value="${htname}"/>
|
||||
<input type="submit" name="submit" value="Add user" />
|
||||
</form>
|
||||
|
||||
<py:if test="usernames">
|
||||
<hr/>
|
||||
|
||||
<h3>Change password</h3>
|
||||
<form name="update-passwd" action="" method="POST">
|
||||
<label for="username_update">User name:</label>
|
||||
<select name="username" id="username_update" size="1">
|
||||
<option py:for="username in usernames">${username}</option>
|
||||
</select>
|
||||
<br/>
|
||||
<label for="password_update">New password:</label>
|
||||
<input type="text" size="20" name="password" id="password_update" />
|
||||
<input type="hidden" name="action" value="update" />
|
||||
<input type="hidden" name="htname" value="${htname}"/>
|
||||
<input type="submit" name="submit" value="Change password" />
|
||||
</form>
|
||||
|
||||
<hr/>
|
||||
|
||||
<h3>Delete user</h3>
|
||||
<form name="del-passwd" action="" method="POST">
|
||||
<label for="username_del">User name:</label>
|
||||
<select name="username" id="username_del" size="1">
|
||||
<option py:for="username in usernames">${username}</option>
|
||||
</select>
|
||||
<input type="hidden" name="action" value="del" />
|
||||
<input type="hidden" name="htname" value="${htname}"/>
|
||||
<input type="submit" name="submit" value="Delete user" />
|
||||
</form>
|
||||
</py:if>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
Loading…
Reference in a new issue