Skip to content

Commit

Permalink
Reformat, package, validate request body
Browse files Browse the repository at this point in the history
  • Loading branch information
dnknth committed Oct 29, 2024
1 parent 1d70132 commit d1242c2
Show file tree
Hide file tree
Showing 47 changed files with 1,974 additions and 1,794 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
.venv*
.activate
__pycache__
*.egg-info

.DS_Store
node_modules
/dist
build
dist
statics

# local env files
.env*
Expand Down
31 changes: 13 additions & 18 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python Debugger: Starlette",
"type": "debugpy",
"request": "launch",
"module": "uvicorn",
"args": [
"app:app",
"--port",
"5000",
"--reload"
]
}
]
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Debug: Backend",
"type": "debugpy",
"request": "launch",
"module": "uvicorn",
"args": ["app:app", "--port", "5000", "--reload"]
}
]
}
12 changes: 3 additions & 9 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
{
"css.customData": [".vscode/tailwind.json"],
"editor.formatOnSave": true,
"[python]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
}
},
"python.testing.unittestArgs": [
"-v",
"-s",
".",
"-p",
"*test.py"
],
"python.testing.pytestEnabled": false,
"python.testing.unittestEnabled": true
"python.testing.unittestEnabled": true,
"python.testing.unittestArgs": ["-v", "-s", "./tests", "-p", "*_test.py"]
}
14 changes: 3 additions & 11 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
FROM node:lts-alpine AS builder
COPY . /app
WORKDIR /app
RUN npm audit && npm i && npm run build

FROM alpine:3
COPY --from=builder /app/dist /app/dist
RUN apk add --no-cache python3 py3-pip py3-pyldap py3-pytoml \
&& pip3 install --break-system-packages python-multipart starlette uvicorn
COPY app.py ldap_api.py ldap_helpers.py schema.py settings.py /app/
RUN apk add --no-cache py3-pip py3-pyldap
RUN pip3 install --break-system-packages ldap-ui

WORKDIR /app
EXPOSE 5000
CMD ["/usr/bin/uvicorn", "--host", "0.0.0.0", "--port", "5000", "app:app"]
CMD ["ldap-ui", "--host", "0.0.0.0", "--port", "5000"]
33 changes: 19 additions & 14 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,45 +1,50 @@
.PHONY: debug run clean tidy image push manifest

TAG = latest-$(subst aarch64,arm64,$(shell uname -m))
SITE = backend/ldap_ui/statics

debug: app.py settings.py .env .venv3 dist
.venv3/bin/python3 .venv3/bin/uvicorn --reload --port 5000 app:app

run: app.py settings.py .env .venv3 dist
.venv3/bin/uvicorn --host 0.0.0.0 --port 5000 app:app
debug: .env .venv3 $(SITE)
.venv3/bin/uvicorn --reload --port 5000 ldap_ui.app:app

.env: env.example
cp $< $@

.venv3: requirements.txt
.venv3: pyproject.toml
[ -d $@ ] || python3 -m venv --system-site-packages $@
.venv3/bin/pip3 install -U pip wheel
.venv3/bin/pip3 install -r $<
.venv3/bin/pip3 install -U build pip httpx twine
.venv3/bin/pip3 install --editable .
touch $@

dist: node_modules
dist: .venv3 $(SITE)
.venv3/bin/python3 -m build --wheel

