Skip to content

Commit 9c02d22

Browse files
committed
Reformat + package
1 parent 1d70132 commit 9c02d22

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1856
-1776
lines changed

.github/workflows/ci.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: CI
2+
3+
on: [push]
4+
5+
jobs:
6+
build:
7+
runs-on: ubuntu-latest
8+
strategy:
9+
matrix:
10+
python-version: ["3.8"]
11+
12+
steps:
13+
- uses: actions/checkout@v3
14+
- name: Set up Python ${{ matrix.python-version }}
15+
uses: actions/setup-python@v4
16+
with:
17+
python-version: ${{ matrix.python-version }}
18+
- name: Install dependencies
19+
run: |
20+
apt install python-ldap
21+
python -m pip install --upgrade pip
22+
python -m pip install . unittest-xml-reporting
23+
- name: Run tests
24+
run: |
25+
python -m xmlrunner discover ldap_ui
26+
- name: Publish test results
27+
uses: dorny/test-reporter@v1
28+
with:
29+
name: Tests
30+
path: "TEST-*.xml"
31+
reporter: java-junit

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
.venv*
33
.activate
44
__pycache__
5+
*.egg-info
56

67
.DS_Store
78
node_modules
8-
/dist
9+
build
10+
dist
11+
statics
912

1013
# local env files
1114
.env*

.vscode/launch.json

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,15 @@
11
{
2-
// Use IntelliSense to learn about possible attributes.
3-
// Hover to view descriptions of existing attributes.
4-
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5-
"version": "0.2.0",
6-
"configurations": [
7-
{
8-
"name": "Python Debugger: Starlette",
9-
"type": "debugpy",
10-
"request": "launch",
11-
"module": "uvicorn",
12-
"args": [
13-
"app:app",
14-
"--port",
15-
"5000",
16-
"--reload"
17-
]
18-
}
19-
]
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"name": "Debug: Backend",
9+
"type": "debugpy",
10+
"request": "launch",
11+
"module": "uvicorn",
12+
"args": ["app:app", "--port", "5000", "--reload"]
13+
}
14+
]
2015
}

.vscode/settings.json

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,13 @@
11
{
22
"css.customData": [".vscode/tailwind.json"],
3+
"editor.formatOnSave": true,
34
"[python]": {
4-
"editor.formatOnSave": true,
55
"editor.defaultFormatter": "charliermarsh.ruff",
66
"editor.codeActionsOnSave": {
77
"source.organizeImports": "explicit"
88
}
99
},
10-
"python.testing.unittestArgs": [
11-
"-v",
12-
"-s",
13-
".",
14-
"-p",
15-
"*test.py"
16-
],
1710
"python.testing.pytestEnabled": false,
18-
"python.testing.unittestEnabled": true
11+
"python.testing.unittestEnabled": true,
12+
"python.testing.unittestArgs": ["-v", "-s", "./tests", "-p", "*_test.py"]
1913
}

Dockerfile

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
1-
FROM node:lts-alpine AS builder
2-
COPY . /app
3-
WORKDIR /app
4-
RUN npm audit && npm i && npm run build
5-
61
FROM alpine:3
7-
COPY --from=builder /app/dist /app/dist
8-
RUN apk add --no-cache python3 py3-pip py3-pyldap py3-pytoml \
9-
&& pip3 install --break-system-packages python-multipart starlette uvicorn
10-
COPY app.py ldap_api.py ldap_helpers.py schema.py settings.py /app/
2+
RUN apk add --no-cache py3-pip py3-pyldap
3+
RUN pip3 install --break-system-packages ldap_ui
114

12-
WORKDIR /app
135
EXPOSE 5000
14-
CMD ["/usr/bin/uvicorn", "--host", "0.0.0.0", "--port", "5000", "app:app"]
6+
CMD ["ldap-ui"]

Makefile

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,47 @@
11
.PHONY: debug run clean tidy image push manifest
22

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

5-
debug: app.py settings.py .env .venv3 dist
6-
.venv3/bin/python3 .venv3/bin/uvicorn --reload --port 5000 app:app
7-
8-
run: app.py settings.py .env .venv3 dist
9-
.venv3/bin/uvicorn --host 0.0.0.0 --port 5000 app:app
6+
debug: .env .venv3 $(SITE)
7+
.venv3/bin/uvicorn --reload --port 5000 ldap_ui.app:app
108

