commit 418651f29e0598129d9c73c371dbd4818ae0d005 Author: Zolfa Date: Wed Jul 1 17:58:59 2020 +0200 initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..7826da7 --- /dev/null +++ b/README.md @@ -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! diff --git a/conf/nginx/locations-lilik_users.conf b/conf/nginx/locations-lilik_users.conf new file mode 100644 index 0000000..7b9640f --- /dev/null +++ b/conf/nginx/locations-lilik_users.conf @@ -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; +} + diff --git a/conf/uwsgi/lilik_users.ini b/conf/uwsgi/lilik_users.ini new file mode 100644 index 0000000..867eafc --- /dev/null +++ b/conf/uwsgi/lilik_users.ini @@ -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 + diff --git a/lilik_users.py b/lilik_users.py new file mode 100755 index 0000000..0aa01fe --- /dev/null +++ b/lilik_users.py @@ -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//create/', 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/', 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') diff --git a/static/add.css b/static/add.css new file mode 100644 index 0000000..aa35729 --- /dev/null +++ b/static/add.css @@ -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; +} diff --git a/static/add.html b/static/add.html new file mode 100644 index 0000000..0427783 --- /dev/null +++ b/static/add.html @@ -0,0 +1,80 @@ + + + + + + Create new user + + + + + + +
+ +
+
+
+

Crea nuovo utente

+

Conferma creazione

+
+ + +
+
+ + + + Nome utente utilizzato per il login e per gli indirizzi nome-utente@lilik.it. + +
+
+ + + + Nome pubblico visualizzato, per esempio in file condivisi o chat. + +
+
+ + + + Nome e cognome completo. Non viene reso pubblico, solo per organizzazione interna. + +
+ + + + +
+
+ + + diff --git a/static/add.js b/static/add.js new file mode 100644 index 0000000..cd2262e --- /dev/null +++ b/static/add.js @@ -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; + }; + }); diff --git a/wsgi.py b/wsgi.py new file mode 100644 index 0000000..daafcaf --- /dev/null +++ b/wsgi.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 + +from user_backend import app + +if __name__ == "__main__": + app.run() +