init
This commit is contained in:
		
							
								
								
									
										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> | ||||
		Reference in New Issue
	
	Block a user