init
This commit is contained in:
commit
35abb85365
3
README.md
Normal file
3
README.md
Normal 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
5
luser/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from flask import Flask
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
from luser import routes
|
203
luser/models.py
Normal file
203
luser/models.py
Normal 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
129
luser/routes.py
Normal 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'
|
35
luser/templates/changepassword.html
Normal file
35
luser/templates/changepassword.html
Normal 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>
|
39
luser/templates/register.html
Normal file
39
luser/templates/register.html
Normal 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>
|
33
luser/templates/unregister.html
Normal file
33
luser/templates/unregister.html
Normal 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>
|
Loading…
Reference in New Issue
Block a user