Compare commits
6
Commits
fc84ded0c6
..
v1.5.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8caf2bddbc
|
||
|
|
bcd0b10a2d
|
||
|
|
4445aa08c7 | ||
|
|
8621e529a3
|
||
|
|
3362a6bb33
|
||
|
|
2e8b3d0b4d
|
No files matched your search
@@ -9,6 +9,7 @@ A lightweight user management system for LDAP, implemented using Flask. This app
|
|||||||
- **User Unregistration**: Users can remove themselves from the LDAP system.
|
- **User Unregistration**: Users can remove themselves from the LDAP system.
|
||||||
- **Captcha Protection**: Uses a custom captcha mechanism to mitigate bot registrations.
|
- **Captcha Protection**: Uses a custom captcha mechanism to mitigate bot registrations.
|
||||||
- **LDAP Integration**: Directly interacts with LDAP for all credential and user management tasks.
|
- **LDAP Integration**: Directly interacts with LDAP for all credential and user management tasks.
|
||||||
|
- **Rate limiting** (new): Setting rate limiting for user registration.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
@@ -37,6 +38,8 @@ A lightweight user management system for LDAP, implemented using Flask. This app
|
|||||||
- `LDAPPASS`: Admin password for LDAP operations.
|
- `LDAPPASS`: Admin password for LDAP operations.
|
||||||
- `USERBASE`: The base DN for users.
|
- `USERBASE`: The base DN for users.
|
||||||
- `CAPTCHA_PATH`: Path where captcha images are stored.
|
- `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**:
|
4. **Setup Captcha script**:
|
||||||
Ensure the `captcha.sh` script is available in `/usr/local/bin/` and is executable.
|
Ensure the `captcha.sh` script is available in `/usr/local/bin/` and is executable.
|
||||||
|
|||||||
@@ -8,4 +8,4 @@ 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.4.1
|
Version: 1.5.0
|
||||||
@@ -4,3 +4,6 @@ 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/register/captcha_img/
|
||||||
|
[ratelimit]
|
||||||
|
PERIOD = 1d
|
||||||
|
LIMIT = 10
|
||||||
+112
-1
@@ -1,6 +1,8 @@
|
|||||||
import ldap3
|
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']
|
OBJECTCLASSES = ['top', 'person', 'organizationalPerson', 'inetOrgPerson', 'posixAccount', 'shadowAccount']
|
||||||
USERATTRIBUTES = ['cn' , 'sn', 'givenName', 'uid', 'uidNumber' , 'gidNumber', 'homeDirectory', 'loginShell', 'gecos' , 'shadowLastChange', 'shadowMax', 'userPassword', 'mail', 'description']
|
USERATTRIBUTES = ['cn' , 'sn', 'givenName', 'uid', 'uidNumber' , 'gidNumber', 'homeDirectory', 'loginShell', 'gecos' , 'shadowLastChange', 'shadowMax', 'userPassword', 'mail', 'description']
|
||||||
@@ -184,7 +186,7 @@ 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):
|
def add(self, user, password):
|
||||||
'''
|
'''
|
||||||
@@ -323,3 +325,112 @@ class LUSER():
|
|||||||
userpass = userpass.decode('utf-8')
|
userpass = userpass.decode('utf-8')
|
||||||
|
|
||||||
return userpass
|
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
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -19,6 +20,9 @@ LDAPPASS = config.get('credentials', 'LDAPPASS')
|
|||||||
USERBASE = config.get('credentials', 'USERBASE')
|
USERBASE = config.get('credentials', 'USERBASE')
|
||||||
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':
|
||||||
@@ -84,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)
|
||||||
@@ -146,6 +153,7 @@ def register():
|
|||||||
#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):
|
||||||
|
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'
|
||||||
|
|||||||
Reference in new issue
Block a user