pypi: clean dist
.venv3/bin/twine upload dist/*

$(SITE): node_modules
npm audit
npm run build

node_modules: package.json
npm install
npm audit
touch $@

clean:
rm -rf dist __pycache__
rm -rf build dist $(SITE) __pycache__

tidy: clean
rm -rf .venv3 node_modules

image: clean
docker build -t dnknth/ldap-ui:$(TAG) .
image:
docker build --no-cache -t dnknth/ldap-ui:$(TAG) .

push: image
docker push dnknth/ldap-ui:$(TAG)

manifest: push
manifest:
docker manifest create \
dnknth/ldap-ui \
--amend dnknth/ldap-ui:latest-x86_64 \
--amend dnknth/ldap-ui:latest-arm64
docker manifest push --purge dnknth/ldap-ui
docker compose pull
92 changes: 56 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Simple LDAP editor
# Fast and versatile LDAP editor

This is a *minimal* web interface for LDAP directories. Docker images for `linux/amd64` and `linux/arm64/v8` are [available](https://hub.docker.com/r/dnknth/ldap-ui).

![Screenshot](screenshot.png?raw=true)
![Screenshot](https://github.com/dnknth/ldap-ui/blob/main/screenshot.png?raw=true)

Features:

Expand All @@ -19,68 +19,88 @@ The app always requires authentication, even if the directory permits anonymous

## Usage

### Environment variables

LDAP access is controlled by these environment variables, possibly from a `.env` file:

* `LDAP_URL` (optional): Connection URL, defaults to `ldap:///`.
* `BASE_DN` (required): Search base, e.g. `dc=example,dc=org`.
* `LOGIN_ATTR` (optional): User name attribute, defaults to `uid`.

* `USE_TLS` (optional): Enable TLS, defaults to true for `ldaps` connections. Set it to a non-empty string to force `STARTTLS` on `ldap` connections.
* `INSECURE_TLS` (optional): Do not require a valid server TLS certificate, defaults to false, implies `USE_TLS`.

For finer-grained control, see [settings.py](settings.py).

### Docker

For the impatient: Run it with

docker run -p 127.0.0.1:5000:5000 \
-e LDAP_URL=ldap://your.ldap.server/ \
-e BASE_DN=dc=example,dc=org dnknth/ldap-ui
```shell
docker run -p 127.0.0.1:5000:5000 \
-e LDAP_URL=ldap://your.ldap.server/ \
-e BASE_DN=dc=example,dc=org dnknth/ldap-ui
```

For the even more impatient with `X86_64` machines: Start a demo with
For the even more impatient: Start a demo with

docker compose up -d
```shell
docker compose up -d
```

and go to [http://localhost:5000/](http://localhost:5000/). You are automatically logged in as `Fred Flintstone`.
and go to <http://localhost:5000/>. You are automatically logged in as `Fred Flintstone`.

#### Environment variables
### Pip

LDAP access is controlled by these environment variables, possibly from a `.env` file:
Install the `python-ldap` dependency with your system's package manager.
Otherwise, Pip will try to compile it from source and this will likely fail because it lacks a development environment.

* `LDAP_URL` (optional): Connection URL, defaults to `ldap:///`).
* `BASE_DN` (required): Search base, e.g. `dc=example,dc=org`.
* `LOGIN_ATTR` (optional): User name attribute, defaults to `uid`.
Then install `ldap-ui` in a virtual environment:

* `USE_TLS` (optional): Enable TLS, defaults to true for `ldaps` connections. Set it to a non-empty string to force `STARTTLS` on `ldap` connections.
* `INSECURE_TLS` (optional): Do not require a valid server TLS certificate, defaults to false, implies `USE_TLS`.

For finer-grained control, adjust [settings.py](settings.py).

### Standalone
```shell
python3 -m venv --system-site-packages venv
. venv/bin/activate
pip3 install ldap-ui
```

Copy [env.example](env.example) to `.env`, adjust it and run the app with
Possibly after a shell `rehash`, it is available as `ldap-ui`:

make run
```text
Usage: ldap-ui [OPTIONS]
then head over to [http://localhost:5000/](http://localhost:5000/).
Options:
-b, --base-dn TEXT LDAP base DN. Required unless the BASE_DN
environment variable is set.
-h, --host TEXT Bind socket to this host. [default:
127.0.0.1]
-p, --port INTEGER Bind socket to this port. If 0, an available
port will be picked. [default: 5000]
-l, --log-level [critical|error|warning|info|debug|trace]
Log level. [default: info]
--version Display the current version and exit.
--help Show this message and exit.
```

## Manual installation and configuration
## Development

Prerequisites:

* [GNU make](https://www.gnu.org/software/make/)
* [node.js](https://nodejs.dev) with NPM
* [node.js](https://nodejs.dev) LTS version with NPM
* [Python3](https://www.python.org) ≥ 3.7
* [pip3](https://packaging.python.org/tutorials/installing-packages/)
* [python-ldap](https://pypi.org/project/python-ldap/); To compile the Python module:
* Debian / Ubuntu: `apt-get install libsasl2-dev python-dev libldap2-dev libssl-dev`
* RedHat / CentOS: `yum install python-devel openldap-devel`

`ldap-ui` consists of a Vue UI and a Python backend that roughly translates parts of the LDAP protocol as a stateless ReST API.
`ldap-ui` consists of a Vue frontend and a Python backend that roughly translates a subset of the LDAP protocol to a stateless ReST API.

For the frontend, `npm run build` assembles everything in the `dist` directory.
The result can then be served either via the backend (during development) or statically by any web server (remotely).
For the frontend, `npm run build` assembles everything in `backend/ldap_ui/statics`.

The backend runs locally, always as a separate process. There is an example `systemd` unit in [etc/ldap-ui.service](etc/ldap-ui.service). Check the [Makefile](Makefile) on how to set up a virtual Python environment for it.

Review the configuration in [settings.py](settings.py). It is very short and mostly self-explaining.
Review the configuration in [settings.py](settings.py). It is short and mostly self-explaining.
Most settings can (and should) be overridden by environment variables or settings in a `.env` file; see [env.demo](env.demo) or [env.example](env.example).

The backend exposes port 5000 on localhost which is not reachable remotely. Therefore, for remote access, some web server configuration is needed.
Let's assume that everything should show up under the HTP path `/ldap`:

* The contents of `dist` should be statically served under `/ldap` by the web server.
* The path `/ldap/api` should be proxied to http://localhost:5000/api
The backend can be run locally with `make`, which will also install dependencies and build the frontend if needed.

## Notes

Expand Down Expand Up @@ -113,4 +133,4 @@ Additionally, arbitrary attributes can be searched with an LDAP filter specifica

## Acknowledgements

The Python backend uses [Starlette](https://starlette.io). The UI is built with [Vue.js](https://vuejs.org) and [Tailwind](https://tailwindcss.com/) for CSS. Kudos for the authors of these elegant frameworks!
The Python backend uses [Starlette](https://starlette.io). The UI is built with [Vue.js](https://vuejs.org) and [Tailwind CSS](https://tailwindcss.com/). Kudos to the authors of these elegant frameworks!
1 change: 1 addition & 0 deletions backend/ldap_ui/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "0.9.5"
85 changes: 85 additions & 0 deletions backend/ldap_ui/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import logging

import click
import uvicorn
from uvicorn.config import LOG_LEVELS
from uvicorn.logging import ColourizedFormatter
from uvicorn.main import LEVEL_CHOICES

import ldap_ui

from . import settings


def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> None:
if value:
click.echo(ldap_ui.__version__)
ctx.exit()


@click.command()
@click.option(
"-b",
"--base-dn",
type=str,
default=settings.BASE_DN,
help="LDAP base DN (required). [default: BASE_DN environment variable]",
)
@click.option(
"-h",
"--host",
type=str,
default="127.0.0.1",
help="Bind socket to this host.",
show_default=True,
)
@click.option(
"-p",
"--port",
type=int,
default=5000,
help="Bind socket to this port. If 0, an available port will be picked.",
show_default=True,
)
@click.option(
"-u",
"--ldap-url",
type=str,
help="LDAP directory connection URL. [default: LDAP_URL environment variable or 'ldap:///']",
)
@click.option(
"-l",
"--log-level",
type=LEVEL_CHOICES,
default="info",
help="Log level. [default: info]",
show_default=True,
)
@click.option(
"--version",
is_flag=True,
callback=print_version,
expose_value=False,
is_eager=True,
help="Display the current version and exit.",
)
def main(base_dn, host, port, ldap_url, log_level):
logging.basicConfig(level=LOG_LEVELS[log_level])
rootHandler = logging.getLogger().handlers[0]
rootHandler.setFormatter(ColourizedFormatter(fmt="%(levelprefix)s %(message)s"))

if base_dn is not None:
settings.BASE_DN = base_dn

if ldap_url is not None:
settings.LDAP_URL = ldap_url

uvicorn.run(
"ldap_ui.app:app",
host=host,
port=port,
)


if __name__ == "__main__":
main()
Loading

0 comments on commit d1242c2

Please sign in to comment.