This commit is contained in:
crudo 2017-03-14 00:36:22 +01:00
commit ced4a29aed
6 changed files with 358 additions and 0 deletions

94
.gitignore vendored Normal file
View File

@ -0,0 +1,94 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# dotenv
.env
# virtualenv
.venv
venv/
ENV/
# Spyder project settings
.spyderproject
# Rope project settings
.ropeproject

95
app.py Executable file
View File

@ -0,0 +1,95 @@
#!/usr/bin/env python3
import os
from os import path
from configparser import ConfigParser
import bottle
from bottle import get, post, static_file, request, route, template
from bottle import SimpleTemplate
import ldap
@get('/')
def get_index():
return index_tpl()
@post('/')
def post_index():
form = request.forms.getunicode
def error(msg):
return index_tpl(username=form('username'), alerts=[('error', msg)])
if form('new-password') != form('confirm-password'):
return error("Password doesn't match the confirmation!")
if len(form('new-password')) < 8:
return error("Password must be at least 8 characters long!")
try:
ldap_change_password(form('username'),
form('old-password'), form('new-password'))
except Error as e:
print("Unsuccessful attemp to change password for {}: {}"
.format(form('username'), str(e)))
return error(str(e))
print("Password successfully changed for: {}"
.format(form('username')))
return index_tpl(alerts=[('success', "Password has been changed")])
@route('/static/<filename>', name='static')
def serve_static(filename):
return static_file(filename, root=path.join(BASE_DIR, 'static'))
def index_tpl(**kwargs):
return template('index', **kwargs)
def ldap_change_password(username, old, new):
dn_name = "uid={},{}".format(username, CONF['ldap']['base'])
l = ldap.initialize(CONF['ldap']['host'])
l.set_option(ldap.OPT_X_TLS_CACERTFILE, CONF['ldap']['tls_cacert'])
l.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_DEMAND)
l.start_tls_s()
l.simple_bind_s(dn_name, old)
l.passwd_s(dn_name, old, new)
l.unbind_s()
def read_config():
config = ConfigParser()
config.read([path.join(BASE_DIR, 'settings.ini'),
os.getenv('CONF_FILE', '')])
return config
class Error(Exception):
pass
BASE_DIR = path.dirname(__file__)
CONF = read_config()
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
bottle.TEMPLATE_PATH = [BASE_DIR]
# Set default attributes to pass into templates.
SimpleTemplate.defaults = dict(CONF['html'])
SimpleTemplate.defaults['url'] = bottle.url
# Run bottle internal server when invoked directly (mainly for development).
if __name__ == '__main__':
bottle.run(**CONF['server'])
# Run bottle in application mode (in production under uWSGI server).
else:
application = bottle.default_app()

43
index.tpl Normal file
View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<title>{{ page_title }}</title>
<link rel="stylesheet" href="{{ url('static', filename='style.css') }}">
</head>
<body>
<main>
<h1>{{ page_title }}</h1>
<form method="post">
<label for="username">Username</label>
<input id="username" name="username" value="{{ get('username', '') }}" type="text" required autofocus>
<label for="old-password">Old password</label>
<input id="old-password" name="old-password" type="password" required>
<label for="new-password">New password</label>
<input id="new-password" name="new-password" type="password"
pattern=".{8,}" x-moz-errormessage="Password must be at least 8 characters long." required>
<label for="confirm-password">Confirm new password</label>
<input id="confirm-password" name="confirm-password" type="password"
pattern=".{8,}" x-moz-errormessage="Password must be at least 8 characters long." required>
<button type="submit">Update password</button>
</form>
<div class="alerts">
%for type, text in get('alerts', []):
<div class="alert {{ type }}">{{ text }}</div>
%end
</div>
</main>
</body>
</html>

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
bottle>=0.12.0
configparser
python-ldap>=2.4.0

12
settings.ini Normal file
View File

@ -0,0 +1,12 @@
[html]
page_title = Change your password on unit.macaomilano.org
[ldap]
host = ldap://unit.macaomilano.org:389
base = ou=Hackers,dc=unit,dc=macaomilano,dc=org
tls_cacert = /etc/ssl/cert.pem
[server]
server = auto
host = localhost
port = 8080

111
static/style.css Normal file
View File

@ -0,0 +1,111 @@
/* TODO make it cooler! */
body {
font-family: sans-serif;
color: #333;
}
main {
margin: 0 auto;
}
h1 {
font-size: 2em;
margin-bottom: 2.5em;
margin-top: 2em;
text-align: center;
}
form {
border-radius: 0.2rem;
border: 1px solid #CCC;
margin: 0 auto;
max-width: 16rem;
padding: 2rem 2.5rem 1.5rem 2.5rem;
}
input {
background-color: #FAFAFA;
border-radius: 0.2rem;
border: 1px solid #CCC;
box-shadow: inset 0 1px 3px #DDD;
box-sizing: border-box;
display: block;
font-size: 1em;
padding: 0.4em 0.6em;
vertical-align: middle;
width: 100%;
}
input:focus {
background-color: #FFF;
border-color: #51A7E8;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075) inset, 0 0 5px rgba(81, 167, 232, 0.5);
outline: 0;
}
label {
color: #666;
display: block;
font-size: 0.9em;
font-weight: bold;
margin: 1em 0 0.25em 0;
}
button {
background-color: #60B044;
background-image: linear-gradient(#8ADD6D, #60B044);
border-radius: 0.2rem;
border: 1px solid #5CA941;
box-sizing: border-box;
color: #fff;
cursor: pointer;
display: block;
font-size: 0.9em;
font-weight: bold;
margin: 2em 0 0.5em 0;
padding: 0.5em 0.7em;
text-align: center;
text-decoration: none;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.3);
user-select: none;
vertical-align: middle;
white-space: nowrap;
}
button:focus,
button:hover {
background-color: #569E3D;
background-image: linear-gradient(#79D858, #569E3D);
border-color: #4A993E;
}
.alerts {
margin: 2rem auto 0 auto;
max-width: 30rem;
}
.alert {
border-radius: 0.2rem;
border: 1px solid;
color: #fff;
padding: 0.7em 1.5em;
}
.alert.error {
background-color: #E74C3C;
border-color: #C0392B;
}
.alert.success {
background-color: #60B044;
border-color: #5CA941;
}
@media only screen and (max-width: 480px) {
form {
border: 0;
}
}