Compare commits

..
2 Commits
Author SHA1 Message Date
andonome a0670e7b4d add passlib requirement 2024-04-10 02:30:14 +02:00
andonome e5c8a8c6f2 add gitignore 2024-04-10 02:29:45 +02:00
17 changed files with 145 additions and 651 deletions

No files matched your search

+1 -4
View File
@@ -1,4 +1 @@
__pycache__ luser/__pycache__/
luser/__pycache__
luser.deb
build-deb/luser/var/
+2 -71
View File
@@ -1,72 +1,3 @@
# LUSER # Flask LDAP user management
A lightweight user management system for LDAP, implemented using Flask. This application provides a web interface for users to register, change passwords, and unregister themselves from an LDAP directory. Web app that allows users to add,remove and change passwords in LDAP system
## Features
- **User Registration**: New users can create an account with a captcha to prevent automated registrations.
- **Password Management**: Users can change their LDAP passwords securely.
- **User Unregistration**: Users can remove themselves from the LDAP system.
- **Captcha Protection**: Uses a custom captcha mechanism to mitigate bot registrations.
- **LDAP Integration**: Directly interacts with LDAP for all credential and user management tasks.
- **Rate limiting** (new): Setting rate limiting for user registration.
## Prerequisites
- Python 3.x
- LDAP Server
- `passlib` for secure password hashing
- `Flask` for the web framework
## Installation
1. **Clone the repository**:
```bash
git clone <repository-url>
cd luser
```
2. **Install dependencies**:
```bash
pip install -r requirements.txt
```
3. **Configure the application**:
Edit the `config.ini` file to provide your LDAP server details and paths:
- `LDAPHOST`: Address of your LDAP server.
- `LDAPADMINNAME`: Admin username for LDAP operations.
- `LDAPPASS`: Admin password for LDAP operations.
- `USERBASE`: The base DN for users.
- `CAPTCHA_PATH`: Path where captcha images are stored.
- `PERIOD`: Time period after which the rate limiting counter resets
- `LIMIT`: Number of allowed user registrations in the given time PERIOD
4. **Setup Captcha script**:
Ensure the `captcha.sh` script is available in `/usr/local/bin/` and is executable.
## Running the Application
You can run the application using the provided `run.py` script:
```bash
python run.py
```
The application will be available at `http://localhost:5000` (or the port specified in your configuration).
## Project Structure
- `luser/`: Main application package.
- `routes.py`: Defines the web endpoints and business logic.
- `models.py`: Handles LDAP interactions.
- `templates/`: HTML templates for the web interface.
- `static/`: Static files (CSS, etc.).
- `build-deb/`: Contains instructions and files for building a Debian package.
- `run.py`: Entry point for the Flask application.
- `config.ini`: Configuration file for LDAP and system settings.
## Security Considerations
- This application uses `ldap_salted_sha1` for password hashing.
- Ensure the LDAP connection is secured (e.g., via LDAPS) in production environments.
- The captcha mechanism is implemented to prevent automated registration attempts.
+2 -5
View File
@@ -1,20 +1,17 @@
all: man deb all: man deb
man: man/luser.1.md man: man/luser.1.md
mkdir -p luser/usr/share/man/man1/
pandoc man/luser.1.md -f markdown+hard_line_breaks -s -t man -o man/luser.1 pandoc man/luser.1.md -f markdown+hard_line_breaks -s -t man -o man/luser.1
cp man/luser.1 luser/usr/share/man/man1/ cp man/luser.1 luser/usr/share/man/man1/
gzip -f luser/usr/share/man/man1/luser.1 gzip -f luser/usr/share/man/man1/luser.1
deb: man ../requirments.txt ../run.py ../luser ../LICENSE deb: man ../requirments.txt ../run.py ../luser ../LICENSE
mkdir -p luser/var/luser/luser
cp -r ../luser/* luser/var/luser/luser/ cp -r ../luser/* luser/var/luser/luser/
cp ../run.py luser/var/luser/ cp ../run.py luser/var/luser/
cp ../LICENSE luser/var/luser/ cp ../LICENSE luser/var/luser/
chmod -w luser/DEBIAN/*
chmod +w luser/DEBIAN/control
dpkg-deb --build luser dpkg-deb --build luser
chmod +w luser/DEBIAN/*
clean: clean:
rm -f luser.deb rm -f luser.deb
rm -f man/luser.1 rm -f man/luser.1
rm -rf luser/var rm -rf luser/var
mkdir -p luser/var/luser/luser
mkdir -p luser/usr/share/man/man1/
+2 -2
View File
@@ -4,8 +4,8 @@ Priority: optional
Architecture: all Architecture: all
Essential: no Essential: no
Installed-Size: 2000 Installed-Size: 2000
Depends: python3-flask, python3-ldap3, gunicorn, imagemagick, python3-passlib Depends: python3-flask, python3-ldap3
Homepage: https://gitea.dmz.rs/fram3d/luser Homepage: https://gitea.dmz.rs/fram3d/luser
Maintainer: fram3d <fram3d@dmz.rs> Maintainer: fram3d <fram3d@dmz.rs>
Description: Web app that allows users to add,remove and change passwords in LDAP system Description: Web app that allows users to add,remove and change passwords in LDAP system
Version: 1.5.0 Version: 1.0.0
-7
View File
@@ -1,7 +0,0 @@
#!/bin/bash
/usr/bin/systemctl enable luser.service
/sbin/service luser start
if [ -f /tmp/oldluserconfig.ini ] ; then
cp /tmp/oldluserconfig.ini /var/luser/luser/config.ini
rm /tmp/oldluserconfig.ini
fi
-4
View File
@@ -1,4 +0,0 @@
#!/bin/bash
if [ -f /var/luser/luser/config.ini ] ; then
cp /var/luser/luser/config.ini /tmp/oldluserconfig.ini
fi
-6
View File
@@ -1,6 +0,0 @@
#!/bin/bash
/sbin/service luser stop
/usr/bin/systemctl disable luser.service
if [ -f /var/luser/luser/config.ini ] ; then
cp /var/luser/luser/config.ini /tmp/oldluserconfig.ini
fi
@@ -5,7 +5,7 @@ After=network.target nss-lookup.target
[Service] [Service]
WorkingDirectory=/var/luser/ WorkingDirectory=/var/luser/
ExecStart=/usr/bin/gunicorn --workers 3 --bind 0.0.0.0:80 run:app ExecStart=/usr/bin/gunicorn --workers 3 --bind 127.0.0.1:5000 run:app
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
-100
View File
@@ -1,100 +0,0 @@
#!/bin/sh
# This script is an example captcha script.
# It takes the text to recognize in the captcha image as a parameter.
# It return the image binary as a result. ejabberd support PNG, JPEG and GIF.
# The whole idea of the captcha script is to let server admins adapt it to
# their own needs. The goal is to be able to make the captcha generation as
# unique as possible, to make the captcha challenge difficult to bypass by
# a bot.
# Server admins are thus supposed to write and use their own captcha generators.
# This script relies on ImageMagick.
# It is NOT compliant with ImageMagick forks like GraphicsMagick.
INPUT=$1
if test -n ${BASH_VERSION:-''} ; then
get_random ()
{
R=$RANDOM
}
else
for n in `od -A n -t u2 -N 48 /dev/urandom`; do RL="$RL$n "; done
get_random ()
{
R=${RL%% *}
RL=${RL#* }
}
fi
get_random
WAVE1_AMPLITUDE=$((2 + $R % 5))
get_random
WAVE1_LENGTH=$((50 + $R % 25))
get_random
WAVE2_AMPLITUDE=$((2 + $R % 5))
get_random
WAVE2_LENGTH=$((50 + $R % 25))
get_random
WAVE3_AMPLITUDE=$((2 + $R % 5))
get_random
WAVE3_LENGTH=$((50 + $R % 25))
get_random
W1_LINE_START_Y=$((10 + $R % 40))
get_random
W1_LINE_STOP_Y=$((10 + $R % 40))
get_random
W2_LINE_START_Y=$((10 + $R % 40))
get_random
W2_LINE_STOP_Y=$((10 + $R % 40))
get_random
W3_LINE_START_Y=$((10 + $R % 40))
get_random
W3_LINE_STOP_Y=$((10 + $R % 40))
get_random
B1_LINE_START_Y=$(($R % 40))
get_random
B1_LINE_STOP_Y=$(($R % 40))
get_random
B2_LINE_START_Y=$(($R % 40))
get_random
B2_LINE_STOP_Y=$(($R % 40))
#B3_LINE_START_Y=$(($R % 40))
#B3_LINE_STOP_Y=$(($R % 40))
get_random
B1_LINE_START_X=$(($R % 20))
get_random
B1_LINE_STOP_X=$((100 + $R % 40))
get_random
B2_LINE_START_X=$(($R % 20))
get_random
B2_LINE_STOP_X=$((100 + $R % 40))
#B3_LINE_START_X=$(($R % 20))
#B3_LINE_STOP_X=$((100 + $R % 40))
get_random
ROLL_X=$(($R % 40))
convert -size 180x60 xc:none -pointsize 40 \
\( -clone 0 -fill white \
-stroke black -strokewidth 4 -annotate +0+40 "$INPUT" \
-stroke white -strokewidth 2 -annotate +0+40 "$INPUT" \
-roll +$ROLL_X+0 \
-wave "$WAVE1_AMPLITUDE"x"$WAVE1_LENGTH" \
-roll -$ROLL_X+0 \) \
\( -clone 0 -stroke black \
-strokewidth 1 -draw \
"line $B1_LINE_START_X,$B1_LINE_START_Y $B1_LINE_STOP_X,$B1_LINE_STOP_Y" \
-strokewidth 1 -draw \
"line $B2_LINE_START_X,$B2_LINE_START_Y $B2_LINE_STOP_X,$B2_LINE_STOP_Y" \
-wave "$WAVE2_AMPLITUDE"x"$WAVE2_LENGTH" \) \
\( -clone 0 -stroke white \
-strokewidth 2 -draw "line 0,$W1_LINE_START_Y 140,$W1_LINE_STOP_Y" \
-strokewidth 2 -draw "line 0,$W2_LINE_START_Y 140,$W2_LINE_STOP_Y" \
-strokewidth 2 -draw "line 0,$W3_LINE_START_Y 140,$W3_LINE_STOP_Y" \
-wave "$WAVE3_AMPLITUDE"x"$WAVE3_LENGTH" \) \
-flatten -crop 140x60 +repage -quality 90 -depth 8 png:-
+1 -1
View File
@@ -1,5 +1,5 @@
from flask import Flask from flask import Flask
app = Flask(__name__, static_url_path='/account/static') app = Flask(__name__)
from luser import routes from luser import routes
+3 -4
View File
@@ -3,7 +3,6 @@ LDAPHOST = ldap.example.org
LDAPADMINNAME = cn=admin,dc=example,dc=org LDAPADMINNAME = cn=admin,dc=example,dc=org
LDAPPASS = verysecr3t LDAPPASS = verysecr3t
USERBASE = ou=Users,dc=example,dc=org USERBASE = ou=Users,dc=example,dc=org
CAPTCHA_PATH = /var/luser/luser/static/register/captcha_img/ CAPTCHA_PATH = /var/luser/luser/static/account/register/captcha_img/
[ratelimit] ALTUSERBASE =
PERIOD = 1d # ALTUSERBASE = ou=UsersAlt,dc=example,dc=org
LIMIT = 10
+60 -269
View File
@@ -1,11 +1,5 @@
import ldap3
from ldap3 import Server,Connection,ALL,MODIFY_REPLACE from ldap3 import Server,Connection,ALL,MODIFY_REPLACE
from datetime import datetime from datetime import datetime
import datetime as dt
import os
OBJECTCLASSES = ['top', 'person', 'organizationalPerson', 'inetOrgPerson', 'posixAccount', 'shadowAccount']
USERATTRIBUTES = ['cn' , 'sn', 'givenName', 'uid', 'uidNumber' , 'gidNumber', 'homeDirectory', 'loginShell', 'gecos' , 'shadowLastChange', 'shadowMax', 'userPassword', 'mail', 'description']
class LUSER(): class LUSER():
''' '''
@@ -15,61 +9,9 @@ class LUSER():
admin_user := string DN of LDAP admin user admin_user := string DN of LDAP admin user
admin_pass := string password of LDAP admin user admin_pass := string password of LDAP admin user
base := string base in LDAP system where users are made 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 findlastlog(self):
'''
Return the largest uidNumber attribute of all users in base
'''
self.ldapconnection.search(search_base=self.logbase,search_filter=f'(objectClass=inetOrgPerson)', attributes=['uid'])
alllogs = self.ldapconnection.response
max = 0
for i in alllogs:
i_log = i['attributes']['uid']
if type(i_log) is list:
i_log = i_log[0]
if str(i_log) == 'total':
continue
if type(i_log) is str or type(i_log) is int:
i_log = int(i_log)
if i_log > max:
max = i_log
return max
def getlastlog(self)->int:
try:
self.ldapconnection.search(search_base=f'uid=total,{self.logbase}',search_filter='(objectClass=person)', attributes=['uidNumber'])
response = self.ldapconnection.response
except:
self.setlastlog(0)
if response == []:
self.setlastlog(0)
response = 0
else:
response = int(response[0]['attributes']['uidNumber'])
return response
def setlastlog(self, newvalue: int):
newvalue = int(newvalue)
# Check if total record already present
self.ldapconnection.search(search_base=f'uid=total,{self.logbase}',search_filter='(objectClass=person)', attributes=['uidNumber'])
response = self.ldapconnection.response
attributes = {'cn' : 'total', 'sn' : 'total', 'givenName' : 'total', 'uid' : 'total', 'uidNumber' : newvalue, 'gidNumber' : newvalue, 'homeDirectory' : f'/home/total', 'loginShell' : '/usr/bin/git-shell', 'gecos' : 'SystemUser', 'shadowLastChange' : self.lastpwchangenow(), 'shadowMax' : '45', 'userPassword' : 'total', 'mail' : f'total@{self.domain}' }
if response == []:
self.ldapconnection.add(f'uid=total,{self.logbase}', OBJECTCLASSES, attributes)
else:
self.ldapconnection.modify(f'uid=total,{self.logbase}', {'uidNumber' : (ldap3.MODIFY_REPLACE, [newvalue])})
return self.ldapconnection.response
def findlastuid(self): def findlastuid(self):
''' '''
@@ -83,20 +25,21 @@ class LUSER():
for i in alluids: for i in alluids:
i_uid = i['attributes']['uidNumber'] i_uid = i['attributes']['uidNumber']
if type(i_uid) is str or type(i_uid) is int:
i_uid = int(i_uid)
if i_uid > max: if i_uid > max:
max = i_uid max = i_uid
return max return max
def expandbase(self): def expandbase(self, basealt = ''):
''' '''
Extract orgnaization, name of dc object and full domain part with all dc values from base Extract orgnaization, name of dc object and full domain part with all dc values from base
basealt := string base in LDAP system where users are made, if not set the function uses base specified on creation of LUSER instance (self.base)
''' '''
# Split base string with commas to find values of organization and dc # Split base string with commas to find values of organization and dc
if basealt == '':
baselist = self.base.split(",") baselist = self.base.split(",")
else:
baselist = self.basealt.split(",")
organization = '' organization = ''
dc = '' dc = ''
@@ -127,13 +70,15 @@ class LUSER():
return organization, dc, dcfull, domain return organization, dc, dcfull, domain
def __init__(self, ldap_host, admin_user, admin_pass, base, autoconnect=True, lastUID = 1000): def __init__(self, ldap_host, admin_user, admin_pass, base, basealt='', autoconnect=True, lastUID = 1000):
self.ldap_host = ldap_host self.ldap_host = ldap_host
self.admin_user = admin_user self.admin_user = admin_user
self.admin_pass = admin_pass self.admin_pass = admin_pass
self.base = base self.base = base
self.basealt = basealt
self.organization, self.dc, self.dcfull, self.domain = self.expandbase() self.organization, self.dc, self.dcfull, self.domain = self.expandbase()
self.logbase = f'ou=log,{self.dcfull}' self.organizationalt, self.dcalt, self.dcfullalt, self.domainalt = self.expandbase(self.basealt)
self.alt = True
self.autoconnect = autoconnect self.autoconnect = autoconnect
ldapserver = Server(ldap_host, use_ssl=True) ldapserver = Server(ldap_host, use_ssl=True)
lastuidfound = 0 lastuidfound = 0
@@ -141,19 +86,9 @@ class LUSER():
self.ldapconnection = Connection(ldapserver, admin_user, admin_pass, auto_bind=True) self.ldapconnection = Connection(ldapserver, admin_user, admin_pass, auto_bind=True)
# uid and gid of most recently registered users # uid and gid of most recently registered users
lastuidfound = self.findlastuid() lastuidfound = self.findlastuid()
lastlogfound = self.findlastlog()
else: else:
self.ldapconnection = Connection(ldapserver, admin_user, admin_pass, auto_bind=False) self.ldapconnection = Connection(ldapserver, admin_user, admin_pass, auto_bind=False)
# Check if base and log base is created
self.ldapconnection.search(search_base=f'{self.base}',search_filter='(objectClass=organizationalUnit)', attributes=['ou'])
if self.ldapconnection.response == []:
self.prepare()
self.ldapconnection.search(search_base=f'{self.logbase}',search_filter='(objectClass=organizationalUnit)', attributes=['ou'])
if self.ldapconnection.response == []:
self.prepare()
if lastuidfound == 0: if lastuidfound == 0:
self.lastuid = lastUID self.lastuid = lastUID
self.lastgid = lastUID self.lastgid = lastUID
@@ -161,24 +96,31 @@ class LUSER():
self.lastuid = lastuidfound self.lastuid = lastuidfound
self.lastgid = lastuidfound self.lastgid = lastuidfound
self.lastlog = lastlogfound
self.setlastlog(lastlogfound) # Set alt boolean to false if basealt not set
if basealt == '':
self.alt = False
def prepare(self): def prepare(self):
''' '''
Create base on LDAP host Create base on LDAP host
''' '''
# Create dcObject on LDAP server and store boolean indicating it's success # Create dcObject on LDAP server and store boolean indicating it's success
rcode1 = self.ldapconnection.add(self.dcfull, ['dcObject', 'organization'], {'o' : self.dc, 'dc' : self.dc})
rcode1 = self.ldapconnection.add(f'dc={self.dcfull}', ['dcObject', 'organization'], {'o' : self.dc, 'dc' : self.dc})
# Create organizational units on LDAP server and store boolean indicating it's success # Create organizational units on LDAP server and store boolean indicating it's success
rcode2 = self.ldapconnection.add(self.base, ['top', 'organizationalUnit'], {'ou' : self.organization}) rcode2 = self.ldapconnection.add(self.base, ['top', 'organizationalUnit'], {'ou' : self.organization})
# Create organizational units for log on LDAP server and store boolean indicating it's success # Add dcobject and organizational units as above for base alt
rcode3 = self.ldapconnection.add(self.logbase, ['top', 'organizationalUnit'], {'ou' : 'log'}) rcode3 = True
rcode4 = True
if self.alt:
rcode3 = self.ldapconnection.add(f'dc={self.dcfull}', ['dcObject', 'organization'], {'o' : self.dc, 'dc' : self.dc})
rcode4 = self.ldapconnection.add(self.base, ['top', 'organizationalUnit'], {'ou' : self.organization})
return rcode1 and rcode2 and rcode3 return rcode1 and rcode2 and rcode3 and rcode4
def lastpwchangenow(self): def lastpwchangenow(self):
''' '''
@@ -186,21 +128,22 @@ class LUSER():
messured in days from UNIX epoch messured in days from UNIX epoch
''' '''
return str((datetime.now() - datetime(1970,1,1)).days) return str((datetime.utcnow() - datetime(1970,1,1)).days)
def add(self, user, password): def add(self, user, password, althash=""):
''' '''
Add a user to base in LDAP with user and pass as credentials Add a user to base in LDAP with user and pass as credentials
user := string containing username user := string containing username
password := string containing user password password := string containing user password
althash := string containing user password/hash for the alternative base
''' '''
# Increase UID and GID counters # Increase UID and GID counters
self.lastuid += 1 self.lastuid += 1
self.lastgid += 1 self.lastgid += 1
# Add user to base # Add user to base
id = f"uid={user}" id = f"uid={user}"
lastlog = self.lastlog + 1
# Object classes of a user entry # Object classes of a user entry
objectClass = ['top', 'person', 'organizationalPerson', 'inetOrgPerson', 'posixAccount', 'shadowAccount'] objectClass = ['top', 'person', 'organizationalPerson', 'inetOrgPerson', 'posixAccount', 'shadowAccount']
@@ -208,66 +151,49 @@ class LUSER():
# Attributes for a user entry # 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, 'mail' : f'{user}@{self.domain}' } 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, 'mail' : f'{user}@{self.domain}' }
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, 'mail' : f'{user}@{self.domainalt}'}
# Return boolean value of new user entry # Return boolean value of new user entry
rcode1 = self.ldapconnection.add(f'uid={user},{self.base}', objectClass, attributes) 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
# Add new user to log if self.alt:
attributes['description'] = 'ADD' rcode2 = self.ldapconnection.add(f'{id},{self.basealt}', objectClass, attributesalt)
attributes['uid'] = str(lastlog)
if rcode1:
rcode2 = self.ldapconnection.add(f'uid={str(lastlog)},{self.logbase}', objectClass, attributes)
else: else:
return False rcode2 = True
if rcode2:
self.setlastlog(lastlog)
self.lastlog=lastlog
# Return True only if both entries was successful # Return True only if both entries was successful
return rcode1 and rcode2 return rcode1 and rcode2
def changepassword(self, user, newpass): def changepassword(self, user, newpass, althash=''):
''' '''
Change password of user to newpass Change password of user to newpass
user := string containing username user := string containing username
newpass := string containing new password newpass := string containing new password
althash := string containing password/hash for alternative base
''' '''
# This variable holds boolean indicating successful change of user password # This variable holds boolean indicating successful change of user password
chpassbool = False 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 # This variable holds boolean indicating successful change of shadowLastChange value to current time
chlastchangebool = False
USERATTRIBUTES=['cn' , 'sn', 'givenName', 'uid', 'uidNumber' , 'gidNumber', 'homeDirectory', 'loginShell', 'gecos' , 'shadowLastChange', 'shadowMax', 'userPassword', 'mail']
OBJECTCLASSES = ['top', 'person', 'organizationalPerson', 'inetOrgPerson', 'posixAccount', 'shadowAccount']
self.ldapconnection.search(search_base=f'uid={user},{self.base}',search_filter='(objectClass=person)', attributes=USERATTRIBUTES)
userdata = self.ldapconnection.response[0]
userdata['attributes']['description'] = 'CHANGEPASS'
lastlog = self.lastlog + 1
userdata['attributes']['uid'] = str(lastlog)
chpassbool = self.ldapconnection.modify(f'uid={user},{self.base}', {'userPassword': (MODIFY_REPLACE,[newpass])})
chlastchangebool = self.ldapconnection.modify(f'uid={user},{self.base}', {'shadowLastChange' : (MODIFY_REPLACE,[self.lastpwchangenow()])}) chlastchangebool = self.ldapconnection.modify(f'uid={user},{self.base}', {'shadowLastChange' : (MODIFY_REPLACE,[self.lastpwchangenow()])})
if chpassbool and chlastchangebool: # If alternative base is set modify user password/hash with althash, if not, pretend change was successful
rcode1 = self.ldapconnection.add(f'uid={lastlog},{self.logbase}', OBJECTCLASSES, userdata['attributes']) if self.alt:
chlastchangeboolalt = self.ldapconnection.modify(f'uid={user},{self.base}', {'shadowLastChange' : (MODIFY_REPLACE, [self.lastpwchangenow()])})
else:
chlastchangeboolalt = True
if rcode1: # Return True only if changing of both password and time of last password change was successful
self.setlastlog(lastlog) return chpassbool and chpassboolalt and chlastchangebool and chlastchangeboolalt
self.lastlog = lastlog
return True
else:
return False
else:
return False
def delete(self, user): def delete(self, user):
''' '''
@@ -276,33 +202,16 @@ class LUSER():
user := string containing username user := string containing username
''' '''
USERATTRIBUTES=['cn' , 'sn', 'givenName', 'uid', 'uidNumber' , 'gidNumber', 'homeDirectory', 'loginShell', 'gecos' , 'shadowLastChange', 'shadowMax', 'userPassword', 'mail']
OBJECTCLASSES = ['top', 'person', 'organizationalPerson', 'inetOrgPerson', 'posixAccount', 'shadowAccount']
self.ldapconnection.search(search_base=f'uid={user},{self.base}',search_filter='(objectClass=person)', attributes=USERATTRIBUTES)
userdata = self.ldapconnection.response[0]
userdata['attributes']['description'] = 'DELETE'
lastlog = self.lastlog + 1
userdata['attributes']['uid'] = str(lastlog)
rcode1 = self.ldapconnection.delete(f'uid={user},{self.base}') rcode1 = self.ldapconnection.delete(f'uid={user},{self.base}')
if rcode1: # If alternative base is set delete user from alternative base, if not, pretend deletion was successful
rcode2 = self.ldapconnection.add(f'uid={lastlog},{self.logbase}', OBJECTCLASSES, userdata['attributes']) if self.alt:
rcode2 = self.ldapconnection.delete(f'uid={user},{self.basealt}')
else:
rcode2 = True
if rcode2: # Return True only if deletion from both bases was successful
self.setlastlog(lastlog) return rcode1 and rcode2
self.lastlog = lastlog
return True
else:
return False
else:
return False
def getpassword(self, user): def getpassword(self, user):
''' '''
@@ -314,123 +223,5 @@ class LUSER():
# Search LDAP entries that have object class inetOrgPerson and uid attribute equal to given user field # 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']) self.ldapconnection.search(search_base=self.base,search_filter=f'(&(objectClass=inetOrgPerson)(uid={user}))', attributes=['userPassword'])
## Check if user exists
if self.ldapconnection.response == []:
return False;
# Return userPassword attribute from the response # Return userPassword attribute from the response
userpass = self.ldapconnection.response[0]['attributes']['userPassword'][0] return self.ldapconnection.response[0]['attributes']['userPassword'][0].decode('utf-8')
if type(userpass) is bytes:
userpass = userpass.decode('utf-8')
return userpass
class ratelimit():
'''
Class that manages rate limiting
period := time period in seconds when the limit restarts
limit := maxiumum number of actions allowed in the time period
path := path to the file where the number of occurances are stored
'''
def save(self):
f = open(self.path,'w')
f.write(str(self.period) + "\n")
f.write(str(self.limit) + "\n")
f.write(self.startdate.isoformat() + "\n")
f.write(str(self.current) + "\n")
f.close()
def load(self):
f = open(self.path,'r')
self.period = int(f.readline())
self.limit = int(f.readline())
self.startdate = datetime.fromisoformat(f.readline()[:-1])
self.current = int(f.readline())
f.close()
def parseperiod(self, period: str)->int:
last = period[-1]
if last.isalpha() == False:
return int(period)
number = int(period[:-1])
if last == 's':
return number
if last == 'm':
return number * 60
if last == 'h':
return number * 3600
if last == 'd':
return number * 86400
if last == 'w':
return number * 604800
raise ValueError("Write the period in seconds as intiger or with accepted units as suffix: s(seconds), m(minutes), h(hours), d(days), w(weeks)")
def __init__(self, period : str, limit : int, path : str):
self.period = self.parseperiod(period)
self.limit = limit
self.path = path
# date from when the period start
self.startdate = datetime.now()
# current number of actions in this time period
self.current = 0
if os.path.exists(path) == True:
setperiod = self.period
setlimit = self.limit
self.load()
self.period = setperiod
self.limit = setlimit
# actions might have happened before the new smaller period started
if setperiod < self.period:
self.current = 0
self.save()
self.update()
def check(self, actions = 1)->bool:
self.update()
if self.current + actions > self.limit:
return False
else:
return True
def add(self, actions = 1)->int:
self.update()
if self.check(actions):
self.current += actions
else:
return 0
self.save()
return self.current
def sub(self, actions = 1)->int:
self.update()
self.current -= actions
if self.current < 0:
self.current = 0
self.save()
return self.current
def update(self)->bool:
r = False
self.load()
delta = (datetime.now() - self.startdate).seconds
if delta > self.period:
self.startdate += dt.timedelta(seconds=(delta - delta%self.period))
self.current = 0
r = True
self.save()
return r
+10 -38
View File
@@ -1,7 +1,6 @@
from flask import render_template, request, redirect from flask import render_template, request, redirect
from luser import app from luser import app
from luser.models import LUSER from luser.models import LUSER
from luser.models import ratelimit
from passlib.hash import ldap_salted_sha1,sha512_crypt from passlib.hash import ldap_salted_sha1,sha512_crypt
import subprocess import subprocess
import random import random
@@ -18,11 +17,9 @@ LDAPHOST = config.get('credentials', 'LDAPHOST')
LDAPADMINNAME = config.get('credentials', 'LDAPADMINNAME') LDAPADMINNAME = config.get('credentials', 'LDAPADMINNAME')
LDAPPASS = config.get('credentials', 'LDAPPASS') LDAPPASS = config.get('credentials', 'LDAPPASS')
USERBASE = config.get('credentials', 'USERBASE') USERBASE = config.get('credentials', 'USERBASE')
ALTUSERBASE = config.get('credentials', 'ALTUSERBASE')
CAPTCHA_PATH = config.get('credentials', 'CAPTCHA_PATH') CAPTCHA_PATH = config.get('credentials', 'CAPTCHA_PATH')
PERIOD = config.get('ratelimit', 'PERIOD')
LIMIT = config.get('ratelimit', 'LIMIT')
@app.route('/account/changepassword/', methods=['POST', 'GET']) @app.route('/account/changepassword/', methods=['POST', 'GET'])
def changepassword(): def changepassword():
if request.method == 'GET': if request.method == 'GET':
@@ -37,22 +34,17 @@ def changepassword():
return 'Error: password is too short' return 'Error: password is too short'
# Create a LUSER connection # Create a LUSER connection
luser = LUSER(LDAPHOST,LDAPADMINNAME,LDAPPASS,USERBASE) luser = LUSER(LDAPHOST,LDAPADMINNAME,LDAPPASS,USERBASE,ALTUSERBASE)
# Retrive current password if ldap_salted_sha1.verify(oldpassword, luser.getpassword(username)) == False:
currentpassword = luser.getpassword(username)
if currentpassword == False:
return "User doesn't exist"
if ldap_salted_sha1.verify(oldpassword, currentpassword) == False and oldpassword != LDAPPASS:
return 'Wrong username/password combination' return 'Wrong username/password combination'
ldaphash = ldap_salted_sha1.hash(newpassword) ldaphash = ldap_salted_sha1.hash(newpassword)
althash = sha512_crypt.hash(newpassword)
# Try to change user password # Try to change user password
try: try:
if luser.changepassword(username, ldaphash) == True: if luser.changepassword(username, ldaphash, althash) == True:
return 'User password successfuly changed' return 'User password successfuly changed'
else: else:
return 'User password change failed' return 'User password change failed'
@@ -70,9 +62,9 @@ def unregister():
password = request.form['password'] password = request.form['password']
# Create a LUSER connection # Create a LUSER connection
luser = LUSER(LDAPHOST,LDAPADMINNAME,LDAPPASS,USERBASE) luser = LUSER(LDAPHOST,LDAPADMINNAME,LDAPPASS,USERBASE,ALTUSERBASE)
if ldap_salted_sha1.verify(password, luser.getpassword(username)) == False and password != LDAPPASS: if ldap_salted_sha1.verify(password, luser.getpassword(username)) == False:
return 'Wrong username/password combination' return 'Wrong username/password combination'
# Try to delete user # Try to delete user
@@ -88,9 +80,6 @@ def unregister():
@app.route('/account/register/', methods=['POST', 'GET']) @app.route('/account/register/', methods=['POST', 'GET'])
def register(): def register():
rl = ratelimit(PERIOD, LIMIT, "/var/luser/luser/.ratelimit")
if rl.check(1) == False:
return 'Error: rate limit exceeded, please come again in ' + PERIOD
if request.method == 'GET': if request.method == 'GET':
captcha_solution = str(random.randint(0,999999)) captcha_solution = str(random.randint(0,999999))
captcha_hash = ldap_salted_sha1.hash(captcha_solution) captcha_hash = ldap_salted_sha1.hash(captcha_solution)
@@ -102,7 +91,6 @@ def register():
elif request.method == 'POST': elif request.method == 'POST':
username = request.form['username'] username = request.form['username']
password = request.form['password'] password = request.form['password']
confirmpassword = request.form['confirmpassword']
captcha_answer = request.form['captchaa'] captcha_answer = request.form['captchaa']
captcha_filename = request.form['captchaq'] captcha_filename = request.form['captchaq']
captcha_path = CAPTCHA_PATH + captcha_filename captcha_path = CAPTCHA_PATH + captcha_filename
@@ -129,31 +117,15 @@ def register():
if len(password) < 8: if len(password) < 8:
return 'Error: password is too short' return 'Error: password is too short'
# Check if passwords matches
if password != confirmpassword:
return 'Error: passwords do not match'
# Check if username has uppercase
if username.islower() == False:
return 'Error: uppercase characters in username are not allowed'
# Check lenght of username
if len(username) < 1 or len(username) > 30:
return 'Error: username has to be between 1 and 30 characters long'
# Check if username is alphanumeric
if not username.isalnum():
return 'Error: username can only contain letters and numbers'
# Create a LUSER connection # Create a LUSER connection
luser = LUSER(LDAPHOST,LDAPADMINNAME,LDAPPASS,USERBASE) luser = LUSER(LDAPHOST,LDAPADMINNAME,LDAPPASS,USERBASE,ALTUSERBASE)
# Try to add user # Try to add user
try: try:
ldaphash = ldap_salted_sha1.hash(password) ldaphash = ldap_salted_sha1.hash(password)
althash = sha512_crypt.hash(password)
#smtpctlout=subprocess.run(["smtpctl","encrypt", password],text=True,stdout=subprocess.PIPE) #smtpctlout=subprocess.run(["smtpctl","encrypt", password],text=True,stdout=subprocess.PIPE)
#smtpdhash=smtpctlout.stdout[:-1] #smtpdhash=smtpctlout.stdout[:-1]
if luser.add(username,ldaphash): if luser.add(username,ldaphash,althash):
rl.add(1)
return 'User successfuly registered' return 'User successfuly registered'
else: else:
return 'User registration failed, username probably taken' return 'User registration failed, username probably taken'
-76
View File
@@ -1,76 +0,0 @@
html {
background-color: #181715;
color: #fff;
}
form {
display: flex;
flex-direction: column;
gap: 16px;
max-width: 360px;
margin: 50px auto;
}
button {
height: 30px;
margin-top: 30px;
background-color: #262a2b;
border: 1px solid #7b7265;
color: #a1998d;
border-radius: 5px;
text-transform: uppercase;
letter-spacing: 2px;
}
button:hover {
opacity: 0.9;
cursor: pointer;
}
button:active {
opacity: 0.7;
}
input {
height: 25px;
padding: 2px 8px;
border: 1px solid #303436;
border-radius: 5px;
color: #7b7265;
background-color: #262a2b;
outline: none;
}
.container {
margin: 50px auto;
}
.form-input {
display: flex;
flex: 1;
justify-content: space-between;
align-items: center;
}
.captcha {
width: 200px;
margin: 10px auto;
text-align: center;
}
.captcha img {
width: 100%;
height: auto;
}
.hint {
letter-spacing: 2px;
text-transform: uppercase;
font-size: 10px;
text-align: center;
font-weight: 500px;
}
.danger {
color: #bb3131;
}
+21 -20
View File
@@ -1,34 +1,35 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="sr">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="/img/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="/account/static/styles/main.css">
<title>User password change in LDAP system</title> <title>User password change in LDAP system</title>
</head> <style>
.container {
max-width:900px;
margin: 0 auto;
font-family: Arial, Helvetica, sans-serif;
color: #3d3d3d;
}
body {
background-color: #e6e6e6;
}
</style>
</head>
<body> <body>
<main class="container"> <main class="container">
<h1 style="text-align:center;">Change password</h1> <h1 style="text-align:center;">Change password</h1>
<form action="/account/changepassword/" method="POST"> <form action="/account/changepassword/" method="POST" style="text-align:center;">
<div class="form-input"> <label for="username">username</label>
<label for="username">Username:</label> <input type="text" name="username" id="username" placeholder="username" required>
<input type="text" name="username" id="username" placeholder="Username" required> <label for="oldpassword">oldpassword</label>
</div> <input type="password" name="oldpassword" id="oldpassword" placeholder="oldpassword" required>
<div class="form-input"> <label for="newpassword">newpassword</label>
<label for="oldpassword">Old password:</label> <input type="password" name="newpassword" id="newpassword" placeholder="newpassword" required>
<input type="password" name="oldpassword" id="oldpassword" placeholder="Old password" required> <br>
</div>
<div class="form-input">
<label for="newpassword">New password:</label>
<input type="password" name="newpassword" id="newpassword" placeholder="New password" required>
</div>
<br />
<button>Change Password</button> <button>Change Password</button>
</form> </form>
</main> </main>
</body> </body>
</html> </html>
+24 -28
View File
@@ -1,43 +1,39 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="sr">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="/img/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="/account/static/styles/main.css">
<title>User registration to LDAP system</title> <title>User registration to LDAP system</title>
</head> <style>
.container {
max-width:900px;
margin: 0 auto;
font-family: Arial, Helvetica, sans-serif;
color: #3d3d3d;
}
body {
background-color: #e6e6e6;
}
</style>
</head>
<body> <body>
<main class="container"> <main class="container">
<h1 style="text-align:center;">Register</h1> <h1 style="text-align:center;">Register</h1>
<form action="/account/register/" method="POST"> <form action="/account/register/" method="POST" style="text-align:center;">
<div class="form-input"> <label for="username">username</label>
<label for="username">Username:</label> <input type="text" name="username" id="username" placeholder="username" required>
<input type="text" name="username" id="username" placeholder="Username" required> <label for="password">password</label>
</div> <input type="password" name="password" id="password" placeholder="password" required>
<div class="form-input"> <p>password must be at least 8 characters long<p>
<label for="password">Password:</label> <br>
<input type="password" name="password" id="password" placeholder="Password" required> <img src=/account/register/captcha_img/{{ imgsrc }} alt="{{ captchahash }}" style="width:200">
</div> <p>
<div class="form-input"> <label for="captcha answer">captcha answer</label>
<label for="confirmpassword">Confirm password:</label> <input type="text" name="captchaa" id="captchaa" placeholder="captcha answer" required>
<input type="password" name="confirmpassword" id="confirmpassword" placeholder="Confirm Password" required>
</div>
<p class="hint danger">Password must be at least 8 characters long </p>
<br />
<div class="captcha">
<img src=/account/static/register/captcha_img/{{ imgsrc }} alt="{{ captchahash }}" />
</div>
<div class="form-input">
<label for="captchaa">Captcha answer:</label>
<input type="text" name="captchaa" id="captchaa" placeholder="Captcha answer" required>
</div>
<input type="hidden" name="captchaq" id="captchaq" value="{{ imgsrc }}"> <input type="hidden" name="captchaq" id="captchaq" value="{{ imgsrc }}">
<button>Register</button> <button>Register</button>
</form> </form>
</main> </main>
</body> </body>
</html> </html>
+18 -15
View File
@@ -1,30 +1,33 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="sr">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="/img/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="/account/static/styles/main.css">
<title>User unregistration to LDAP system</title> <title>User unregistration to LDAP system</title>
</head> <style>
.container {
max-width:900px;
margin: 0 auto;
font-family: Arial, Helvetica, sans-serif;
color: #3d3d3d;
}
body {
background-color: #e6e6e6;
}
</style>
</head>
<body> <body>
<main class="container"> <main class="container">
<h1 style="text-align:center;">Unregister</h1> <h1 style="text-align:center;">Unregister</h1>
<form action="/account/unregister/" method="POST"> <form action="/account/unregister/" method="POST" style="text-align:center;">
<div class="form-input"> <label for="username">username</label>
<label for="username">Username:</label> <input type="text" name="username" id="username" placeholder="username" required>
<input type="text" name="username" id="username" placeholder="Username" required> <label for="password">password</label>
</div> <input type="password" name="password" id="password" placeholder="password" required>
<div class="form-input">
<label for="password">Password:</label>
<input type="password" name="password" id="password" placeholder="Password" required>
</div>
<br> <br>
<button>Unregister</button> <button>Unregister</button>
</form> </form>
</main> </main>
</body> </body>
</html> </html>