119
.env: env.example
1210
cp $< $@
1311

14-
.venv3: requirements.txt
12+
.venv3: pyproject.toml
1513
[ -d $@ ] || python3 -m venv --system-site-packages $@
16-
.venv3/bin/pip3 install -U pip wheel
17-
.venv3/bin/pip3 install -r $<
14+
.venv3/bin/pip3 install -U build pip httpx twine
15+
.venv3/bin/pip3 install --editable .
1816
touch $@
1917

20-
dist: node_modules
18+
dist: .venv3 $(SITE)
19+
.venv3/bin/python3 -m build --wheel
20+
21+
pypi: clean dist
22+
.venv3/bin/twine upload dist/*
23+
24+
$(SITE): node_modules
25+
npm audit
2126
npm run build
2227

2328
node_modules: package.json
2429
npm install
25-
npm audit
2630
touch $@
2731

2832
clean:
29-
rm -rf dist __pycache__
33+
rm -rf build dist $(SITE) __pycache__
3034

3135
tidy: clean
3236
rm -rf .venv3 node_modules
3337

34-
image: clean
38+
image:
3539
docker build -t dnknth/ldap-ui:$(TAG) .
3640

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

40-
manifest: push
44+
manifest:
4145
docker manifest create \
4246
dnknth/ldap-ui \
4347
--amend dnknth/ldap-ui:latest-x86_64 \

README.md

Lines changed: 41 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
# Simple LDAP editor
1+
# Fast and versatile LDAP editor
22

33
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).
44

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

77
Features:
88

@@ -19,68 +19,72 @@ The app always requires authentication, even if the directory permits anonymous
1919

2020
## Usage
2121

22-
### Docker
22+
### Environment variables
2323

24-
For the impatient: Run it with
24+
LDAP access is controlled by these environment variables, possibly from a `.env` file:
25+
26+
* `LDAP_URL` (optional): Connection URL, defaults to `ldap:///`.
27+
* `BASE_DN` (required): Search base, e.g. `dc=example,dc=org`.
28+
* `LOGIN_ATTR` (optional): User name attribute, defaults to `uid`.
29+
30+
* `USE_TLS` (optional): Enable TLS, defaults to true for `ldaps` connections. Set it to a non-empty string to force `STARTTLS` on `ldap` connections.
31+
* `INSECURE_TLS` (optional): Do not require a valid server TLS certificate, defaults to false, implies `USE_TLS`.
2532

26-
docker run -p 127.0.0.1:5000:5000 \
27-
-e LDAP_URL=ldap://your.ldap.server/ \
28-
-e BASE_DN=dc=example,dc=org dnknth/ldap-ui
33+
For finer-grained control, see [settings.py](settings.py).
2934

30-
For the even more impatient with `X86_64` machines: Start a demo with
35+
### Docker
3136

32-
docker compose up -d
37+
For the impatient: Run it with
3338

34-
and go to [http://localhost:5000/](http://localhost:5000/). You are automatically logged in as `Fred Flintstone`.
39+
```shell
40+
docker run -p 127.0.0.1:5000:5000 \
41+
-e LDAP_URL=ldap://your.ldap.server/ \
42+
-e BASE_DN=dc=example,dc=org dnknth/ldap-ui
43+
```
3544

36-
#### Environment variables
45+
For the even more impatient: Start a demo with
3746

38-
LDAP access is controlled by these environment variables, possibly from a `.env` file:
47+
```shell
48+
docker compose up -d
49+
```
3950

40-
* `LDAP_URL` (optional): Connection URL, defaults to `ldap:///`).
41-
* `BASE_DN` (required): Search base, e.g. `dc=example,dc=org`.
42-
* `LOGIN_ATTR` (optional): User name attribute, defaults to `uid`.
51+
and go to <http://localhost:5000/>. You are automatically logged in as `Fred Flintstone`.
4352

44-
* `USE_TLS` (optional): Enable TLS, defaults to true for `ldaps` connections. Set it to a non-empty string to force `STARTTLS` on `ldap` connections.
45-
* `INSECURE_TLS` (optional): Do not require a valid server TLS certificate, defaults to false, implies `USE_TLS`.
46-
47-
For finer-grained control, adjust [settings.py](settings.py).
53+
### Pip
4854

49-
### Standalone
55+
Install the `python-ldap` dependency with your system's package manager.
56+
Otherwise, Pip will try to compile it from source and this will likely fail because it lacks a development environment.
5057

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

53-
make run
60+
```shell
61+
python3 -m venv --system-site-packages venv
62+
. venv/bin/activate
63+
pip3 install ldap-ui
64+
```
5465

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

57-
## Manual installation and configuration
68+
## Development
5869

5970
Prerequisites:
6071

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

69-
`ldap-ui` consists of a Vue UI and a Python backend that roughly translates parts of the LDAP protocol as a stateless ReST API.
80+
`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.
7081

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

74-
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.
75-
76-
Review the configuration in [settings.py](settings.py). It is very short and mostly self-explaining.
84+
Review the configuration in [settings.py](settings.py). It is short and mostly self-explaining.
7785
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).
7886

79-
The backend exposes port 5000 on localhost which is not reachable remotely. Therefore, for remote access, some web server configuration is needed.
80-
Let's assume that everything should show up under the HTP path `/ldap`:
81-
82-
* The contents of `dist` should be statically served under `/ldap` by the web server.
83-
* The path `/ldap/api` should be proxied to http://localhost:5000/api
87+
The backend can be run locally with `make`, which will also install dependencies and build the frontend if needed.
8488

8589
## Notes
8690

@@ -113,4 +117,4 @@ Additionally, arbitrary attributes can be searched with an LDAP filter specifica
113117

114118
## Acknowledgements
115119

116-
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!
120+
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!

backend/ldap_ui/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import uvicorn
2+
3+
from . import settings
4+
5+
__version__ = "0.9.1"
6+
7+
8+
def run():
9+
uvicorn.run(
10+
"ldap_ui.app:app",
11+
log_level="info",
12+
host="127.0.0.1" if settings.DEBUG else None,
13+
port=5000,
14+
)

backend/ldap_ui/__main__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from ldap_ui import run
2+
3+
if __name__ == "__main__":
4+
run()

app.py renamed to backend/ldap_ui/app.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from typing import AsyncGenerator
1515

1616
import ldap
17+
import uvicorn
1718
from ldap.schema import SubSchema
1819
from starlette.applications import Starlette
1920
from starlette.authentication import (
@@ -32,9 +33,9 @@
3233
from starlette.routing import Mount
3334
from starlette.staticfiles import StaticFiles
3435

35-
import settings
36-
from ldap_api import api
37-
from ldap_helpers import WITH_OPERATIONAL_ATTRS, empty, ldap_connect, unique
36+
from . import settings
37+
from .ldap_api import api
38+
from .ldap_helpers import WITH_OPERATIONAL_ATTRS, empty, ldap_connect, unique
3839

3940
# Force authentication
4041
UNAUTHORIZED = Response(
@@ -103,7 +104,7 @@ def ldap_exception_message(exc: ldap.LDAPError) -> str:
103104
class LdapUser(SimpleUser):
104105
"LDAP credentials"
105106

106-
def __init__(self, username: str, password: str) -> None:
107+
def __init__(self, username: str, password: str):
107108
super().__init__(username)
108109
self.password = password
109110

@@ -143,7 +144,7 @@ async def dispatch(
143144
return response
144145

145146

146-
async def http_exception(request: Request, exc: HTTPException):
147+
async def http_exception(_request: Request, exc: HTTPException):
147148
"Send error responses"
148149
assert exc.status_code >= 400
149150
return PlainTextResponse(
@@ -153,7 +154,7 @@ async def http_exception(request: Request, exc: HTTPException):
153154
)
154155

155156

156-
async def forbidden(request: Request, exc: ldap.LDAPError):
157+
async def forbidden(_request: Request, exc: ldap.LDAPError):
157158
"HTTP 403 Forbidden"
158159
return PlainTextResponse(ldap_exception_message(exc), status_code=403)
159160

@@ -190,6 +191,6 @@ async def lifespan(app):
190191
),
191192
routes=[
192193
Mount("/api", routes=api.routes),
193-
Mount("/", StaticFiles(directory="dist", html=True)),
194+
Mount("/", StaticFiles(packages=["ldap_ui"], html=True)),
194195
],
195196
)

0 commit comments

Comments
 (0)