@ -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() | |||||