docs | ||
lumi2 | ||
scss | ||
tests | ||
.dockerignore | ||
.gitignore | ||
config.py | ||
docker-compose.yml | ||
Dockerfile | ||
LICENSE.md | ||
package-lock.json | ||
package.json | ||
pytest.ini | ||
README.md | ||
requirements.txt | ||
shell.nix |
LUMI 2
Lumi2 is the LDAP user management interface, a minimalistic web-ui for managing simple LDAP authentication backends.
It lets an administrator create/read/update/delete users and groups in a user-friendly interface:
Lumi2 eliminates the need for complex LDAP frontends, such as PhpLdapAdmin, or modifying the DIT (directory information tree) through LDIF files.
It is quite opinionated and suitable only for small-scale deployments (such as a homelab or small enterprise), but it works really well there!
Lumi2 can be used with an existing LDAP deployment, but it makes some core assumptions about the DIT it works on.
Deployment
Lumi2 is designed for use and deployment in a microservice environment.
It works well together with a dockerized LDAP server (such as osixia's OpenLDAP image),
and you can use the included docker-compose.yml
as a reference.
If you are deploying Lumi2 alongside a fresh LDAP instance of the osixia OpenLDAP image,
everything works out of the box and you won't ever need to modify your LDAP entries directly.
Assumptions and limitations
Currently, the connection between Lumi2 and your LDAP server does not encrypt traffic using TLS. Your Lumi2 instance and the LDAP server it manages should run on the same host, otherwise it will be possible for a man-in-the-middle to read your user's credentials and personal information.
Lumi2 is simplistic and makes some assumptions about the structure of your LDAP DIT. The DIT hierarchy it expects (or creates) looks like this:
When deploying a new LDAP server, you don't need to pay too much attention to the rest of this section, as Lumi2 will create the DIT entries for you as necessary. You should, however, be aware of this structure when configuring other applications to use your LDAP backend.
If you point Lumi2 at an existing LDAP server, make sure its DIT matches the structure shown above and described below, otherwise Lumi2 will not work with your LDAP instance.
Users
- All user entries are direct children of a single OU (
organizationalUnit
). Thecn
(name) of this OU is configurable. - This OU is a direct child entry of your DIT's root entry
- The RDN (relative distinguished name) for users is their
uid
attribute - User entries are LDAP objects of type
inetOrgPerson
. Lumi2 sets (and expects to find) the following attributes for each user:uid
- usernamecn
- first namesn
- last namedisplayName
- preferred name (or nickname)mail
- email addressjpegPhoto
- profile picture in JPEG formatuserPassword
- SSHA password hash
The uid
(username) can contain latin characters, digits, underscores, hypens and periods, and must have a letter as the first character.
Groups
- All group entries are direct children of a single OU (
organizationalUnit
). Thecn
(name) of this OU is configurable. - This OU is a direct child entry of your DIT's root entry
- The RDN (relative distinguished name) for groups is their
cn
attribute - Group entries are LDAP objects of type
groupOfUniqueNames
:- A group always has at least one member (an LDAP limitation)
- Members of the group are listed in its
uniqueMember
attribute
The groupname (cn
) can contain only latin characters.
Configuration
Your Lumi2 instance is configured using the config.py
python file.
Customize the file so that Lumi2 can connect to your LDAP server and bind to it with admin credentials.
Make sure you configure a secure SECRET_KEY
and ADMIN_PASSWORD
prior to deployment, as described below.
It is recommended you use docker-compose
for Lumi2 and your LDAP server. Use the docker-compose.yml
and config.py
files in this repo as a starting point for this.
Security settings
To generate a secret key and a password hash, you first need to import some of Lumi2's dependencies. The easiest way to do this is by using a virtual environment. In the repo's root folder, run the following shell commands:
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
Lumi2 uses a private key to encrypt session cookies and secrets. Generate a strong secret key now, by running the following command:
python -c 'import secrets; print(secrets.token_hex())'
Replace the insecure SECRET_KEY
in your config.py
with the random string you just generated.
Next, we need to replace the default password hash in config.py
with a more secure one.
Pick a strong password, which you will use later to log in to Lumi2, and generate its hash using the following command:
python -c 'from werkzeug.security import generate_password_hash as gen; from getpass import getpass as p; print("Hash: " + gen(p("Password: ")))'
Replace the existing ADMIN_PASSWORD
in your config.py
with the hash you just generated.
You can deactivate and delete the virtual environment now by running:
deactivate
rm -rf .venv
LDAP settings
Connection settings
Lumi2 needs to know where to reach your LDAP server. This is set in the LDAP_HOSTNAME
variable.
What you put here depends on your environment, but if you are using docker-compose
with both Lumi2 and OpenLDAP running in the same compose-stack,
you can simply set the LDAP container's hostname here.
By default, Lumi2 tries to connnect on 389, the standard LDAP port, but you can specify a non-standard port as well. The following are all valid options:
LDAP_HOSTNAME = 'myhost'
LDAP_HOSTNAME = 'ldap.example.com:9000'
LDAP_HOSTNAME = 'ldap://foo.bar.org'
Important: Communication between Lumi2 and LDAP is currently not encrypted, so anyone listening to the network traffic between the two can read user information being
exchanged between Lumi2 and your LDAP server.
Deploying Lumi2 alongside LDAP using docker-compose
is therefor highly recommended.
Bind user settings
Provide the DN (distinguished name) and password for a user with read- and write-access to your LDAP server by setting the LDAP_BIND_USER_DN
and LDAP_BIND_USER_PASSWORD
variables respectively.
The LDAP_BASE_DN
variable tells Lumi2 what the base DN (the root entry) of your LDAP server is called.
The LDAP_USERS_OU
and LDAP_GROUPS_OU
variables tell Lumi2 under which OU users and groups can be found or created.
Both must be direct children of the root entry.
If they do not exist yet on your LDAP server, they will be newly created by Lumi2 when it first starts.
Logging
By default, the lumi2 container logs HTTP access information to the console.
You can additionally write the HTTP access logs to a file by specifying LOG_FILE_PATH
.
Note that the specified path points to inside the container, so if you want to persist access logs across container restarts, you should set up a Docker volume accordingly.
Make sure the specified path is writeable by Lumi2.
LOG_FILE_MAX_SIZE
specifies how large the log file can get before being replaced (log rotation).
Two access log files are kept: the one currently in use, and the previous one which has reached the maximum size. Any older log files are automatically deleted.
To disable log rotation, leave the variable unspecified or set it to 0
.
Running the server
The Dockerfile
and docker-compose.yml
create a Lumi2 instance running behind a waitress WSGI server,
exposing the Lumi2 web interface on port 80.
It is recommended to use a reverse proxy in front of the waitress server.
Inside the container, Lumi2 and waitress are run by a non-root user.
You can specify the UID
/GID
of this user by setting the LUMI2_UID
/LUMI2_GID
build arguments (see docker-compose.yml
for reference).
You should make sure your config.py
file persists across container restarts and is readable by the container's non-root user.
The LUMI2_CONFIG
environment variable is necessary to tell Lumi2 where config.py
can be found inside the container.
Once you have configured docker-compose.yml
and config.py
, start Lumi2 with
sudo docker-compose up -d --build
Development
Lumi2 is a python application, built using the Flask framework. The following frameworks and/or libraries are also used:
- Backend:
- Flask - a python web framework
- WTForms - a python library for web form handling
- FlaskWTF - an extension to WTForms specific to Flask
- flask-restful - a minimalistic library for Flask to build RESTful web APIs
- ldap3 - python bindings for interaction with LDAP servers
- pytest - a python testing framework
- coverage - test code coverage reporting used in conjunction with pytest
- Pillow - a python image manipulation library
- Flask - a python web framework
- Frontend:
- Bootstrap 5 - a CSS framework and component library
- Bootstrap Icons - an SVG-icon pack used in conjunction with Bootstrap
- jQuery - a JavaScript library for DOM manipulation and AJAX routines
Theming (SASS)
To customize the bootstrap theme, some of Bootstrap 5's SASS variables are modified in /scss/bootstrap.scss
and then compiled using a SASS preprocessor.
Using a CLI SASS preprocessor:
npm install -g sass
You can modify the SASS variables and then compile into CSS:
sass scss/bootstrap.scss lumi2/static/css/bootstrap.css
Testing
Make sure all dependencies listed in requirements.txt
are installed.
To run all unit tests, simply run the following from within the repository root:
coverage run -m pytest && coverage report
This will run all unit tests and display the test coverage in your terminal.