@ -0,0 +1,41 @@ | |||
# Requirements | |||
List of debian (bullseye) package required. | |||
- `python3-ldap` | |||
- `python3-flask` | |||
- `uwsgi` | |||
- `uwsgi-plugin-python3` | |||
- `nginx` | |||
# Configuration | |||
1. Copy all the files in `/opt/lilk-users` | |||
2. Edit LDAP settings in `lilik_users.py` | |||
3. Symlink `conf/uwsgi/lilik_users.ini` in `/etc/uwsgi/apps-enabled/` | |||
ln -s /opt/lilik-users/conf/uwsgi/lilik_users.ini /etc/uwsgi/apps-enabled/lilik_users.ini | |||
4. Configure nginx creating a `server` section in `/etc/nginx/sites-enabled/` | |||
# /etc/nginx/sites-enabled/site1.lilik.it.conf | |||
server { | |||
listen 443 ssl http2; | |||
listen [::]:443 ssl http2; | |||
# ... server_name and ssl configuration | |||
include /opt/lilik-users/conf/nginx/locations-lilik_users.conf; | |||
} | |||
5. Restart uwsgi and nginx | |||
systemctl restart uwsgi | |||
systemctl restart nginx | |||
6. Point your browser to `/add/add.html` and enjoy! |
@ -0,0 +1,10 @@ | |||
location /user_backend { | |||
rewrite ^/user_backend/(.*) /$1 break; | |||
include uwsgi_params; | |||
uwsgi_pass unix:/run/lilik_backend.sock; | |||
} | |||
location /add { | |||
alias /opt/lilik-users/static; | |||
} | |||
@ -0,0 +1,9 @@ | |||
[uwsgi] | |||
processes = 2 | |||
socket = /run/lilik_backend.sock | |||
uid = lilik-users | |||
gid = www-data | |||
chdir = /opt/lilik-users | |||
plugin = python3 | |||
module = wsgi:app | |||
@ -0,0 +1,169 @@ | |||
#!/usr/bin/env python3 | |||
import ldap | |||
import ldap.modlist | |||
import string | |||
from random import SystemRandom | |||
# LDAP Server Configuration | |||
LDAP_URI = "ldap://ldap1.dmz.lilik.it" | |||
LDAP_STARTTLS = True | |||
LDAP_BASEDN = "dc=lilik,dc=it" | |||
MAIL_DOMAIN = "lilik.it" | |||
NEW_PW_LEN = 10 | |||
# Manager Account | |||
# Required privileges: | |||
# - New user creation in `ou=People` | |||
# - User search in `ou=People` | |||
# All other action are performed as the logged-in user, and must be | |||
# allowed by LDAP ACLs | |||
LDAPMANAGER_BINDDN = "cn=,ou=Server,dc=lilik,dc=it" | |||
LDAPMANAGER_BINDPW = "" | |||
def ldapmanager_check_uid_exists(uid): | |||
conn = ldap.initialize(LDAP_URI) | |||
if(LDAP_STARTTLS): | |||
conn.start_tls_s() | |||
conn.simple_bind_s(LDAPMANAGER_BINDDN, LDAPMANAGER_BINDPW) | |||
results = conn.search_s('ou=People,{}'.format(LDAP_BASEDN), | |||
ldap.SCOPE_SUBTREE, | |||
'uid={}'.format(uid), | |||
['uid']) | |||
conn.unbind_s() | |||
return len(results) > 0 | |||
def ldapmanager_create_uid(uid, group_cn, req_attrs): | |||
dn="uid={},ou=People,{}".format(uid, LDAP_BASEDN) | |||
# New user template | |||
attrs = { | |||
'objectClass': [b'top', b'inetOrgPerson', b'authorizedServiceObject'], | |||
'uid': uid.encode('utf-8'), | |||
'mail': "{}@{}".format(uid, MAIL_DOMAIN).encode('utf-8'), | |||
'manager': "cn={},ou=Group,{}".format(group_cn, LDAP_BASEDN).encode('utf-8'), | |||
'cn': req_attrs['cn'].encode('utf-8'), | |||
'sn': req_attrs['sn'].encode('utf-8'), | |||
'authorizedService': [b'nextcloud', b'matrix', b'gitea'] | |||
} | |||
conn = ldap.initialize(LDAP_URI) | |||
if(LDAP_STARTTLS): | |||
conn.start_tls_s() | |||
conn.simple_bind_s(LDAPMANAGER_BINDDN, LDAPMANAGER_BINDPW) | |||
conn.add_s(dn, ldap.modlist.addModlist(attrs)) | |||
conn.unbind_s() | |||
return | |||
class Group(object): | |||
def __init__(self, admin_uid, admin_pass): | |||
self.admin_uid = admin_uid | |||
self.conn = self.bind(admin_uid, admin_pass) | |||
def bind(self, admin_uid, admin_pass): | |||
conn = ldap.initialize(LDAP_URI) | |||
if(LDAP_STARTTLS): | |||
conn.start_tls_s() | |||
conn.simple_bind_s("uid={},ou=People,{}".format(admin_uid, LDAP_BASEDN), admin_pass) | |||
return conn | |||
def group_list(self): | |||
results = self.conn.search_s('ou=Group,{}'.format(LDAP_BASEDN), | |||
ldap.SCOPE_SUBTREE, | |||
'owner=uid={},ou=People,{}'.format(self.admin_uid, LDAP_BASEDN), | |||
['cn', 'description']) | |||
return [ (group["cn"][0].decode('utf-8'), | |||
group["description"][0].decode('utf-8')) for key, group in results ] | |||
def member_list(self, group_cn): | |||
results = self.conn.search_s('cn={},ou=Group,{}'.format(group_cn, LDAP_BASEDN), | |||
ldap.SCOPE_BASE, | |||
'owner=uid={},ou=People,{}'.format(self.admin_uid, LDAP_BASEDN), | |||
['member']) | |||
return [ ldap.dn.str2dn(member)[0][0][1] for member in results[0][1]["member"] ] | |||
def add_member(self, group_cn, uid): | |||
self.conn.modify_s('cn={},ou=Group,{}'.format(group_cn, LDAP_BASEDN), | |||
[ (ldap.MOD_ADD, 'member', ["uid={},ou=People,{}".format(uid, LDAP_BASEDN).encode('utf-8')]) ]) | |||
return | |||
def reset_password(self, uid): | |||
valid_chars = (string.ascii_uppercase | |||
+ string.ascii_lowercase | |||
+ string.digits) | |||
rng = SystemRandom() | |||
passwd = "".join([rng.choice(valid_chars) for _ in range(NEW_PW_LEN)]) | |||
result = self.conn.passwd_s('uid={},ou=People,{}'.format(uid, LDAP_BASEDN), | |||
None, | |||
passwd) | |||
return passwd | |||
from flask import Flask | |||
from flask import request, jsonify | |||
app = Flask(__name__) | |||
@app.route('/get_list', methods=['POST']) | |||
def display_list(): | |||
try: | |||
g = Group(request.form['username'], request.form['password']) | |||
result = [ | |||
{ "cn": group[0], | |||
"description": group[1], | |||
"members": g.member_list(group[0]) | |||
} | |||
for group in g.group_list() | |||
] | |||
except Exception as e: | |||
return jsonify({"failed": True, "reason": str(e)}) | |||
return jsonify(result) | |||
@app.route('/group/<group_cn>/create/<new_uid>', methods=['POST']) | |||
def new_user(group_cn, new_uid): | |||
try: | |||
g = Group(request.json['username'], request.json['password']) | |||
if group_cn not in [ group[0] for group in g.group_list() ]: | |||
result = { "failed": True, | |||
"reason": "User {} is not an administrator for group {}".format(request.json['username'], group_cn) } | |||
elif ldapmanager_check_uid_exists(new_uid): | |||
result = { "failed": True, | |||
"reason": "User {} already exists, choose another name.".format(new_uid) } | |||
else: | |||
ldapmanager_create_uid(new_uid, group_cn, request.json['newEntry']) | |||
g.add_member(group_cn, new_uid) | |||
new_passwd = g.reset_password(new_uid) | |||
result = { "failed": False, | |||
"newPasswd": new_passwd } | |||
except Exception as e: | |||
return jsonify({ "failed": True, "reason": str(e) }) | |||
return jsonify(result) | |||
@app.route('/groups', methods=['POST']) | |||
def group_list(): | |||
try: | |||
g = Group(request.json['username'], request.json['password']) | |||
result = { | |||
"failed": False, | |||
"groups": { group[0]: group[1] for group in g.group_list() } | |||
} | |||
except Exception as e: | |||
return jsonify({"failed": True, "reason": str(e)}) | |||
return jsonify(result) | |||
@app.route('/reset_password/<target_user>', methods=['POST']) | |||
def reset_password(target_user): | |||
try: | |||
g = Group(request.form['username'], request.form['password']) | |||
newpasswd = g.reset_password(target_user) | |||
except Exception as e: | |||
return jsonify({"failed": True, "reason": str(e)}) | |||
return jsonify({"failed": False, "passwd": newpasswd}) | |||
if __name__ == "__main__": | |||
app.run(host='127.0.0.1') |
@ -0,0 +1,44 @@ | |||
html, | |||
body { | |||
height: 100%; | |||
} | |||
body { | |||
display: -ms-flexbox; | |||
display: flex; | |||
-ms-flex-align: center; | |||
align-items: center; | |||
padding-top: 40px; | |||
padding-bottom: 40px; | |||
background-color: #f5f5f5; | |||
} | |||
.form-signin { | |||
width: 100%; | |||
max-width: 330px; | |||
padding: 15px; | |||
margin: auto; | |||
} | |||
.form-signin .checkbox { | |||
font-weight: 400; | |||
} | |||
.form-signin .form-control { | |||
position: relative; | |||
box-sizing: border-box; | |||
height: auto; | |||
padding: 10px; | |||
font-size: 16px; | |||
} | |||
.form-signin .form-control:focus { | |||
z-index: 2; | |||
} | |||
.form-signin input[type="username"] { | |||
margin-bottom: -1px; | |||
border-bottom-right-radius: 0; | |||
border-bottom-left-radius: 0; | |||
} | |||
.form-signin input[type="password"] { | |||
margin-bottom: 10px; | |||
border-top-left-radius: 0; | |||
border-top-right-radius: 0; | |||
} |
@ -0,0 +1,80 @@ | |||
<!doctype html> | |||
<html lang="it" ng-app="lilikAddUserApp"> | |||
<head> | |||
<meta charset="utf-8" /> | |||
<meta name="viewport" content="width=device-width, initial-scale=1" /> | |||
<title>Create new user</title> | |||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous" /> | |||
<link rel="stylesheet" href="add.css" /> | |||
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.9/angular.min.js" integrity="sha384-7N66VCkbpJUVwHKCEz5qEgog94BLAJpgfXvD55ThBuHpdPrffMfva/Kl1WH8Y8FS" crossorigin="anonymous"></script> | |||
<script src="add.js"></script> | |||
</head> | |||
<body ng-controller="AddUserController"> | |||
<div class="container text-center" ng-show="showLoginForm"> | |||
<form class="form-signin"> | |||
<h1 class="h3 mb-3 font-weight-normal">Accesso admin</h1> | |||
<label for="adminUid" class="sr-only">Username:</label> | |||
<input class="form-control" type="username" id="adminUid" ng-model="adminUid" placeholder="Username" required autofocus /> | |||
<input class="form-control" type="password" id="adminPwd" ng-model="adminPwd" placeholder="Password" required /> | |||
<button class="btn btn-lg btn-primary btn-block" type="submit" ng-click="getGroups()">Login</button> | |||
<div ng-show="loginError" class="alert alert-danger" role="alert"> | |||
<h4>Impossibile accedere!</h4> | |||
<p>Non è stato possibile autenticarsi come amministratore.</p> | |||
</div> | |||
</form> | |||
</div> | |||
<div class="container" ng-show="showAddForm"> | |||
<form name="newUserForm"> | |||
<h1 ng-show="!startConfirm" class="h3 mb-3 font-weight-normal">Crea nuovo utente</h1> | |||
<h1 ng-show="startConfirm" class="h3 mb-3 font-weight-normal">Conferma creazione</h1> | |||
<div class="form-group"> | |||
<label for="newGroup">Gruppo principale</label> | |||
<select class="form-control" id="newGroup" ng-model="newGroup" required ng-readonly="startConfirm"> | |||
<option ng-repeat="(key, value) in groups" value="{{key}}">{{value}}</option> | |||
</select> | |||
</div> | |||
<div class="form-group"> | |||
<label for="newUid">Username</label> | |||
<input class="form-control" type="username" id="newUid" ng-model="newUid" placeholder="nome-utente" aria-describedby="newUidHelp" required ng-readonly="startConfirm" ng-keyup="typingUser()" /> | |||
<small id="newUidHelp" class="form-text text-muted"> | |||
Nome utente utilizzato per il login e per gli indirizzi <i>nome-utente</i><b>@lilik.it</b>. | |||
</small> | |||
</div> | |||
<div class="form-group"> | |||
<label for="newCn">Nome visualizzato</label> | |||
<input class="form-control" type="commonname" id="newCn" ng-model="newCn" placeholder="Nome Utente" aria-describedby="newCnHelp" required ng-readonly="startConfirm" /> | |||
<small id="newCnHelp" class="form-text text-muted"> | |||
Nome pubblico visualizzato, per esempio in file condivisi o chat. | |||
</small> | |||
</div> | |||
<div class="form-group"> | |||
<label for="newCn">Nome e cognome</label> | |||
<input class="form-control" type="fullname" id="newSn" ng-model="newSn" placeholder="Nome Cognome" aria-describedby="newSnHelp" required ng-readonly="startConfirm" /> | |||
<small id="newSnHelp" class="form-text text-muted"> | |||
Nome e cognome completo. Non viene reso pubblico, solo per organizzazione interna. | |||
</small> | |||
</div> | |||
<button ng-disabled="newUserForm.$invalid" ng-show="!startConfirm" class="btn btn-lg btn-primary btn-block" type="submit" ng-click="createNewUser()">Crea</button> | |||
<button ng-disabled="creationPending" ng-show="startConfirm && !newPasswd && !createError" class="btn btn-lg btn-primary btn-block" type="submit" ng-click="confirmNewUser()">Conferma</button> | |||
<div ng-show="newPasswd" class="alert alert-success" role="alert"> | |||
<h4>Utente creato!</h4> | |||
<p>Quella che segue è la password temporanea. Comunicala all'utente attraverso un canale sicuro e invitalo a modificarla prima di ogni altra cosa, | |||
accedendo a <u>https://login.lilik.it</u>. Copiala e inviala ora, non verrà mostrata in un secondo momento!</p> | |||
<hr> | |||
<h2 class="text-monospace text-center">{{newPasswd}}</h2> | |||
<hr> | |||
<a href="#" class="alert-link" ng-click="anotherUser()">Crea un altro utente</a> | |||
</div> | |||
<div ng-show="createError" class="alert alert-danger" role="alert"> | |||
<h4>Errore!</h4> | |||
<p>Non è stato possibile completare la creazione dell'utente per il seguente motivo:</p> | |||
<hr> | |||
<p>{{createError}}</p> | |||
<hr> | |||
<a href="#" class="alert-link" ng-click="editUser()">Modifica dati utente</a> | |||
</div> | |||
</form> | |||
</div> | |||
</body> | |||
</body> | |||
</html> |
@ -0,0 +1,87 @@ | |||
function createLdapUser(adminUid, adminPwd, newEntry) { | |||
if (adminUid === "admin" && adminPwd === "password") { | |||
return { "failed": false, | |||
"newPasswd": "NuOvA-PaSsWwOrD" }; | |||
} | |||
return { "failed": true, | |||
"reason": "Invalid admin username or password." }; | |||
}; | |||
var lilikAddUserApp = angular.module('lilikAddUserApp', []); | |||
lilikAddUserApp.controller('AddUserController', function AddUserController($scope, $http) { | |||
// Initialize scope variable | |||
$scope.showLoginForm = true; | |||
$scope.showAddForm = false; | |||
$scope.showConfirmForm = false; | |||
$scope.startConfirm = false; | |||
$scope.creationPending = false; | |||
$scope.typingUser = function () { | |||
$scope.newUid = $scope.newUid.toLowerCase().replace(' ', '-'); | |||
$scope.newCn = $scope.newUid.replace('-',' ').replace(/(^\w|\s\w)/g, m => m.toUpperCase()); | |||
}; | |||
// Try to login and download groups | |||
$scope.getGroups = function () { | |||
$scope.creationPending = true; | |||
delete $scope.loginError; | |||
$http.post('../user_backend/groups', { 'username': $scope.adminUid, 'password': $scope.adminPwd }) | |||
.then(function (response) { | |||
result = response.data; | |||
if (result.failed === false) { | |||
$scope.showAddForm = true; | |||
$scope.showLoginForm = false; | |||
$scope.groups = result.groups; | |||
} else { | |||
$scope.loginError = result.reason; | |||
} | |||
}); | |||
$scope.creationPending = false; | |||
}; | |||
// Ask for confirmation of user details | |||
$scope.createNewUser = function () { | |||
$scope.startConfirm = true; | |||
}; | |||
// Try to create the user | |||
$scope.confirmNewUser = function () { | |||
newEntry = { | |||
"cn": $scope.newCn, | |||
"sn": $scope.newSn | |||
}; | |||
$http.post('../user_backend/group/'+$scope.newGroup+'/create/'+$scope.newUid, | |||
{ | |||
'username': $scope.adminUid, | |||
'password': $scope.adminPwd, | |||
'newEntry': newEntry | |||
}) | |||
.then(function (response) { | |||
result = response.data; | |||
if (result.failed === false) { | |||
$scope.newPasswd = result.newPasswd; | |||
} else { | |||
$scope.createError = result.reason; | |||
} | |||
}); | |||
}; | |||
// Clear old user scope variable and start creating another | |||
$scope.anotherUser = function () { | |||
delete $scope.newPasswd; | |||
delete $scope.newGroup; | |||
delete $scope.newUid; | |||
delete $scope.newCn; | |||
delete $scope.newSn; | |||
$scope.startConfirm = false; | |||
}; | |||
$scope.editUser = function () { | |||
delete $scope.createError; | |||
$scope.startConfirm = false; | |||
}; | |||
}); |
@ -0,0 +1,7 @@ | |||
#!/usr/bin/env python3 | |||
from user_backend import app | |||
if __name__ == "__main__": | |||
app.run() | |||