This commit is contained in:
fram3d 2023-04-03 11:16:15 +02:00
commit 35abb85365
Signed by: fram3d
GPG Key ID: 938920E709EEA32A
8 changed files with 451 additions and 0 deletions

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# Flask LDAP user management
Web app for adding,delating and modifying users using LDAP

5
luser/__init__.py Normal file
View File

@ -0,0 +1,5 @@
from flask import Flask
app = Flask(__name__)
from luser import routes

203
luser/models.py Normal file
View File

@ -0,0 +1,203 @@
from ldap3 import Server,Connection,ALL,MODIFY_REPLACE
from datetime import datetime
class LUSER():
'''
Class that represents secure connection to LDAP server
LDAPhost := string IP or hostname of LDAP server
admin_user := string DN of LDAP admin user
admin_pass := string password of LDAP admin user
base := string base in LDAP system where users are made
basealt := string base in LDAP system where users are made with password hashes generated for openalt
'''
def __init__(self, ldap_host, admin_user, admin_pass, base, basealt=''):
self.ldap_host = ldap_host
self.admin_user = admin_user
self.admin_pass = admin_pass
self.base = base
self.basealt = basealt
self.alt = True
ldapserver = Server(ldap_host, use_ssl=True)
self.ldapconnection = Connection(ldapserver, admin_user, admin_pass, auto_bind=True)
# uid and gid of most recently registered users
self.lastuid = 1337
self.lastgid = 1337
# Set alt boolean to false if basealt not set
if basealt == '':
self.alt = False
def prepareluser(self):
'''
Create base on LDAP host
'''
# Split base string with commas to find values of organization and dc
baselist = self.base.split(",")
basealtlist = self.basealt.split(",")
# Find ou in base and set it as organization variable
for i in baselist:
if i.split('=')[0] == 'ou':
organization = i.split('=')[1]
break
for i in basealtlist:
if i.split('=')[0] == 'ou':
organizationalt = i.split('=')[1]
break
# Find first dc and set it as dc variable
for i in baselist:
if i.split('=')[0] == 'dc':
dc = i.split('=')[1]
break
for i in basealtlist:
if i.split('=')[0] == 'dc':
dcalt = i.split('=')[1]
break
# Find full dc and set it as dcfull variable
for i in baselist:
if i.split('=')[0] == 'dc':
dcfull += ',dc=' + i.split('=')[1]
for i in basealtlist:
if i.split('=')[0] == 'dc':
dcfullalt += ',dc=' + i.split('=')[1]
# Remove first column character
dcfull = dcfull[1:]
dcfullalt = dcfull[1:]
# Create organization on LDAP server and store boolean indicating it's success
rcode1 = self.ldapconnection.add(f'dc={dcfull}', ['dcObject', 'organization'], {'o' : dc, 'dc' : dc})
if self.alt:
rcode2 = self.ldapconnection.add(f'dc={dcfullalt}', ['dcObject', 'organization'], {'o' : dcalt, 'dc' : dcalt})
else:
rcode2 = True
# Create organizational units on LDAP server and store boolean indicating it's success
rcode3 = self.ldapconnection.add(self.base, ['top', 'organizationalUnit'], {'ou' : organization})
if self.alt :
rcode4 = self.ldapconnection.add(self.basealt, ['top', 'organizationalUnit'], {'ou' : organizationalt})
else:
rcode4 = True
# Return True only if all return values are true
return rcode1 and rcode2 and rcode3 and rcode4
def lastpwchangenow(self):
'''
Return time of last password change for the user set to current time
messured in days from UNIX epoch
'''
return str((datetime.utcnow() - datetime(1970,1,1)).days)
def add(self, user, password, althash=""):
'''
Add a user to base in LDAP with user and pass as credentials
user := string containing username
password := string containing user password
althash := string containing user password/hash for the alternative base
'''
# Increase UID and GID counters
self.lastuid += 1
self.lastgid += 1
# Add user to base
id = f"uid={user}"
# Object classes of a user entry
objectClass = ['top', 'person', 'organizationalPerson', 'inetOrgPerson', 'posixAccount', 'shadowAccount']
# Attributes for a user entry
attributes = {'cn' : user, 'sn' : user, 'givenName' : user, 'uid' : user, 'uidNumber' : self.lastuid, 'gidNumber' : self.lastgid, 'homeDirectory' : f'/home/{user}', 'loginShell' : '/usr/bin/git-shell', 'gecos' : 'SystemUser', 'shadowLastChange' : self.lastpwchangenow(), 'shadowMax' : '45', 'userPassword' : password}
attributesalt = {'cn' : user, 'sn' : user, 'givenName' : user, 'uid' : user, 'uidNumber' : self.lastuid, 'gidNumber' : self.lastgid, 'homeDirectory' : f'/home/{user}', 'loginShell' : '/usr//bin/git-shell', 'gecos' : 'SystemUser', 'shadowLastChange' : self.lastpwchangenow(), 'shadowMax' : '45', 'userPassword' : althash}
# Return boolean value of new user entry
rcode1 = self.ldapconnection.add(f'{id},{self.base}', objectClass, attributes)
# If alternative base is set add the same user to the alternative base as well, if not act as if it was success
if self.alt:
rcode2 = self.ldapconnection.add(f'{id},{self.basealt}', objectClass, attributesalt)
else:
rcode2 = True
# Return True only if both entries was successful
return rcode1 and rcode2
def changepassword(self, user, newpass, althash=''):
'''
Change password of user to newpass
user := string containing username
newpass := string containing new password
althash := string containing password/hash for alternative base
'''
# This variable holds boolean indicating successful change of user password
chpassbool = self.ldapconnection.modify(f'uid={user},{self.base}', {'userPassword': (MODIFY_REPLACE,[newpass])})
# If alternative base is set modify user password/hash with althash, if not, pretend change was successful
if self.alt:
chpassboolalt = self.ldapconnection.modify(f'uid={user},{self.basealt}', {'userPassword': (MODIFY_REPLACE,[althash])})
else:
chpassboolalt = True
# This variable holds boolean indicating successful change of shadowLastChange value to current time
chlastchangebool = self.ldapconnection.modify(f'uid={user},{self.base}', {'shadowLastChange' : (MODIFY_REPLACE,[self.lastpwchangenow()])})
# If alternative base is set modify user password/hash with althash, if not, pretend change was successful
if self.alt:
chlastchangeboolalt = self.ldapconnection.modify(f'uid={user},{self.base}', {'shadowLastChange' : (MODIFY_REPLACE, [self.lastpwchangenow()])})
else:
chlastchangeboolalt = True
# Return True only if changing of both password and time of last password change was successful
return chpassbool and chpassboolalt and chlastchangebool and chlastchangeboolalt
def delete(self, user):
'''
Delete user given username of user
user := string containing username
'''
rcode1 = self.ldapconnection.delete(f'uid={user},{self.base}')
# If alternative base is set delete user from alternative base, if not, pretend deletion was successful
if self.alt:
rcode2 = self.ldapconnection.delete(f'uid={user},{self.basealt}')
else:
rcode2 = True
# Return True only if deletion from both bases was successful
return rcode1 and rcode2
def getpassword(self, user):
'''
Retrive password of a user
user := string containing username
'''
# Search LDAP entries that have object class inetOrgPerson and uid attribute equal to given user field
self.ldapconnection.search(search_base=self.base,search_filter=f'(&(objectClass=inetOrgPerson)(uid={user}))', attributes=['userPassword'])
# Return userPassword attribute from the response
return self.ldapconnection.response[0]['attributes']['userPassword'][0].decode('utf-8')

