Repository: jirutka/ldap-passwd-webui
Branch: master
Commit: 72fc5b06eda1
Files: 10
Total size: 14.6 KB
Directory structure:
gitextract_s9vf694b/
├── .editorconfig
├── .gitignore
├── CHANGELOG.adoc
├── LICENSE
├── README.adoc
├── app.py
├── index.tpl
├── requirements.txt
├── settings.ini.example
└── static/
└── style.css
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
[*]
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
================================================
FILE: .gitignore
================================================
/settings.ini
================================================
FILE: CHANGELOG.adoc
================================================
= Changelog
:repo-uri: https://github.com/jirutka/ldap-passwd-webui
:issues: {repo-uri}/issues
:pulls: {repo-uri}/pull
:tags: {repo-uri}/releases/tag
== link:{tags}/v2.1.0[2.1.0] (2019-01-09)
* Add support for changing password in multiple LDAPs at once.
== link:{tags}/v2.0.0[2.0.0] (2017-07-14)
* Update for ldap3 2.x.
* Rename project to ldap-passwd-webui.
* Use `logging` module for logging instead of `print()`.
* Allow to enable bottle debug mode by setting environment variable `DEBUG`.
== link:{tags}/v1.0.0[1.0.0] (2017-06-06)
* First stable release.
================================================
FILE: LICENSE
================================================
The MIT License
Copyright 2015-2017 Jakub Jirutka <jakub@jirutka.cz>.
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.
================================================
FILE: README.adoc
================================================
= Web UI for changing LDAP password
Jakub Jirutka <https://github.com/jirutka[@jirutka]>
//custom
:proj-name: ldap-passwd-webui
:gh-name: jirutka/{proj-name}
:wikip-url: https://en.wikipedia.org/wiki
:pypi-url: https://pypi.python.org/pypi
The aim of this project is to provide a very simple web form for users to be able to change their password stored in LDAP or Active Directory (Samba 4 AD).
It’s built with http://bottlepy.org[Bottle], a WSGI micro web-framework for Python.
== Installation
=== Alpine Linux
. Install package *ldap-passwd-webui-waitress* from the Alpine’s community repository:
+
[source, sh]
apk add ldap-passwd-webui-waitress
+
IMPORTANT: This package is in Alpine stable since v3.7. You can also install it from _edge_ (unstable) branch.
. Adjust configuration in `/etc/ldap-passwd-webui.ini` and `/etc/conf.d/`.
. Start service ldap-passwd-webui:
+
[source]
/etc/init.d/ldap-passwd-webui start
=== Manually
Clone this repository and install dependencies:
[source, sh, subs="+attributes"]
----
git clone git@github.com:{gh-name}.git
cd {proj-name}
pip install -r requirements.txt
----
Read the next sections to learn how to run it.
=== Requirements
* Python 3.x
* {pypi-url}/bottle/[bottle]
* {pypi-url}/ldap3[ldap3] 2.x
== Configuration
Configuration is read from the file link:settings.ini.example[settings.ini].
You may change location of the settings file using the environment variable `CONF_FILE`.
If you have Active Directory (or Samba 4 AD), then you *must* use encrypted connection (i.e. LDAPS or StartTLS) – AD doesn’t allow changing password via unencrypted connection.
== Run it
There are multiple ways how to run it:
* with the built-in default WSGI server based on https://docs.python.org/3/library/wsgiref.html#module-wsgiref.simple_server[wsgiref],
* under a {wikip-url}/Web_Server_Gateway_Interface[WSGI] server like https://uwsgi-docs.readthedocs.org[uWSGI], https://docs.pylonsproject.org/projects/waitress[Waitress], http://gunicorn.org[Gunicorn], … (recommended)
* as a {wikip-url}/Common_Gateway_Interface[CGI] script.
=== Run with the built-in server
Simply execute the `app.py`:
[source, python]
python3 app.py
Then you can access the app on http://localhost:8080.
The port and host may be changed in link:settings.ini.example[settings.ini].
=== Run with Waitress
[source, sh, subs="+attributes"]
----
cd {proj-name}
waitress-serve --listen=*:8080 app:application
----
=== Run with uWSGI and nginx
If you have many micro-apps like this, it’s IMO kinda overkill to run each in a separate uWSGI process, isn’t it?
It’s not so well known, but uWSGI allows to “mount” multiple application in a single uWSGI process and with a single socket.
[source, ini, subs="+attributes"]
.Sample uWSGI configuration:
----
[uwsgi]
plugins = python3
socket = /run/uwsgi/main.sock
chdir = /var/www/scripts
logger = file:/var/log/uwsgi/main.log
processes = 1
threads = 2
# map URI paths to applications
mount = /admin/{proj-name}={proj-name}/app.py
#mount = /admin/change-world=change-world/app.py
manage-script-name = true
----
[source, nginx]
.Sample nginx configuration as a reverse proxy in front of uWSGI:
----
server {
listen 443 ssl;
server_name example.org;
ssl_certificate /etc/ssl/nginx/nginx.crt;
ssl_certificate_key /etc/ssl/nginx/nginx.key;
# uWSGI scripts
location /admin/ {
uwsgi_pass unix:/run/uwsgi/main.sock;
include uwsgi_params;
}
}
----
== Screenshot
image::doc/screenshot.png[]
== License
This project is licensed under http://opensource.org/licenses/MIT/[MIT License].
For the full text of the license, see the link:LICENSE[LICENSE] file.
================================================
FILE: app.py
================================================
#!/usr/bin/env python3
import bottle
from bottle import get, post, static_file, request, route, template
from bottle import SimpleTemplate
from configparser import ConfigParser
from ldap3 import Connection, Server
from ldap3 import SIMPLE, SUBTREE
from ldap3.core.exceptions import LDAPBindError, LDAPConstraintViolationResult, \
LDAPInvalidCredentialsResult, LDAPUserNameIsMandatoryError, \
LDAPSocketOpenError, LDAPExceptionError
import logging
import os
from os import environ, path
BASE_DIR = path.dirname(__file__)
LOG = logging.getLogger(__name__)
LOG_FORMAT = '%(asctime)s %(levelname)s: %(message)s'
VERSION = '2.1.0'
@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:
change_passwords(form('username'), form('old-password'), form('new-password'))
except Error as e:
LOG.warning("Unsuccessful attempt to change password for %s: %s" % (form('username'), e))
return error(str(e))
LOG.info("Password successfully changed for: %s" % 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 connect_ldap(conf, **kwargs):
server = Server(host=conf['host'],
port=conf.getint('port', None),
use_ssl=conf.getboolean('use_ssl', False),
connect_timeout=5)
return Connection(server, raise_exceptions=True, **kwargs)
def change_passwords(username, old_pass, new_pass):
changed = []
for key in (key for key in CONF.sections()
if key == 'ldap' or key.startswith('ldap:')):
LOG.debug("Changing password in %s for %s" % (key, username))
try:
change_password(CONF[key], username, old_pass, new_pass)
changed.append(key)
except Error as e:
for key in reversed(changed):
LOG.info("Reverting password change in %s for %s" % (key, username))
try:
change_password(CONF[key], username, new_pass, old_pass)
except Error as e2:
LOG.error('{}: {!s}'.format(e.__class__.__name__, e2))
raise e
def change_password(conf, *args):
try:
if conf.get('type') == 'ad':
change_password_ad(conf, *args)
else:
change_password_ldap(conf, *args)
except (LDAPBindError, LDAPInvalidCredentialsResult, LDAPUserNameIsMandatoryError):
raise Error('Username or password is incorrect!')
except LDAPConstraintViolationResult as e:
# Extract useful part of the error message (for Samba 4 / AD).
msg = e.message.split('check_password_restrictions: ')[-1].capitalize()
raise Error(msg)
except LDAPSocketOpenError as e:
LOG.error('{}: {!s}'.format(e.__class__.__name__, e))
raise Error('Unable to connect to the remote server.')
except LDAPExceptionError as e:
LOG.error('{}: {!s}'.format(e.__class__.__name__, e))
raise Error('Encountered an unexpected error while communicating with the remote server.')
def change_password_ldap(conf, username, old_pass, new_pass):
with connect_ldap(conf) as c:
user_dn = find_user_dn(conf, c, username)
# Note: raises LDAPUserNameIsMandatoryError when user_dn is None.
with connect_ldap(conf, authentication=SIMPLE, user=user_dn, password=old_pass) as c:
c.bind()
c.extend.standard.modify_password(user_dn, old_pass, new_pass)
def change_password_ad(conf, username, old_pass, new_pass):
user = username + '@' + conf['ad_domain']
with connect_ldap(conf, authentication=SIMPLE, user=user, password=old_pass) as c:
c.bind()
user_dn = find_user_dn(conf, c, username)
c.extend.microsoft.modify_password(user_dn, new_pass, old_pass)
def find_user_dn(conf, conn, uid):
search_filter = conf['search_filter'].replace('{uid}', uid)
conn.search(conf['base'], "(%s)" % search_filter, SUBTREE)
return conn.response[0]['dn'] if conn.response else None
def read_config():
config = ConfigParser()
config.read([path.join(BASE_DIR, 'settings.ini'), os.getenv('CONF_FILE', '')])
return config
class Error(Exception):
pass
if environ.get('DEBUG'):
bottle.debug(True)
# Set up logging.
logging.basicConfig(format=LOG_FORMAT)
LOG.setLevel(logging.INFO)
LOG.info("Starting ldap-passwd-webui %s" % VERSION)
CONF = read_config()
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()
================================================
FILE: index.tpl
================================================
<!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,}" oninvalid="SetCustomValidity('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,}" oninvalid="SetCustomValidity('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>
================================================
FILE: requirements.txt
================================================
bottle >= 0.12.8
ldap3 >= 2.0, < 3.0
configparser; python_version < '3.3'
================================================
FILE: settings.ini.example
================================================
[html]
page_title = Change your password on example.org
[ldap:0]
host = localhost
port = 636
use_ssl = true
base = ou=People,dc=example,dc=org
search_filter = uid={uid}
# Uncomment for AD / Samba 4
#type = ad
#ad_domain = ad.example.org
#search_filter = sAMAccountName={uid}
# You may specify multiple LDAPs, the password will be changed in all.
# If one fails, the previous password changes are reverted.
#[ldap:1]
#host = localhost
#base = ou=People,dc=example,dc=org
#search_filter = uid={uid}
[server]
server = auto
host = localhost
port = 8080
================================================
FILE: static/style.css
================================================
/* 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;
}
}
gitextract_s9vf694b/
├── .editorconfig
├── .gitignore
├── CHANGELOG.adoc
├── LICENSE
├── README.adoc
├── app.py
├── index.tpl
├── requirements.txt
├── settings.ini.example
└── static/
└── style.css
SYMBOL INDEX (12 symbols across 1 files) FILE: app.py function get_index (line 24) | def get_index(): function post_index (line 29) | def post_index(): function serve_static (line 53) | def serve_static(filename): function index_tpl (line 57) | def index_tpl(**kwargs): function connect_ldap (line 61) | def connect_ldap(conf, **kwargs): function change_passwords (line 70) | def change_passwords(username, old_pass, new_pass): function change_password (line 90) | def change_password(conf, *args): function change_password_ldap (line 114) | def change_password_ldap(conf, username, old_pass, new_pass): function change_password_ad (line 124) | def change_password_ad(conf, username, old_pass, new_pass): function find_user_dn (line 133) | def find_user_dn(conf, conn, uid): function read_config (line 140) | def read_config(): class Error (line 147) | class Error(Exception):
Condensed preview — 10 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (16K chars).
[
{
"path": ".editorconfig",
"chars": 165,
"preview": "[*]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\ntrim_trailing_whitespace = true\nindent_style = space\nin"
},
{
"path": ".gitignore",
"chars": 14,
"preview": "/settings.ini\n"
},
{
"path": "CHANGELOG.adoc",
"chars": 569,
"preview": "= Changelog\n:repo-uri: https://github.com/jirutka/ldap-passwd-webui\n:issues: {repo-uri}/issues\n:pulls: {repo-uri}/pull\n:"
},
{
"path": "LICENSE",
"chars": 1095,
"preview": "The MIT License\n\nCopyright 2015-2017 Jakub Jirutka <jakub@jirutka.cz>.\n\nPermission is hereby granted, free of charge, to"
},
{
"path": "README.adoc",
"chars": 3682,
"preview": "= Web UI for changing LDAP password\nJakub Jirutka <https://github.com/jirutka[@jirutka]>\n//custom\n:proj-name: ldap-passw"
},
{
"path": "app.py",
"chars": 5428,
"preview": "#!/usr/bin/env python3\n\nimport bottle\nfrom bottle import get, post, static_file, request, route, template\nfrom bottle im"
},
{
"path": "index.tpl",
"chars": 1501,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"utf-8\">\n <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n "
},
{
"path": "requirements.txt",
"chars": 74,
"preview": "bottle >= 0.12.8\nldap3 >= 2.0, < 3.0\nconfigparser; python_version < '3.3'\n"
},
{
"path": "settings.ini.example",
"chars": 553,
"preview": "[html]\npage_title = Change your password on example.org\n\n[ldap:0]\nhost = localhost\nport = 636\nuse_ssl = true\nbase = ou=P"
},
{
"path": "static/style.css",
"chars": 1868,
"preview": "/* TODO make it cooler! */\n\nbody {\n font-family: sans-serif;\n color: #333;\n}\n\nmain {\n margin: 0 auto;\n}\n\nh1 {\n font-"
}
]
About this extraction
This page contains the full source code of the jirutka/ldap-passwd-webui GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 10 files (14.6 KB), approximately 4.2k tokens, and a symbol index with 12 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.