Browse Source

initial commit

master
Zolfa 4 years ago
commit
418651f29e
Signed by: zolfa GPG Key ID: E1A43B038C4D6616
8 changed files with 447 additions and 0 deletions
  1. +41
    -0
      README.md
  2. +10
    -0
      conf/nginx/locations-lilik_users.conf
  3. +9
    -0
      conf/uwsgi/lilik_users.ini
  4. +169
    -0
      lilik_users.py
  5. +44
    -0
      static/add.css
  6. +80
    -0
      static/add.html
  7. +87
    -0
      static/add.js
  8. +7
    -0
      wsgi.py

+ 41
- 0
README.md View File

@ -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!

+ 10
- 0
conf/nginx/locations-lilik_users.conf View File

@ -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;
}

+ 9
- 0
conf/uwsgi/lilik_users.ini View File

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

+ 169
- 0
lilik_users.py View File

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

+ 44
- 0
static/add.css View File

@ -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;
}

+ 80
- 0
static/add.html View File

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

+ 87
- 0
static/add.js View File

@ -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;
};
});

+ 7
- 0
wsgi.py View File

@ -0,0 +1,7 @@
#!/usr/bin/env python3
from user_backend import app
if __name__ == "__main__":
app.run()

Loading…
Cancel
Save