129
luser/routes.py Normal file
View File

@ -0,0 +1,129 @@
from flask import render_template, request, redirect
from luser import app
from luser.models import LUSER
from passlib.hash import ldap_salted_sha1,sha512_crypt
import subprocess
import random
import base64
import os
LDAPHOST = 'ldap.example.org'
LDAPADMINNAME = 'cn=admin,dc=example,dc=org'
LDAPPASS = 'verysecr3t'
USERBASE = 'ou=Users,dc=example,dc=org'
ALTUSERBASE = '' # Optional
CAPTCHA_PATH = '/var/luser/luser/static/account/register/captcha_img/'
@app.route('/account/changepassword/', methods=['POST', 'GET'])
def changepassword():
if request.method == 'GET':
return render_template('changepassword.html')
elif request.method == 'POST':
username = request.form['username']
oldpassword = request.form['oldpassword']
newpassword = request.form['newpassword']
# Check lenght of password
if len(newpassword) < 8:
return 'Error: password is too short'
# Create a LUSER connection
luser = LUSER(LDAPHOST,LDAPADMINNAME,LDAPPASS,USERBASE,ALTUSERBASE)
if ldap_salted_sha1.verify(oldpassword, luser.getpassword(username)) == False:
return 'Wrong username/password combination'
ldaphash = ldap_salted_sha1.hash(newpassword)
althash = sha512_crypt.hash(newpassword)
# Try to change user password
try:
if luser.changepassword(username, ldaphash, althash) == True:
return 'User password successfuly changed'
else:
return 'User password change failed'
except:
return 'User password change failed, exception raised'
else:
return 'HTTP request method not recogniezed'
@app.route('/account/unregister/', methods=['POST', 'GET'])
def unregister():
if request.method == 'GET':
return render_template('unregister.html')
elif request.method == 'POST':
username = request.form['username']
password = request.form['password']
# Create a LUSER connection
luser = LUSER(LDAPHOST,LDAPADMINNAME,LDAPPASS,USERBASE,ALTUSERBASE)
if ldap_salted_sha1.verify(password, luser.getpassword(username)) == False:
return 'Wrong username/password combination'
# Try to delete user
try:
if luser.delete(username) == True:
return 'User successfuly unregistered'
else:
return 'User unregistration failed'
except:
return 'User unregistration failed, exception raised'
else:
return 'HTTP request method not recogniezed'
@app.route('/account/register/', methods=['POST', 'GET'])
def register():
if request.method == 'GET':
captcha_solution = str(random.randint(0,999999))
captcha_hash = ldap_salted_sha1.hash(captcha_solution)
captcha_filename = base64.b64encode(captcha_hash.encode('utf-8')).decode('utf-8')[8:-1] + ".png"
captcha_path = CAPTCHA_PATH + captcha_filename
captcha_file = open(captcha_path, 'w')
subprocess.run(["/usr/local/bin/captcha.sh",captcha_solution],stdout=captcha_file)
return render_template('register.html',imgsrc=captcha_filename,captchahash=captcha_hash)
elif request.method == 'POST':
username = request.form['username']
password = request.form['password']
captcha_answer = request.form['captchaa']
captcha_filename = request.form['captchaq']
captcha_path = CAPTCHA_PATH + captcha_filename
if captcha_filename[:-4].isalnum() == False and len(captcha_filename) != 47 :
return 'Error: Captcha question is manipulated'
captcha_valid = True
if os.path.exists(captcha_path) == False:
captcha_valid = False
else:
os.remove(captcha_path)
captcha_hash = base64.b64decode(base64.b64encode(b"{SSHA}").decode('utf-8') + captcha_filename[:-4] + "=").decode('utf-8')
if ldap_salted_sha1.verify(captcha_answer, captcha_hash) == False:
captcha_valid = False
if captcha_valid == False:
return 'Error: Captcha is wrong!'
# Check lenght of password
if len(password) < 8:
return 'Error: password is too short'
# Create a LUSER connection
luser = LUSER(LDAPHOST,LDAPADMINNAME,LDAPPASS,USERBASE,ALTUSERBASE)
# Try to add user
try:
ldaphash = ldap_salted_sha1.hash(password)
althash = sha512_crypt.hash(password)
#smtpctlout=subprocess.run(["smtpctl","encrypt", password],text=True,stdout=subprocess.PIPE)
#smtpdhash=smtpctlout.stdout[:-1]
if luser.add(username,ldaphash,althash):
return 'User successfuly registered'
else:
return 'User registration failed, username probably taken'
except:
return 'User registration failed, exception raised'
else:
return 'HTTP request method not recogniezed'

