Compare commits
6
Commits
fc84ded0c6
..
master
| 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.
|
||||
- **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
|
||||
|
||||
@@ -37,6 +38,8 @@ A lightweight user management system for LDAP, implemented using Flask. This app
|
||||
- `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.
|
||||
|
||||
@@ -8,4 +8,4 @@ Depends: python3-flask, python3-ldap3, gunicorn, imagemagick, python3-passlib
|
||||
Homepage: https://gitea.dmz.rs/fram3d/luser
|
||||
Maintainer: fram3d <fram3d@dmz.rs>
|
||||
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
|
||||
USERBASE = ou=Users,dc=example,dc=org
|
||||
CAPTCHA_PATH = /var/luser/luser/static/register/captcha_img/
|
||||
[ratelimit]
|
||||
PERIOD = 1d
|
||||
LIMIT = 10
|
||||
+112
-1
@@ -1,6 +1,8 @@
|
||||
import ldap3
|
||||
from ldap3 import Server,Connection,ALL,MODIFY_REPLACE
|
||||
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']
|
||||
@@ -184,7 +186,7 @@ class LUSER():
|
||||
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):
|
||||
'''
|
||||
@@ -323,3 +325,112 @@ class LUSER():
|
||||
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
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from flask import render_template, request, redirect
|
||||
from luser import app
|
||||
from luser.models import LUSER
|
||||
from luser.models import ratelimit
|
||||
from passlib.hash import ldap_salted_sha1,sha512_crypt
|
||||
import subprocess
|
||||
import random
|
||||
@@ -19,6 +20,9 @@ LDAPPASS = config.get('credentials', 'LDAPPASS')
|
||||
USERBASE = config.get('credentials', 'USERBASE')
|
||||
CAPTCHA_PATH = config.get('credentials', 'CAPTCHA_PATH')
|
||||
|
||||
PERIOD = config.get('ratelimit', 'PERIOD')
|
||||
LIMIT = config.get('ratelimit', 'LIMIT')
|
||||
|
||||
@app.route('/account/changepassword/', methods=['POST', 'GET'])
|
||||
def changepassword():
|
||||
if request.method == 'GET':
|
||||
@@ -84,6 +88,9 @@ def unregister():
|
||||
|
||||
@app.route('/account/register/', methods=['POST', 'GET'])
|
||||
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':
|
||||
captcha_solution = str(random.randint(0,999999))
|
||||
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)
|
||||
#smtpdhash=smtpctlout.stdout[:-1]
|
||||
if luser.add(username,ldaphash):
|
||||
rl.add(1)
|
||||
return 'User successfuly registered'
|
||||
else:
|
||||
return 'User registration failed, username probably taken'
|
||||
|
||||
Reference in new issue
Block a user