Skip to content

Commit

Permalink
Reformat + package
Browse files Browse the repository at this point in the history
  • Loading branch information
dnknth committed Oct 26, 2024
1 parent 1d70132 commit b104769
Show file tree
Hide file tree
Showing 48 changed files with 1,855 additions and 1,776 deletions.
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: CI

on: [push]

jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8"]

steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install . unittest-xml-reporting
- name: Run tests
run: |
python -m xmlrunner discover ldap_ui
- name: Publish test results
uses: dorny/test-reporter@v1
with:
name: Tests
path: "TEST-*.xml"
reporter: java-junit
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"]
30 changes: 17 additions & 13 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,43 +1,47 @@
.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
image:
docker build -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 \
Expand Down
78 changes: 41 additions & 37 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,72 @@ The app always requires authentication, even if the directory permits anonymous

## Usage

### Docker
### Environment variables

For the impatient: Run it with
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`.

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 finer-grained control, see [settings.py](settings.py).

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

docker compose up -d
For the impatient: Run it with

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

#### Environment variables
For the even more impatient: Start a demo with

LDAP access is controlled by these environment variables, possibly from a `.env` file:
```shell
docker compose up -d
```

* `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`.
and go to <http://localhost:5000/>. You are automatically logged in as `Fred Flintstone`.

* `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).
### Pip

### Standalone
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.

Copy [env.example](env.example) to `.env`, adjust it and run the app with
Then install `ldap-ui` in a virtual environment:

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

then head over to [http://localhost:5000/](http://localhost:5000/).
Possibly after a shell `rehash`, it is available as `ldap-ui`.

## 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 +117,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!
14 changes: 14 additions & 0 deletions backend/ldap_ui/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import uvicorn

from . import settings

__version__ = "0.9.1"


def run():
uvicorn.run(
"ldap_ui.app:app",
log_level="info",
host="127.0.0.1" if settings.DEBUG else None,
port=5000,
)
4 changes: 4 additions & 0 deletions backend/ldap_ui/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from ldap_ui import run

if __name__ == "__main__":
run()
15 changes: 8 additions & 7 deletions app.py → backend/ldap_ui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from typing import AsyncGenerator

import ldap
import uvicorn
from ldap.schema import SubSchema
from starlette.applications import Starlette
from starlette.authentication import (
Expand All @@ -32,9 +33,9 @@
from starlette.routing import Mount
from starlette.staticfiles import StaticFiles

import settings
from ldap_api import api
from ldap_helpers import WITH_OPERATIONAL_ATTRS, empty, ldap_connect, unique
from . import settings
from .ldap_api import api
from .ldap_helpers import WITH_OPERATIONAL_ATTRS, empty, ldap_connect, unique

# Force authentication
UNAUTHORIZED = Response(
Expand Down Expand Up @@ -103,7 +104,7 @@ def ldap_exception_message(exc: ldap.LDAPError) -> str:
class LdapUser(SimpleUser):
"LDAP credentials"

def __init__(self, username: str, password: str) -> None:
def __init__(self, username: str, password: str):
super().__init__(username)
self.password = password

Expand Down Expand Up @@ -143,7 +144,7 @@ async def dispatch(
return response


async def http_exception(request: Request, exc: HTTPException):
async def http_exception(_request: Request, exc: HTTPException):
"Send error responses"
assert exc.status_code >= 400
return PlainTextResponse(
Expand All @@ -153,7 +154,7 @@ async def http_exception(request: Request, exc: HTTPException):
)


async def forbidden(request: Request, exc: ldap.LDAPError):
async def forbidden(_request: Request, exc: ldap.LDAPError):
"HTTP 403 Forbidden"
return PlainTextResponse(ldap_exception_message(exc), status_code=403)

Expand Down Expand Up @@ -190,6 +191,6 @@ async def lifespan(app):
),
routes=[
Mount("/api", routes=api.routes),
Mount("/", StaticFiles(directory="dist", html=True)),
Mount("/", StaticFiles(packages=["ldap_ui"], html=True)),
],
)
Loading

0 comments on commit b104769

Please sign in to comment.