# 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:

![Demo](/docs/demo.gif)

Lumi2 eliminates the need for complex LDAP frontends, such as [PhpLdapAdmin](https://phpldapadmin.sourceforge.net/wiki/index.php/Main_Page), 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](#assumptions-and-limitations) about the DIT it works on.
If you are deploying Lumi2 alongside a fresh LDAP instance, it will create the necessary DIT entries for you.

# 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](https://github.com/osixia/docker-openldap)), and you can use the included `docker-compose.yml` as a reference.

## 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:

![LDAP DIT structure](/docs/ldap-dit-structure.svg)

You should 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 has this structure, otherwise Lumi2 will not work with your LDAP instance.
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.

### Users

- All user entries are direct children of a single OU (`organizationalUnit`). The `cn` (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` - username
    - `cn` - first name
    - `sn` - last name
    - `displayName` - preferred name (or nickname)
    - `mail` - email address
    - `jpegPhoto` - profile picture in JPEG format
    - `userPassword` - SHA512 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`). The `cn` (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. Edit the file accordingly 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:

```bash
python -m venv .venv
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:

```bash
python -c 'import secrets; print(secrets.token_hex())'
```

Set the `SECRET_KEY` in your `config.py` to 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:

```bash
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:
```bash
deactivate
rm -rf .venv
```

### LDAP 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, optionally prefixed with `ldap://` and/or suffixed with the port on which the LDAP server is listening.
By default, Lumi2 tries to connnect on 389, the standard LDAP port.
**Important:** deploying Lumi2 alongside LDAP using `docker-compose` is highly recommended. 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.

Next, 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](https://docs.pylonsproject.org/projects/waitress/en/latest/) 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](https://flask.palletsprojects.com/en/2.2.x/) - a python web framework
        - [WTForms](https://wtforms.readthedocs.io/en/3.0.x/) - a python library for web form handling
        - [FlaskWTF](https://flask-wtf.readthedocs.io/en/1.0.x/) - an extension to WTForms specific to Flask
        - [flask-restful](https://flask-restful.readthedocs.io/en/latest/quickstart.html) - a minimalistic library for Flask to build RESTful web APIs
    - [ldap3](https://ldap3.readthedocs.io/en/latest/) - python bindings for interaction with LDAP servers
    - [pytest](https://docs.pytest.org/en/7.2.x/) - a python testing framework
    - [coverage](https://coverage.readthedocs.io/en/6.5.0/) - test code coverage reporting used in conjunction with pytest
    - [Pillow](https://pillow.readthedocs.io/en/stable/) - a python image manipulation library
- Frontend:
    - [Bootstrap 5](https://getbootstrap.com/docs/5.2/getting-started/introduction/) - a CSS framework and component library
    - [Bootstrap Icons](https://icons.getbootstrap.com/) - an SVG-icon pack used in conjunction with Bootstrap
    - [jQuery](https://api.jquery.com/) - 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:

```bash
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:

```bash
coverage run -m pytest && coverage report
```

This will run all unit tests and display the test coverage in your terminal.
