Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8caf2bddbc
|
||
|
|
bcd0b10a2d
|
||
|
|
4445aa08c7 | ||
|
|
8621e529a3
|
||
|
|
3362a6bb33
|
||
|
|
2e8b3d0b4d
|
||
|
|
fc84ded0c6 | ||
|
|
dc50e422ba
|
||
|
|
66f0ab50dd
|
||
|
|
ad5cdde65e | ||
|
|
3c75fe3e2c | ||
|
|
f2cd767c5c | ||
|
|
6bc05bcca3 | ||
|
|
fe0e8f8d0c | ||
|
|
0cdde03725
|
||
|
|
2b8c5ea453
|
||
|
|
3818419cae
|
||
|
|
09c22b23a7
|
||
|
|
6251e610e8
|
||
|
|
67f3fefae5
|
||
|
|
e6fdb916bf
|
||
|
|
1da508d0d6
|
||
|
|
7f1ab45d53
|
||
|
|
b33110f8f4
|
||
|
|
718cdf6c89
|
||
|
|
ae167a764e
|
||
|
|
7435588363
|
||
|
|
b94b94b0a3
|
||
|
|
23d4fe44e8
|
||
|
|
d0ba096710
|
||
|
|
0d33b52243
|
||
|
|
654dfceaf0
|
||
|
|
d45dad4540
|
||
|
|
4e764e104a
|
||
|
|
41553b2d74
|
No files matched your search
+4
-1
@@ -1 +1,4 @@
|
|||||||
luser/__pycache__/
|
__pycache__
|
||||||
|
luser/__pycache__
|
||||||
|
luser.deb
|
||||||
|
build-deb/luser/var/
|
||||||
@@ -1,3 +1,72 @@
|
|||||||
# Flask LDAP user management
|
# LUSER
|
||||||
|
|
||||||
Web app that allows users to add,remove and change passwords in LDAP system
|
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.
|
||||||
|
|
||||||
|
## 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.
|
||||||
+5
-2
@@ -1,17 +1,20 @@
|
|||||||
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/
|
|
||||||
@@ -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
|
Depends: python3-flask, python3-ldap3, gunicorn, imagemagick, python3-passlib
|
||||||
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.0.0
|
Version: 1.5.0
|
||||||
Executable
+7
@@ -0,0 +1,7 @@
|
|||||||
|
#!/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
|
||||||
Executable
+4
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
if [ -f /var/luser/luser/config.ini ] ; then
|
||||||
|
cp /var/luser/luser/config.ini /tmp/oldluserconfig.ini
|
||||||
|
fi
|
||||||
Executable
+6
@@ -0,0 +1,6 @@
|
|||||||
|
#!/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 127.0.0.1:5000 run:app
|
ExecStart=/usr/bin/gunicorn --workers 3 --bind 0.0.0.0:80 run:app
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|||||||
Executable
+100
@@ -0,0 +1,100 @@
|
|||||||
|
#!/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
@@ -1,5 +1,5 @@
|
|||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__, static_url_path='/account/static')
|
||||||
|
|
||||||
from luser import routes
|
from luser import routes
|
||||||
+4
-3
@@ -3,6 +3,7 @@ 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/account/register/captcha_img/
|
CAPTCHA_PATH = /var/luser/luser/static/register/captcha_img/
|
||||||
ALTUSERBASE =
|
[ratelimit]
|
||||||
# ALTUSERBASE = ou=UsersAlt,dc=example,dc=org
|
PERIOD = 1d
|
||||||
|
LIMIT = 10
|
||||||
+269
-60
@@ -1,5 +1,11 @@
|
|||||||
|
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():
|
||||||
'''
|
'''
|
||||||
@@ -9,9 +15,61 @@ 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):
|
||||||
'''
|
'''
|
||||||
@@ -25,21 +83,20 @@ 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, basealt = ''):
|
def expandbase(self):
|
||||||
'''
|
'''
|
||||||
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 = ''
|
||||||
@@ -70,15 +127,13 @@ class LUSER():
|
|||||||
|
|
||||||
return organization, dc, dcfull, domain
|
return organization, dc, dcfull, domain
|
||||||
|
|
||||||
def __init__(self, ldap_host, admin_user, admin_pass, base, basealt='', autoconnect=True, lastUID = 1000):
|
def __init__(self, ldap_host, admin_user, admin_pass, base, 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.organizationalt, self.dcalt, self.dcfullalt, self.domainalt = self.expandbase(self.basealt)
|
self.logbase = f'ou=log,{self.dcfull}'
|
||||||
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
|
||||||
@@ -86,9 +141,19 @@ 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
|
||||||
@@ -96,31 +161,24 @@ class LUSER():
|
|||||||
self.lastuid = lastuidfound
|
self.lastuid = lastuidfound
|
||||||
self.lastgid = lastuidfound
|
self.lastgid = lastuidfound
|
||||||
|
|
||||||
|
self.lastlog = lastlogfound
|
||||||
# Set alt boolean to false if basealt not set
|
self.setlastlog(lastlogfound)
|
||||||
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
|
|
||||||
|
|
||||||
rcode1 = self.ldapconnection.add(f'dc={self.dcfull}', ['dcObject', 'organization'], {'o' : self.dc, 'dc' : self.dc})
|
# 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})
|
||||||
|
|
||||||
# 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})
|
||||||
|
|
||||||
# Add dcobject and organizational units as above for base alt
|
# Create organizational units for log on LDAP server and store boolean indicating it's success
|
||||||
rcode3 = True
|
rcode3 = self.ldapconnection.add(self.logbase, ['top', 'organizationalUnit'], {'ou' : 'log'})
|
||||||
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 and rcode4
|
return rcode1 and rcode2 and rcode3
|
||||||
|
|
||||||
def lastpwchangenow(self):
|
def lastpwchangenow(self):
|
||||||
'''
|
'''
|
||||||
@@ -128,22 +186,21 @@ class LUSER():
|
|||||||
messured in days from UNIX epoch
|
messured in days from UNIX epoch
|
||||||
'''
|
'''
|
||||||
|
|
||||||
return str((datetime.utcnow() - datetime(1970,1,1)).days)
|
return str((datetime.now() - datetime(1970,1,1)).days)
|
||||||
|
|
||||||
def add(self, user, password, althash=""):
|
def add(self, user, password):
|
||||||
'''
|
'''
|
||||||
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']
|
||||||
@@ -151,49 +208,66 @@ 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'{id},{self.base}', objectClass, attributes)
|
rcode1 = self.ldapconnection.add(f'uid={user},{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:
|
# Add new user to log
|
||||||
rcode2 = self.ldapconnection.add(f'{id},{self.basealt}', objectClass, attributesalt)
|
attributes['description'] = 'ADD'
|
||||||
|
attributes['uid'] = str(lastlog)
|
||||||
|
|
||||||
|
if rcode1:
|
||||||
|
rcode2 = self.ldapconnection.add(f'uid={str(lastlog)},{self.logbase}', objectClass, attributes)
|
||||||
else:
|
else:
|
||||||
rcode2 = True
|
return False
|
||||||
|
|
||||||
|
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, althash=''):
|
def changepassword(self, user, newpass):
|
||||||
'''
|
'''
|
||||||
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 = self.ldapconnection.modify(f'uid={user},{self.base}', {'userPassword': (MODIFY_REPLACE,[newpass])})
|
chpassbool = False
|
||||||
|
|
||||||
# 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 alternative base is set modify user password/hash with althash, if not, pretend change was successful
|
if chpassbool and chlastchangebool:
|
||||||
if self.alt:
|
rcode1 = self.ldapconnection.add(f'uid={lastlog},{self.logbase}', OBJECTCLASSES, userdata['attributes'])
|
||||||
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
|
if rcode1:
|
||||||
return chpassbool and chpassboolalt and chlastchangebool and chlastchangeboolalt
|
self.setlastlog(lastlog)
|
||||||
|
self.lastlog = lastlog
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
def delete(self, user):
|
def delete(self, user):
|
||||||
'''
|
'''
|
||||||
@@ -202,16 +276,33 @@ 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 alternative base is set delete user from alternative base, if not, pretend deletion was successful
|
if rcode1:
|
||||||
if self.alt:
|
rcode2 = self.ldapconnection.add(f'uid={lastlog},{self.logbase}', OBJECTCLASSES, userdata['attributes'])
|
||||||
rcode2 = self.ldapconnection.delete(f'uid={user},{self.basealt}')
|
|
||||||
else:
|
|
||||||
rcode2 = True
|
|
||||||
|
|
||||||
# Return True only if deletion from both bases was successful
|
if rcode2:
|
||||||
return rcode1 and rcode2
|
self.setlastlog(lastlog)
|
||||||
|
self.lastlog = lastlog
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
def getpassword(self, user):
|
def getpassword(self, user):
|
||||||
'''
|
'''
|
||||||
@@ -223,5 +314,123 @@ 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
|
||||||
return self.ldapconnection.response[0]['attributes']['userPassword'][0].decode('utf-8')
|
userpass = self.ldapconnection.response[0]['attributes']['userPassword'][0]
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
+38
-10
@@ -1,6 +1,7 @@
|
|||||||
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
|
||||||
@@ -17,9 +18,11 @@ 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':
|
||||||
@@ -34,17 +37,22 @@ 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,ALTUSERBASE)
|
luser = LUSER(LDAPHOST,LDAPADMINNAME,LDAPPASS,USERBASE)
|
||||||
|
|
||||||
if ldap_salted_sha1.verify(oldpassword, luser.getpassword(username)) == False:
|
# Retrive current password
|
||||||
|
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, althash) == True:
|
if luser.changepassword(username, ldaphash) == True:
|
||||||
return 'User password successfuly changed'
|
return 'User password successfuly changed'
|
||||||
else:
|
else:
|
||||||
return 'User password change failed'
|
return 'User password change failed'
|
||||||
@@ -62,9 +70,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,ALTUSERBASE)
|
luser = LUSER(LDAPHOST,LDAPADMINNAME,LDAPPASS,USERBASE)
|
||||||
|
|
||||||
if ldap_salted_sha1.verify(password, luser.getpassword(username)) == False:
|
if ldap_salted_sha1.verify(password, luser.getpassword(username)) == False and password != LDAPPASS:
|
||||||
return 'Wrong username/password combination'
|
return 'Wrong username/password combination'
|
||||||
|
|
||||||
# Try to delete user
|
# Try to delete user
|
||||||
@@ -80,6 +88,9 @@ 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)
|
||||||
@@ -91,6 +102,7 @@ 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
|
||||||
@@ -117,15 +129,31 @@ 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,ALTUSERBASE)
|
luser = LUSER(LDAPHOST,LDAPADMINNAME,LDAPPASS,USERBASE)
|
||||||
# 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,althash):
|
if luser.add(username,ldaphash):
|
||||||
|
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'
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@@ -1,35 +1,34 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="sr">
|
<html lang="en">
|
||||||
|
|
||||||
<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>
|
||||||
<style>
|
|
||||||
.container {
|
|
||||||
max-width:900px;
|
|
||||||
margin: 0 auto;
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
color: #3d3d3d;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: #e6e6e6;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</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" style="text-align:center;">
|
<form action="/account/changepassword/" method="POST">
|
||||||
<label for="username">username</label>
|
<div class="form-input">
|
||||||
<input type="text" name="username" id="username" placeholder="username" required>
|
<label for="username">Username:</label>
|
||||||
<label for="oldpassword">oldpassword</label>
|
<input type="text" name="username" id="username" placeholder="Username" required>
|
||||||
<input type="password" name="oldpassword" id="oldpassword" placeholder="oldpassword" required>
|
</div>
|
||||||
<label for="newpassword">newpassword</label>
|
<div class="form-input">
|
||||||
<input type="password" name="newpassword" id="newpassword" placeholder="newpassword" required>
|
<label for="oldpassword">Old password:</label>
|
||||||
<br>
|
<input type="password" name="oldpassword" id="oldpassword" placeholder="Old password" required>
|
||||||
|
</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>
|
||||||
@@ -1,39 +1,43 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="sr">
|
<html lang="en">
|
||||||
|
|
||||||
<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>
|
||||||
<style>
|
|
||||||
.container {
|
|
||||||
max-width:900px;
|
|
||||||
margin: 0 auto;
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
color: #3d3d3d;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: #e6e6e6;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</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" style="text-align:center;">
|
<form action="/account/register/" method="POST">
|
||||||
<label for="username">username</label>
|
<div class="form-input">
|
||||||
<input type="text" name="username" id="username" placeholder="username" required>
|
<label for="username">Username:</label>
|
||||||
<label for="password">password</label>
|
<input type="text" name="username" id="username" placeholder="Username" required>
|
||||||
<input type="password" name="password" id="password" placeholder="password" required>
|
</div>
|
||||||
<p>password must be at least 8 characters long<p>
|
<div class="form-input">
|
||||||
<br>
|
<label for="password">Password:</label>
|
||||||
<img src=/account/register/captcha_img/{{ imgsrc }} alt="{{ captchahash }}" style="width:200">
|
<input type="password" name="password" id="password" placeholder="Password" required>
|
||||||
<p>
|
</div>
|
||||||
<label for="captcha answer">captcha answer</label>
|
<div class="form-input">
|
||||||
<input type="text" name="captchaa" id="captchaa" placeholder="captcha answer" required>
|
<label for="confirmpassword">Confirm password:</label>
|
||||||
|
<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>
|
||||||
@@ -1,33 +1,30 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="sr">
|
<html lang="en">
|
||||||
|
|
||||||
<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>
|
||||||
<style>
|
|
||||||
.container {
|
|
||||||
max-width:900px;
|
|
||||||
margin: 0 auto;
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
color: #3d3d3d;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: #e6e6e6;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</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" style="text-align:center;">
|
<form action="/account/unregister/" method="POST">
|
||||||
<label for="username">username</label>
|
<div class="form-input">
|
||||||
<input type="text" name="username" id="username" placeholder="username" required>
|
<label for="username">Username:</label>
|
||||||
<label for="password">password</label>
|
<input type="text" name="username" id="username" placeholder="Username" required>
|
||||||
<input type="password" name="password" id="password" placeholder="password" required>
|
</div>
|
||||||
|
<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>
|
||||||
Reference in new issue
Block a user