View File

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="sr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>User password change in LDAP system</title>
<style>
.container {
max-width:900px;
margin: 0 auto;
font-family: Arial, Helvetica, sans-serif;
color: #3d3d3d;
}
body {
background-color: #e6e6e6;
}
</style>
</head>
<body>
<main class="container">
<h1 style="text-align:center;">Change password</h1>
<form action="/account/changepassword/" method="POST" style="text-align:center;">
<label for="username">username</label>
<input type="text" name="username" id="username" placeholder="username" required>
<label for="oldpassword">oldpassword</label>
<input type="password" name="oldpassword" id="oldpassword" placeholder="oldpassword" required>
<label for="newpassword">newpassword</label>
<input type="password" name="newpassword" id="newpassword" placeholder="newpassword" required>
<br>
<button>Change Password</button>
</form>
</main>
</body>
</html>

View File

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="sr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>User registration to LDAP system</title>
<style>
.container {
max-width:900px;
margin: 0 auto;
font-family: Arial, Helvetica, sans-serif;
color: #3d3d3d;
}
body {
background-color: #e6e6e6;
}
</style>
</head>
<body>
<main class="container">
<h1 style="text-align:center;">Register</h1>
<form action="/account/register/" method="POST" style="text-align:center;">
<label for="username">username</label>
<input type="text" name="username" id="username" placeholder="username" required>
<label for="password">password</label>
<input type="password" name="password" id="password" placeholder="password" required>
<p>password must be at least 8 characters long<p>
<br>
<img src=/account/register/captcha_img/{{ imgsrc }} alt="{{ captchahash }}" style="width:200">
<p>
<label for="captcha answer">captcha answer</label>
<input type="text" name="captchaa" id="captchaa" placeholder="captcha answer" required>
<input type="hidden" name="captchaq" id="captchaq" value="{{ imgsrc }}">
<button>Register</button>
</form>
</main>
</body>
</html>

View File

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="sr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>User unregistration to LDAP system</title>
<style>
.container {
max-width:900px;
margin: 0 auto;
font-family: Arial, Helvetica, sans-serif;
color: #3d3d3d;
}
body {
background-color: #e6e6e6;
}
</style>
</head>
<body>
<main class="container">
<h1 style="text-align:center;">Unregister</h1>
<form action="/account/unregister/" method="POST" style="text-align:center;">
<label for="username">username</label>
<input type="text" name="username" id="username" placeholder="username" required>
<label for="password">password</label>
<input type="password" name="password" id="password" placeholder="password" required>
<br>
<button>Unregister</button>
</form>
</main>
</body>
</html>

4
run.py Executable file
View File

@ -0,0 +1,4 @@
from luser import app
if __name__ == '__main__':
app.run(debug=False)