Compare commits
20 commits
Author | SHA1 | Date | |
---|---|---|---|
49ea64c75d | |||
094b515ef1 | |||
a48f4f7434 | |||
ed2b5cc275 | |||
f8bd9411f4 | |||
49b356c855 | |||
9754430012 | |||
6848278380 | |||
103516548e | |||
104da4fadb | |||
e99539b53f | |||
560607330c | |||
9fbb6c21b6 | |||
a61d81cc36 | |||
9f559c7c33 | |||
0db426f137 | |||
27750c4cbd | |||
e123051d74 | |||
3f2b4554d7 | |||
42279bbcfa |
20
.htaccess
20
.htaccess
|
@ -1,7 +1,19 @@
|
||||||
RewriteEngine on
|
# required if using apache2: a2enmod authnz_ldap
|
||||||
RewriteCond %{HTTPS} !=on
|
AuthType Basic
|
||||||
RewriteCond %{ENV:HTTPS} !=on
|
AuthName "Login"
|
||||||
RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L,NE]
|
AuthBasicProvider ldap
|
||||||
|
#AuthLDAPBindAuthoritative off
|
||||||
|
AuthLDAPBindDN UID=bind,OU=Users,DC=ldap,DC=freiesnetz,DC=at
|
||||||
|
AuthLDAPBindPassword ldapbindpassword
|
||||||
|
AuthLDAPURL ldap://localhost/OU=Users,DC=ldap,DC=freiesnetz,DC=at?uid
|
||||||
|
LDAPReferrals off
|
||||||
|
Require valid-user
|
||||||
|
|
||||||
|
# disabled because of client error: ERR_TOO_MANY_REDIRECTS
|
||||||
|
#RewriteEngine on
|
||||||
|
#RewriteCond %{HTTPS} !=on
|
||||||
|
#RewriteCond %{ENV:HTTPS} !=on
|
||||||
|
#RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L,NE]
|
||||||
|
|
||||||
Options +ExecCGI
|
Options +ExecCGI
|
||||||
AddHandler cgi-script .py
|
AddHandler cgi-script .py
|
||||||
|
|
32
README.md
32
README.md
|
@ -1,22 +1,34 @@
|
||||||
# VMailMgr chpw CGI
|
# LDAP chpw CGI
|
||||||
|
|
||||||
This is a Python CGI script that lets virtual `VMailMgr`/`qmail` users change
|
This is a Python CGI script that lets ldap users change
|
||||||
their own mail passwords via a web interface.
|
their own ldap passwords via a web interface.
|
||||||
|
|
||||||
This script is specifically tailored to work on hosts at
|
If users with same UID and same oldpassword are found in other OUs,
|
||||||
[uberspace.de](https://uberspace.de). But it may also work on other qmail-based
|
those passwords are updated too.
|
||||||
systems as long as virtual mail users are managed via `VMailMgr`.
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
To install the script, simply extract all the repository contents into a folder
|
To install the script, simply extract all the repository contents into a folder
|
||||||
under your document root. No paths need to be configured. Only make sure that the
|
under your document root. No paths need to be configured. Only make sure that the
|
||||||
location is reachable via HTTPS.
|
location is reachable via HTTPS. If used with Apache2, this module is required: `a2enmod authnz_ldap`
|
||||||
|
|
||||||
In case you are using a U7 uberspace, you have to add a SELinux permission to allow apache to access both your home directory and the VMailMgr user database:
|
Configure LDAP settings for your LDAP server in index.py:
|
||||||
`chcon -t httpd_sys_content_t ~ ~/passwd.cdb`
|
```
|
||||||
|
ldap_proto = 'ldap://'
|
||||||
|
ldap_server = 'localhost'
|
||||||
|
ldap_basedn = 'dc=ldap,dc=freiesnetz,dc=at'
|
||||||
|
ldap_userdn = 'ou=Users' +','+ ldap_basedn
|
||||||
|
ldap_bind_attr = 'uid'
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure LDAP settings for your LDAP server in .htaccess:
|
||||||
|
```
|
||||||
|
AuthLDAPBindDN UID=bind,OU=Users,DC=ldap,DC=freiesnetz,DC=at
|
||||||
|
AuthLDAPBindPassword ldapbindpassword
|
||||||
|
AuthLDAPURL ldap://localhost/OU=Users,DC=ldap,DC=freiesnetz,DC=at?uid
|
||||||
|
```
|
||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
This is a majorly refined version of a script originally developed by Dirk Boye.
|
This is a majorly for ldap-support rewritten version of a script originally developed by Dirk Boye.
|
||||||
See [dirkboye/mailpw_change](https://github.com/dirkboye/mailpw_change) at GitHub
|
See [dirkboye/mailpw_change](https://github.com/dirkboye/mailpw_change) at GitHub
|
||||||
for the original source code.
|
for the original source code.
|
||||||
|
|
||||||
|
|
82
index.py
82
index.py
|
@ -2,12 +2,16 @@
|
||||||
import cgi, cgitb
|
import cgi, cgitb
|
||||||
import re
|
import re
|
||||||
import sys, os
|
import sys, os
|
||||||
from subprocess import check_output, Popen, PIPE, STDOUT, CalledProcessError
|
import ldap
|
||||||
from os.path import expanduser
|
|
||||||
|
ldap_proto = 'ldap://'
|
||||||
|
ldap_server = 'localhost'
|
||||||
|
ldap_basedn = 'dc=ldap,dc=freiesnetz,dc=at'
|
||||||
|
ldap_userdn = 'ou=Users' +','+ ldap_basedn
|
||||||
|
ldap_bind_attr = 'uid'
|
||||||
|
|
||||||
|
cgitb.enable(display=0, logdir='logs/')
|
||||||
|
|
||||||
cgitb.enable()
|
|
||||||
home_dir = expanduser("~")
|
|
||||||
os.environ['HOME'] = home_dir
|
|
||||||
|
|
||||||
def check_form(formvars, form):
|
def check_form(formvars, form):
|
||||||
for varname in formvars:
|
for varname in formvars:
|
||||||
|
@ -18,6 +22,7 @@ def check_form(formvars, form):
|
||||||
return None
|
return None
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def read_template_file(filename, **vars):
|
def read_template_file(filename, **vars):
|
||||||
with open('tpl/' + filename, mode='r', encoding='utf-8') as f:
|
with open('tpl/' + filename, mode='r', encoding='utf-8') as f:
|
||||||
template = f.read()
|
template = f.read()
|
||||||
|
@ -25,38 +30,30 @@ def read_template_file(filename, **vars):
|
||||||
template = template.replace('{$' + key + '}', vars[key])
|
template = template.replace('{$' + key + '}', vars[key])
|
||||||
return template
|
return template
|
||||||
|
|
||||||
|
|
||||||
def check_oldpw(accountname, oldpass):
|
def check_oldpw(accountname, oldpass):
|
||||||
try:
|
try:
|
||||||
dumpvuserargs = ['dumpvuser', accountname]
|
conn = ldap.initialize(ldap_proto+ldap_server)
|
||||||
userdump = check_output(dumpvuserargs).strip().decode('utf-8')
|
conn.set_option(ldap.OPT_REFERRALS, 0)
|
||||||
m = re.search('Encrypted-Password: (\$([^\$]+)\$([^\$]+)\$([^\$\n]+))', userdump)
|
conn.set_option(ldap.OPT_PROTOCOL_VERSION, 3)
|
||||||
if None == m:
|
if conn.simple_bind(ldap_bind_attr+'='+accountname+','+ldap_userdn, oldpass) == True:
|
||||||
return False
|
|
||||||
oldhash = m.group(1)
|
|
||||||
hashtype = m.group(2)
|
|
||||||
salt = m.group(3)
|
|
||||||
except CalledProcessError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
opensslargs = ['openssl', 'passwd', '-' + hashtype, '-salt', salt, '-stdin']
|
|
||||||
p = Popen(opensslargs, stdin=PIPE, stdout=PIPE, stderr=STDOUT)
|
|
||||||
p.stdin.write(oldpass.encode('utf-8') + b'\n')
|
|
||||||
p.stdin.close()
|
|
||||||
if p.wait() == 0:
|
|
||||||
newhash = p.stdout.readline().strip().decode('utf-8');
|
|
||||||
|
|
||||||
if newhash == oldhash:
|
|
||||||
return True
|
return True
|
||||||
|
except ldap.INVALID_CREDENTIALS:
|
||||||
|
conn.unbind()
|
||||||
|
return False
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def generate_headers():
|
def generate_headers():
|
||||||
return "Content-Type: text/html; charset=utf-8\n"
|
return "Content-Type: text/html; charset=utf-8\n"
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
main_content = ''
|
main_content = ''
|
||||||
|
|
||||||
form = cgi.FieldStorage()
|
form = cgi.FieldStorage()
|
||||||
|
http_host = os.environ.get('HTTP_HOST')
|
||||||
if 'submit' in form.keys():
|
if 'submit' in form.keys():
|
||||||
formvars = ['accountname', 'oldpass', 'newpass', 'newpass2']
|
formvars = ['accountname', 'oldpass', 'newpass', 'newpass2']
|
||||||
form_ok = check_form(formvars, form)
|
form_ok = check_form(formvars, form)
|
||||||
|
@ -68,16 +65,30 @@ def main():
|
||||||
newpass2 = form['newpass2'].value
|
newpass2 = form['newpass2'].value
|
||||||
if newpass == newpass2:
|
if newpass == newpass2:
|
||||||
if check_oldpw(accountname, oldpass):
|
if check_oldpw(accountname, oldpass):
|
||||||
vpasswdargs = ['vpasswd', accountname]
|
conn = ldap.initialize(ldap_proto+ldap_server)
|
||||||
p = Popen(vpasswdargs, stdin=PIPE, stdout=PIPE, stderr=STDOUT)
|
conn.set_option(ldap.OPT_REFERRALS, 0)
|
||||||
p.stdin.write(newpass.encode('utf-8') + b'\n')
|
conn.set_option(ldap.OPT_PROTOCOL_VERSION, 3)
|
||||||
p.stdin.write(newpass2.encode('utf-8') + b'\n')
|
conn.simple_bind(ldap_bind_attr+'='+accountname+','+ldap_userdn, oldpass)
|
||||||
p.stdin.close()
|
results = conn.search_s(ldap_basedn, ldap.SCOPE_SUBTREE, '('+ldap_bind_attr+'='+accountname+')', ['dn'])
|
||||||
if p.wait() == 0:
|
conn.unbind()
|
||||||
|
for dn in results:
|
||||||
|
conn = ldap.initialize(ldap_proto+ldap_server)
|
||||||
|
conn.set_option(ldap.OPT_REFERRALS, 0)
|
||||||
|
conn.set_option(ldap.OPT_PROTOCOL_VERSION, 3)
|
||||||
|
# do a synchronous ldap bind
|
||||||
|
conn.simple_bind_s(dn[0], oldpass)
|
||||||
|
conn.passwd_s(dn[0], oldpass, newpass)
|
||||||
|
conn.unbind_s()
|
||||||
|
conn = ldap.initialize(ldap_proto+ldap_server)
|
||||||
|
conn.set_option(ldap.OPT_REFERRALS, 0)
|
||||||
|
conn.set_option(ldap.OPT_PROTOCOL_VERSION, 3)
|
||||||
|
if conn.simple_bind(ldap_bind_attr+'='+accountname+','+ldap_userdn, newpass) == True:
|
||||||
# We did it
|
# We did it
|
||||||
main_content = read_template_file('success.tpl')
|
conn.unbind()
|
||||||
|
main_content = read_template_file('success.tpl', http_host=http_host)
|
||||||
else:
|
else:
|
||||||
main_content = read_template_file('fail.tpl', message=cgi.escape(p.stdout.read()))
|
conn.unbind()
|
||||||
|
main_content = read_template_file('fail.tpl', message=cgi.escape(ldap.LDAPError))
|
||||||
else:
|
else:
|
||||||
main_content = read_template_file('fail.tpl', message='User not found or wrong password entered.')
|
main_content = read_template_file('fail.tpl', message='User not found or wrong password entered.')
|
||||||
else:
|
else:
|
||||||
|
@ -87,15 +98,16 @@ def main():
|
||||||
else:
|
else:
|
||||||
main_content = read_template_file('fail.tpl', message='Invalid data type supplied.')
|
main_content = read_template_file('fail.tpl', message='Invalid data type supplied.')
|
||||||
else:
|
else:
|
||||||
# Submit button not pressed, show form
|
|
||||||
formaction = cgi.escape("https://" + os.environ["HTTP_HOST"] + os.environ["REQUEST_URI"])
|
formaction = cgi.escape("https://" + os.environ["HTTP_HOST"] + os.environ["REQUEST_URI"])
|
||||||
form = read_template_file('form.tpl', formaction=formaction)
|
#accountname = os.environ.get('REMOTE_USER')
|
||||||
|
accountname = os.environ.get('AUTHENTICATE_UID')
|
||||||
|
form = read_template_file('form.tpl', formaction=formaction, accountname=accountname, http_host=http_host)
|
||||||
main_content = form
|
main_content = form
|
||||||
|
|
||||||
response = generate_headers() + "\n"
|
response = generate_headers() + "\n"
|
||||||
response += read_template_file('main.tpl', main_content=main_content)
|
response += read_template_file('main.tpl', main_content=main_content)
|
||||||
sys.stdout.buffer.write(response.encode('utf-8'))
|
sys.stdout.buffer.write(response.encode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<div class="error">
|
<div class="error">
|
||||||
<h2>Oops, something went wrong…</h2>
|
<h2>Oops, something went wrong</h2>
|
||||||
<p>Message: <span class="message">{$message}</span></p>
|
<p>Message: <span class="message">{$message}</span></p>
|
||||||
<a href="" class="back">Try again</a>
|
<a href="" class="back">Try again</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
11
tpl/form.tpl
11
tpl/form.tpl
|
@ -1,13 +1,14 @@
|
||||||
<form action="{$formaction}" method="post">
|
<form action="{$formaction}" method="post">
|
||||||
<div id="PasswordForm">
|
<div id="PasswordForm">
|
||||||
<p><label for="accountname">Mail Account Name:</label>
|
<p><label for="accountname">Account Name:</label>
|
||||||
<input type="text" name="accountname" id="accountname" placeholder="Your account name, including internal prefix" required></p>
|
<input type="text" name="accountname" id="accountname" placeholder="Your account name, including internal prefix" required value="{$accountname}"></p>
|
||||||
<p><label for="oldpass">Old Mail Password:</label>
|
<p><label for="oldpass">Old Password:</label>
|
||||||
<input type="password" name="oldpass" id="oldpass" required></p>
|
<input type="password" name="oldpass" id="oldpass" required></p>
|
||||||
<p><label for="newpass">New Mail Password:</label>
|
<p><label for="newpass">New Password:</label>
|
||||||
<input type="password" name="newpass" id="newpass" required></p>
|
<input type="password" name="newpass" id="newpass" required></p>
|
||||||
<p><label for="newpass2">Repeat New Mail Password:</label>
|
<p><label for="newpass2">Repeat New Password:</label>
|
||||||
<input type="password" name="newpass2" id="newpass2" required></p>
|
<input type="password" name="newpass2" id="newpass2" required></p>
|
||||||
<p><input type="submit" name="submit" value="Change Password"></p>
|
<p><input type="submit" name="submit" value="Change Password"></p>
|
||||||
|
<p><a href="https://log:out@{$http_host}/">Logout</a> (after logout click cancel and close browser/tab!)
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Change Mailbox Password</title>
|
<title>LDAP Change Password</title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<style>
|
<style>
|
||||||
html {
|
html {
|
||||||
|
@ -49,7 +49,6 @@
|
||||||
a:hover, a:focus {
|
a:hover, a:focus {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#Main {
|
#Main {
|
||||||
margin: 3rem auto;
|
margin: 3rem auto;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
@ -75,7 +74,7 @@
|
||||||
</style>
|
</style>
|
||||||
<body>
|
<body>
|
||||||
<div id="Main">
|
<div id="Main">
|
||||||
<h1>Change Mailbox Password</h1>
|
<h1>Change LDAP Password</h1>
|
||||||
{$main_content}
|
{$main_content}
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
<p class="success">Your password was changed successfully.</p>
|
<p class="success">Your password was changed successfully.</p>
|
||||||
|
<p><a href="https://log:out@{$http_host}/">Logout</a> (after logout click cancel and close browser/tab!)
|
||||||
|
|
Loading…
Reference in a new issue