diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..cb2dfd8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.{css,tpl}] +indent_size = 2 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..752edf1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright 2015 Jakub Jirutka . + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.adoc b/README.adoc new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/README.adoc diff --git a/app.py b/app.py new file mode 100644 index 0000000..8fb5d9a --- /dev/null +++ b/app.py @@ -0,0 +1,82 @@ +import bottle +from bottle import get, post, static_file, request, route, template +from bottle import SimpleTemplate +from configparser import ConfigParser +from ldap3 import Connection, LDAPBindError, LDAPInvalidCredentialsResult, Server +from ldap3 import AUTH_SIMPLE, SUBTREE +from os import path + + +@get('/') +def get_index(): + return index_tpl() + + +@post('/') +def post_index(): + form = request.forms.get + + 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!") + + if not change_password(form('username'), form('old-password'), form('new-password')): + return error("Username or password is incorrect!") + + return index_tpl(alerts=[('success', "Password has been changed")]) + + +@route('/static/', 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 change_password(username, old_pass, new_pass): + print("Changing password for user: %s" % username) + + server = Server(CONF['ldap']['host'], int(CONF['ldap']['port'])) + user_dn = find_user_dn(server, username) + + try: + with Connection(server, authentication=AUTH_SIMPLE, raise_exceptions=True, + user=user_dn, password=old_pass) as c: + c.bind() + c.extend.standard.modify_password(user_dn, old_pass, new_pass) + return True + except (LDAPBindError, LDAPInvalidCredentialsResult): + return False + + +def find_user_dn(server, uid): + with Connection(server) as c: + c.search(CONF['ldap']['base'], "(uid=%s)" % uid, SUBTREE, attributes=['dn']) + return c.response[0]['dn'] if c.response else None + + +BASE_DIR = path.dirname(__file__) + +CONF = ConfigParser() +CONF.read(path.join(BASE_DIR, 'settings.ini')) + +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 test server when invoked directly (in development). +if __name__ == '__main__': + bottle.run(host='0.0.0.0', port=8080) +# Run bottle in application mode (in production under uWSGI server). +else: + application = bottle.default_app() diff --git a/index.tpl b/index.tpl new file mode 100644 index 0000000..506a442 --- /dev/null +++ b/index.tpl @@ -0,0 +1,42 @@ + + + + + + + + {{ page_title }} + + + + + +
+

{{ page_title }}

+ +
+ + + + + + + + + + + + + +
+ +
+ %for type, text in get('alerts', []): +
{{ text }}
+ %end +
+
+ + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..111532d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +bottle>=0.12.8 +ldap3>=0.9 diff --git a/settings.ini b/settings.ini new file mode 100644 index 0000000..8662e4e --- /dev/null +++ b/settings.ini @@ -0,0 +1,7 @@ +[html] +page_title = Change your password on example.org + +[ldap] +host = localhost +port = 389 +base = ou=People,dc=example,dc=org diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..1064a20 --- /dev/null +++ b/static/style.css @@ -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; + } +}