ldapgate 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ldapgate-0.1.1/.gitignore +46 -0
- ldapgate-0.1.1/PKG-INFO +140 -0
- ldapgate-0.1.1/README.md +116 -0
- ldapgate-0.1.1/assets/logo-full-dark.svg +28 -0
- ldapgate-0.1.1/assets/logo-full.svg +28 -0
- ldapgate-0.1.1/assets/logo.svg +25 -0
- ldapgate-0.1.1/docker/Dockerfile +15 -0
- ldapgate-0.1.1/docker/alice.ldif +16 -0
- ldapgate-0.1.1/docker/docker-compose.yml +31 -0
- ldapgate-0.1.1/docker/ldapgate.local.yaml +19 -0
- ldapgate-0.1.1/ldapgate/__init__.py +19 -0
- ldapgate-0.1.1/ldapgate/cli.py +121 -0
- ldapgate-0.1.1/ldapgate/config.py +134 -0
- ldapgate-0.1.1/ldapgate/ldap.py +181 -0
- ldapgate-0.1.1/ldapgate/middleware.py +113 -0
- ldapgate-0.1.1/ldapgate/proxy.py +396 -0
- ldapgate-0.1.1/ldapgate/sessions.py +52 -0
- ldapgate-0.1.1/ldapgate/templates/login.html +232 -0
- ldapgate-0.1.1/ldapgate.yaml.example +78 -0
- ldapgate-0.1.1/pyproject.toml +39 -0
- ldapgate-0.1.1/tests/__init__.py +1 -0
- ldapgate-0.1.1/tests/test_config.py +145 -0
- ldapgate-0.1.1/tests/test_ldap.py +95 -0
- ldapgate-0.1.1/tests/test_sessions.py +72 -0
- ldapgate-0.1.1/uv.lock +1023 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
pip-wheel-metadata/
|
|
20
|
+
share/python-wheels/
|
|
21
|
+
*.egg-info/
|
|
22
|
+
.installed.cfg
|
|
23
|
+
*.egg
|
|
24
|
+
MANIFEST
|
|
25
|
+
.venv
|
|
26
|
+
.venv/
|
|
27
|
+
|
|
28
|
+
# pytest
|
|
29
|
+
.pytest_cache/
|
|
30
|
+
.coverage
|
|
31
|
+
htmlcov/
|
|
32
|
+
|
|
33
|
+
# IDE
|
|
34
|
+
.vscode/
|
|
35
|
+
.idea/
|
|
36
|
+
*.swp
|
|
37
|
+
*.swo
|
|
38
|
+
*~
|
|
39
|
+
|
|
40
|
+
# OS
|
|
41
|
+
.DS_Store
|
|
42
|
+
Thumbs.db
|
|
43
|
+
|
|
44
|
+
# Local testing
|
|
45
|
+
ldapgate.yaml
|
|
46
|
+
memory/
|
ldapgate-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ldapgate
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Lightweight LDAP/AD authentication proxy and FastAPI middleware
|
|
5
|
+
Author-email: Anudeep D <anudeep@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: click>=8.1.0
|
|
9
|
+
Requires-Dist: fastapi>=0.111.0
|
|
10
|
+
Requires-Dist: httpx>=0.27
|
|
11
|
+
Requires-Dist: itsdangerous>=2.2.0
|
|
12
|
+
Requires-Dist: jinja2>=3.1.0
|
|
13
|
+
Requires-Dist: ldap3>=2.9.1
|
|
14
|
+
Requires-Dist: pydantic-settings>=2.0
|
|
15
|
+
Requires-Dist: pydantic>=2.7.0
|
|
16
|
+
Requires-Dist: python-multipart>=0.0.9
|
|
17
|
+
Requires-Dist: pyyaml>=6.0
|
|
18
|
+
Requires-Dist: uvicorn[standard]>=0.29.0
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: httpx[http2]>=0.27; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
<p align="center">
|
|
26
|
+
<img src="https://raw.githubusercontent.com/anudeepd/ldapgate/main/assets/logo.svg" alt="LDAPGate" width="120"/>
|
|
27
|
+
</p>
|
|
28
|
+
|
|
29
|
+
<h1 align="center">LDAPGate</h1>
|
|
30
|
+
|
|
31
|
+
<p align="center">Lightweight LDAP/AD authentication gateway for Python web apps. Install it, configure it, done.</p>
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- **Two deployment modes** — standalone reverse proxy or drop-in FastAPI middleware
|
|
36
|
+
- **Pure Python LDAP** — no OS-level libs required, uses `ldap3`
|
|
37
|
+
- **Signed cookie sessions** — stateless, no server-side session storage
|
|
38
|
+
- **OpenLDAP and Active Directory** — `uid=` and `sAMAccountName=` out of the box
|
|
39
|
+
- **Optional group gating** — restrict access to members of a specific LDAP group
|
|
40
|
+
- **Header injection** — injects `X-Forwarded-User` for apps that support it
|
|
41
|
+
- **Bundled login form** — responsive, dark/light mode, works air-gapped
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install ldapgate
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Config file
|
|
50
|
+
|
|
51
|
+
Both modes share the same `ldapgate.yaml`:
|
|
52
|
+
|
|
53
|
+
```yaml
|
|
54
|
+
ldap:
|
|
55
|
+
url: ldaps://dc.example.com:636
|
|
56
|
+
bind_dn: CN=svc,CN=Users,DC=example,DC=com
|
|
57
|
+
bind_password: secret
|
|
58
|
+
base_dn: DC=example,DC=com
|
|
59
|
+
user_filter: "(sAMAccountName={username})" # AD; OpenLDAP: (uid={username})
|
|
60
|
+
group_dn: CN=app-users,CN=Users,DC=example,DC=com # optional
|
|
61
|
+
|
|
62
|
+
proxy:
|
|
63
|
+
listen_host: 0.0.0.0
|
|
64
|
+
listen_port: 9000
|
|
65
|
+
backend_url: http://localhost:8080
|
|
66
|
+
secret_key: change-me-to-something-random
|
|
67
|
+
session_ttl: 3600
|
|
68
|
+
user_header: X-Forwarded-User
|
|
69
|
+
login_path: /_auth/login
|
|
70
|
+
app_name: MyApp
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
All settings can also be provided via environment variables with `__` separators (e.g. `LDAP__URL`, `PROXY__SECRET_KEY`).
|
|
74
|
+
|
|
75
|
+
## Mode 1 — Standalone Reverse Proxy
|
|
76
|
+
|
|
77
|
+
Run ldapgate as a standalone process in front of any app. Only authenticated requests are forwarded to the backend.
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
Browser → ldapgate :9000 → backend app :8080
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
ldapgate serve --config ldapgate.yaml
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Example: copyparty**
|
|
88
|
+
|
|
89
|
+
Start copyparty with IDP header auth and point its logout at ldapgate:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
copyparty -p 8080 --idp-h-usr X-Forwarded-User --idp-logout /_auth/logout -v ~/Documents:/:rw
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
copyparty trusts the `X-Forwarded-User` header injected by ldapgate, and its logout button redirects to `/_auth/logout` which clears the ldapgate session before sending the user back to the login page.
|
|
96
|
+
|
|
97
|
+
## Mode 2 — FastAPI Middleware
|
|
98
|
+
|
|
99
|
+
Drop ldapgate auth directly into an existing FastAPI app — no separate process needed.
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from fastapi import FastAPI
|
|
103
|
+
from ldapgate.config import load_config
|
|
104
|
+
from ldapgate.middleware import add_ldap_auth
|
|
105
|
+
|
|
106
|
+
app = FastAPI()
|
|
107
|
+
config = load_config("ldapgate.yaml")
|
|
108
|
+
add_ldap_auth(app, config)
|
|
109
|
+
|
|
110
|
+
@app.get("/api/data")
|
|
111
|
+
async def data(request):
|
|
112
|
+
return {"user": request.state.user} # authenticated username
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Example: lagun** — see [lagun](https://github.com/anudeepd/lagun) and [torrus](https://github.com/anudeepd/torrus) for real-world integrations.
|
|
116
|
+
|
|
117
|
+
## CLI Options
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
ldapgate serve --config PATH Path to ldapgate.yaml [default: ldapgate.yaml]
|
|
121
|
+
--host TEXT Override listen host
|
|
122
|
+
--port INTEGER Override listen port
|
|
123
|
+
--backend TEXT Override backend URL
|
|
124
|
+
--reload Enable auto-reload (dev)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Development
|
|
128
|
+
|
|
129
|
+
Requires [uv](https://github.com/astral-sh/uv).
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
git clone https://github.com/anudeepd/ldapgate
|
|
133
|
+
cd ldapgate
|
|
134
|
+
uv sync
|
|
135
|
+
pytest tests/
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
MIT
|
ldapgate-0.1.1/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/anudeepd/ldapgate/main/assets/logo.svg" alt="LDAPGate" width="120"/>
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">LDAPGate</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">Lightweight LDAP/AD authentication gateway for Python web apps. Install it, configure it, done.</p>
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Two deployment modes** — standalone reverse proxy or drop-in FastAPI middleware
|
|
12
|
+
- **Pure Python LDAP** — no OS-level libs required, uses `ldap3`
|
|
13
|
+
- **Signed cookie sessions** — stateless, no server-side session storage
|
|
14
|
+
- **OpenLDAP and Active Directory** — `uid=` and `sAMAccountName=` out of the box
|
|
15
|
+
- **Optional group gating** — restrict access to members of a specific LDAP group
|
|
16
|
+
- **Header injection** — injects `X-Forwarded-User` for apps that support it
|
|
17
|
+
- **Bundled login form** — responsive, dark/light mode, works air-gapped
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install ldapgate
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Config file
|
|
26
|
+
|
|
27
|
+
Both modes share the same `ldapgate.yaml`:
|
|
28
|
+
|
|
29
|
+
```yaml
|
|
30
|
+
ldap:
|
|
31
|
+
url: ldaps://dc.example.com:636
|
|
32
|
+
bind_dn: CN=svc,CN=Users,DC=example,DC=com
|
|
33
|
+
bind_password: secret
|
|
34
|
+
base_dn: DC=example,DC=com
|
|
35
|
+
user_filter: "(sAMAccountName={username})" # AD; OpenLDAP: (uid={username})
|
|
36
|
+
group_dn: CN=app-users,CN=Users,DC=example,DC=com # optional
|
|
37
|
+
|
|
38
|
+
proxy:
|
|
39
|
+
listen_host: 0.0.0.0
|
|
40
|
+
listen_port: 9000
|
|
41
|
+
backend_url: http://localhost:8080
|
|
42
|
+
secret_key: change-me-to-something-random
|
|
43
|
+
session_ttl: 3600
|
|
44
|
+
user_header: X-Forwarded-User
|
|
45
|
+
login_path: /_auth/login
|
|
46
|
+
app_name: MyApp
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
All settings can also be provided via environment variables with `__` separators (e.g. `LDAP__URL`, `PROXY__SECRET_KEY`).
|
|
50
|
+
|
|
51
|
+
## Mode 1 — Standalone Reverse Proxy
|
|
52
|
+
|
|
53
|
+
Run ldapgate as a standalone process in front of any app. Only authenticated requests are forwarded to the backend.
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
Browser → ldapgate :9000 → backend app :8080
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
ldapgate serve --config ldapgate.yaml
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Example: copyparty**
|
|
64
|
+
|
|
65
|
+
Start copyparty with IDP header auth and point its logout at ldapgate:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
copyparty -p 8080 --idp-h-usr X-Forwarded-User --idp-logout /_auth/logout -v ~/Documents:/:rw
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
copyparty trusts the `X-Forwarded-User` header injected by ldapgate, and its logout button redirects to `/_auth/logout` which clears the ldapgate session before sending the user back to the login page.
|
|
72
|
+
|
|
73
|
+
## Mode 2 — FastAPI Middleware
|
|
74
|
+
|
|
75
|
+
Drop ldapgate auth directly into an existing FastAPI app — no separate process needed.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from fastapi import FastAPI
|
|
79
|
+
from ldapgate.config import load_config
|
|
80
|
+
from ldapgate.middleware import add_ldap_auth
|
|
81
|
+
|
|
82
|
+
app = FastAPI()
|
|
83
|
+
config = load_config("ldapgate.yaml")
|
|
84
|
+
add_ldap_auth(app, config)
|
|
85
|
+
|
|
86
|
+
@app.get("/api/data")
|
|
87
|
+
async def data(request):
|
|
88
|
+
return {"user": request.state.user} # authenticated username
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Example: lagun** — see [lagun](https://github.com/anudeepd/lagun) and [torrus](https://github.com/anudeepd/torrus) for real-world integrations.
|
|
92
|
+
|
|
93
|
+
## CLI Options
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
ldapgate serve --config PATH Path to ldapgate.yaml [default: ldapgate.yaml]
|
|
97
|
+
--host TEXT Override listen host
|
|
98
|
+
--port INTEGER Override listen port
|
|
99
|
+
--backend TEXT Override backend URL
|
|
100
|
+
--reload Enable auto-reload (dev)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Development
|
|
104
|
+
|
|
105
|
+
Requires [uv](https://github.com/astral-sh/uv).
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
git clone https://github.com/anudeepd/ldapgate
|
|
109
|
+
cd ldapgate
|
|
110
|
+
uv sync
|
|
111
|
+
pytest tests/
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<svg viewBox="0 0 420 120" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<!-- Icon background -->
|
|
3
|
+
<rect x="4" y="4" width="112" height="112" rx="22" fill="#eff6ff"/>
|
|
4
|
+
|
|
5
|
+
<!-- Gate posts -->
|
|
6
|
+
<line x1="36" y1="94" x2="36" y2="52" stroke="#1d4ed8" stroke-width="5" stroke-linecap="round"/>
|
|
7
|
+
<line x1="84" y1="94" x2="84" y2="52" stroke="#1d4ed8" stroke-width="5" stroke-linecap="round"/>
|
|
8
|
+
|
|
9
|
+
<!-- Gate arch -->
|
|
10
|
+
<path d="M 36 52 Q 60 26 84 52" fill="none" stroke="#1d4ed8" stroke-width="5" stroke-linecap="round"/>
|
|
11
|
+
|
|
12
|
+
<!-- Gate crossbar -->
|
|
13
|
+
<line x1="36" y1="72" x2="84" y2="72" stroke="#1d4ed8" stroke-width="3.5" stroke-linecap="round"/>
|
|
14
|
+
|
|
15
|
+
<!-- Padlock shackle -->
|
|
16
|
+
<path d="M 51 72 L 51 64 Q 51 57 60 57 Q 69 57 69 64 L 69 72"
|
|
17
|
+
fill="none" stroke="#3b82f6" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
|
18
|
+
|
|
19
|
+
<!-- Padlock body -->
|
|
20
|
+
<rect x="47" y="70" width="26" height="20" rx="4" fill="#eff6ff" stroke="#3b82f6" stroke-width="3"/>
|
|
21
|
+
|
|
22
|
+
<!-- Keyhole -->
|
|
23
|
+
<circle cx="60" cy="78" r="2.5" fill="#3b82f6"/>
|
|
24
|
+
<line x1="60" y1="80" x2="60" y2="84" stroke="#3b82f6" stroke-width="2" stroke-linecap="round"/>
|
|
25
|
+
|
|
26
|
+
<!-- Wordmark -->
|
|
27
|
+
<text x="136" y="74" font-family="'Inter', 'Helvetica Neue', sans-serif" font-size="52" font-weight="600" letter-spacing="-1" fill="#1e3a8a">LDAPGate</text>
|
|
28
|
+
</svg>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<svg viewBox="0 0 420 120" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<!-- Icon background -->
|
|
3
|
+
<rect x="4" y="4" width="112" height="112" rx="22" fill="#eff6ff"/>
|
|
4
|
+
|
|
5
|
+
<!-- Gate posts -->
|
|
6
|
+
<line x1="36" y1="94" x2="36" y2="52" stroke="#1d4ed8" stroke-width="5" stroke-linecap="round"/>
|
|
7
|
+
<line x1="84" y1="94" x2="84" y2="52" stroke="#1d4ed8" stroke-width="5" stroke-linecap="round"/>
|
|
8
|
+
|
|
9
|
+
<!-- Gate arch -->
|
|
10
|
+
<path d="M 36 52 Q 60 26 84 52" fill="none" stroke="#1d4ed8" stroke-width="5" stroke-linecap="round"/>
|
|
11
|
+
|
|
12
|
+
<!-- Gate crossbar -->
|
|
13
|
+
<line x1="36" y1="72" x2="84" y2="72" stroke="#1d4ed8" stroke-width="3.5" stroke-linecap="round"/>
|
|
14
|
+
|
|
15
|
+
<!-- Padlock shackle -->
|
|
16
|
+
<path d="M 51 72 L 51 64 Q 51 57 60 57 Q 69 57 69 64 L 69 72"
|
|
17
|
+
fill="none" stroke="#3b82f6" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
|
18
|
+
|
|
19
|
+
<!-- Padlock body -->
|
|
20
|
+
<rect x="47" y="70" width="26" height="20" rx="4" fill="#eff6ff" stroke="#3b82f6" stroke-width="3"/>
|
|
21
|
+
|
|
22
|
+
<!-- Keyhole -->
|
|
23
|
+
<circle cx="60" cy="78" r="2.5" fill="#3b82f6"/>
|
|
24
|
+
<line x1="60" y1="80" x2="60" y2="84" stroke="#3b82f6" stroke-width="2" stroke-linecap="round"/>
|
|
25
|
+
|
|
26
|
+
<!-- Wordmark -->
|
|
27
|
+
<text x="136" y="74" font-family="'Inter', 'Helvetica Neue', sans-serif" font-size="52" font-weight="600" letter-spacing="-1" fill="#dbeafe">LDAPGate</text>
|
|
28
|
+
</svg>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<!-- Rounded square background -->
|
|
3
|
+
<rect x="6" y="6" width="188" height="188" rx="36" fill="#eff6ff"/>
|
|
4
|
+
|
|
5
|
+
<!-- Gate posts -->
|
|
6
|
+
<line x1="58" y1="158" x2="58" y2="86" stroke="#1d4ed8" stroke-width="9" stroke-linecap="round"/>
|
|
7
|
+
<line x1="142" y1="158" x2="142" y2="86" stroke="#1d4ed8" stroke-width="9" stroke-linecap="round"/>
|
|
8
|
+
|
|
9
|
+
<!-- Gate arch -->
|
|
10
|
+
<path d="M 58 86 Q 100 40 142 86" fill="none" stroke="#1d4ed8" stroke-width="9" stroke-linecap="round"/>
|
|
11
|
+
|
|
12
|
+
<!-- Gate crossbar -->
|
|
13
|
+
<line x1="58" y1="120" x2="142" y2="120" stroke="#1d4ed8" stroke-width="6" stroke-linecap="round"/>
|
|
14
|
+
|
|
15
|
+
<!-- Padlock shackle -->
|
|
16
|
+
<path d="M 88 120 L 88 107 Q 88 94 100 94 Q 112 94 112 107 L 112 120"
|
|
17
|
+
fill="none" stroke="#3b82f6" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
18
|
+
|
|
19
|
+
<!-- Padlock body (fill covers crossbar behind it) -->
|
|
20
|
+
<rect x="83" y="117" width="34" height="27" rx="5" fill="#eff6ff" stroke="#3b82f6" stroke-width="5"/>
|
|
21
|
+
|
|
22
|
+
<!-- Keyhole -->
|
|
23
|
+
<circle cx="100" cy="128" r="3.5" fill="#3b82f6"/>
|
|
24
|
+
<line x1="100" y1="131" x2="100" y2="137" stroke="#3b82f6" stroke-width="3.5" stroke-linecap="round"/>
|
|
25
|
+
</svg>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
FROM python:3.12-slim
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
RUN pip install uv
|
|
6
|
+
|
|
7
|
+
COPY pyproject.toml uv.lock README.md ./
|
|
8
|
+
COPY ldapgate/ ./ldapgate/
|
|
9
|
+
COPY assets/ ./assets/
|
|
10
|
+
|
|
11
|
+
RUN uv pip install --system .
|
|
12
|
+
|
|
13
|
+
EXPOSE 9000
|
|
14
|
+
|
|
15
|
+
CMD ["ldapgate", "serve", "--config", "/app/ldapgate.yaml"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
dn: ou=users,dc=example,dc=org
|
|
2
|
+
objectClass: organizationalUnit
|
|
3
|
+
ou: users
|
|
4
|
+
|
|
5
|
+
dn: uid=alice,ou=users,dc=example,dc=org
|
|
6
|
+
objectClass: inetOrgPerson
|
|
7
|
+
objectClass: posixAccount
|
|
8
|
+
objectClass: shadowAccount
|
|
9
|
+
uid: alice
|
|
10
|
+
cn: Alice
|
|
11
|
+
sn: Test
|
|
12
|
+
mail: alice@example.org
|
|
13
|
+
userPassword: alice123
|
|
14
|
+
uidNumber: 1000
|
|
15
|
+
gidNumber: 1000
|
|
16
|
+
homeDirectory: /home/alice
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
services:
|
|
2
|
+
|
|
3
|
+
openldap:
|
|
4
|
+
image: osixia/openldap:1.5.0
|
|
5
|
+
environment:
|
|
6
|
+
LDAP_ORGANISATION: "Example"
|
|
7
|
+
LDAP_DOMAIN: "example.org"
|
|
8
|
+
LDAP_ADMIN_PASSWORD: "admin"
|
|
9
|
+
volumes:
|
|
10
|
+
- ./alice.ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom/alice.ldif:ro
|
|
11
|
+
command: --copy-service
|
|
12
|
+
ports:
|
|
13
|
+
- "389:389"
|
|
14
|
+
|
|
15
|
+
httpbin:
|
|
16
|
+
image: kennethreitz/httpbin
|
|
17
|
+
ports:
|
|
18
|
+
- "8080:80"
|
|
19
|
+
|
|
20
|
+
ldapgate:
|
|
21
|
+
build:
|
|
22
|
+
context: ..
|
|
23
|
+
dockerfile: docker/Dockerfile
|
|
24
|
+
volumes:
|
|
25
|
+
- ./ldapgate.local.yaml:/app/ldapgate.yaml:ro
|
|
26
|
+
ports:
|
|
27
|
+
- "9000:9000"
|
|
28
|
+
depends_on:
|
|
29
|
+
- openldap
|
|
30
|
+
- httpbin
|
|
31
|
+
command: ["ldapgate", "serve", "--config", "/app/ldapgate.yaml"]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
ldap:
|
|
2
|
+
url: ldap://openldap:389
|
|
3
|
+
bind_dn: cn=admin,dc=example,dc=org
|
|
4
|
+
bind_password: "admin"
|
|
5
|
+
base_dn: dc=example,dc=org
|
|
6
|
+
user_filter: "(uid={username})"
|
|
7
|
+
timeout: 10
|
|
8
|
+
follow_referrals: false
|
|
9
|
+
|
|
10
|
+
proxy:
|
|
11
|
+
listen_host: 0.0.0.0
|
|
12
|
+
listen_port: 9000
|
|
13
|
+
backend_url: http://httpbin:80
|
|
14
|
+
secret_key: local-dev-secret-not-for-production
|
|
15
|
+
session_ttl: 3600
|
|
16
|
+
user_header: X-Forwarded-User
|
|
17
|
+
login_path: /_auth/login
|
|
18
|
+
app_name: ldapgate-local
|
|
19
|
+
secure_cookies: false
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""ldapgate - LDAP/AD authentication proxy and FastAPI middleware"""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"LDAPAuthenticator",
|
|
7
|
+
"LDAPConfig",
|
|
8
|
+
"LDAPAuthMiddleware",
|
|
9
|
+
"SessionManager",
|
|
10
|
+
"create_proxy_app",
|
|
11
|
+
"create_login_router",
|
|
12
|
+
"add_ldap_auth",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
from ldapgate.config import LDAPConfig
|
|
16
|
+
from ldapgate.ldap import LDAPAuthenticator
|
|
17
|
+
from ldapgate.middleware import LDAPAuthMiddleware, add_ldap_auth
|
|
18
|
+
from ldapgate.proxy import create_proxy_app, create_login_router
|
|
19
|
+
from ldapgate.sessions import SessionManager
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""CLI for ldapgate reverse proxy."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
import uvicorn
|
|
8
|
+
|
|
9
|
+
from ldapgate.config import load_config
|
|
10
|
+
from ldapgate.proxy import create_proxy_app
|
|
11
|
+
|
|
12
|
+
# Env var used to pass config path to the module-level app factory for --reload mode
|
|
13
|
+
_CONFIG_ENV_VAR = "LDAPGATE_CONFIG_PATH"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.group()
|
|
17
|
+
def cli():
|
|
18
|
+
"""ldapgate - LDAP/AD authentication proxy."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@cli.command()
|
|
23
|
+
@click.option(
|
|
24
|
+
"--config",
|
|
25
|
+
type=click.Path(path_type=Path),
|
|
26
|
+
default=None,
|
|
27
|
+
help="Path to ldapgate.yaml config file (reads env vars if omitted)",
|
|
28
|
+
)
|
|
29
|
+
@click.option(
|
|
30
|
+
"--host",
|
|
31
|
+
default=None,
|
|
32
|
+
help="Override listen host (default: 0.0.0.0)",
|
|
33
|
+
)
|
|
34
|
+
@click.option(
|
|
35
|
+
"--port",
|
|
36
|
+
type=int,
|
|
37
|
+
default=None,
|
|
38
|
+
help="Override listen port (default: 9000)",
|
|
39
|
+
)
|
|
40
|
+
@click.option(
|
|
41
|
+
"--backend",
|
|
42
|
+
default=None,
|
|
43
|
+
help="Override backend URL",
|
|
44
|
+
)
|
|
45
|
+
@click.option(
|
|
46
|
+
"--reload",
|
|
47
|
+
is_flag=True,
|
|
48
|
+
help="Enable auto-reload on code changes",
|
|
49
|
+
)
|
|
50
|
+
def serve(config: Path, host: str, port: int, backend: str, reload: bool):
|
|
51
|
+
"""Start ldapgate reverse proxy server.
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
ldapgate serve --config ldapgate.yaml
|
|
55
|
+
ldapgate serve --backend http://localhost:3923
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
cfg = load_config(config)
|
|
59
|
+
|
|
60
|
+
if host:
|
|
61
|
+
cfg.proxy.listen_host = host
|
|
62
|
+
if port:
|
|
63
|
+
cfg.proxy.listen_port = port
|
|
64
|
+
if backend:
|
|
65
|
+
cfg.proxy.backend_url = backend
|
|
66
|
+
|
|
67
|
+
click.echo(
|
|
68
|
+
f"Starting ldapgate proxy on {cfg.proxy.listen_host}:{cfg.proxy.listen_port}"
|
|
69
|
+
)
|
|
70
|
+
click.echo(f"Backend: {cfg.proxy.backend_url}")
|
|
71
|
+
click.echo(f"Login path: {cfg.proxy.login_path}")
|
|
72
|
+
|
|
73
|
+
if reload:
|
|
74
|
+
# Reload mode requires an import string; pass config + overrides via env vars
|
|
75
|
+
# so the factory in this module can reconstruct the app identically.
|
|
76
|
+
if config:
|
|
77
|
+
os.environ[_CONFIG_ENV_VAR] = str(config)
|
|
78
|
+
if backend:
|
|
79
|
+
os.environ["LDAPGATE_BACKEND_URL"] = backend
|
|
80
|
+
uvicorn.run(
|
|
81
|
+
"ldapgate.cli:_reload_app_factory",
|
|
82
|
+
factory=True,
|
|
83
|
+
host=cfg.proxy.listen_host,
|
|
84
|
+
port=cfg.proxy.listen_port,
|
|
85
|
+
reload=True,
|
|
86
|
+
log_level="info",
|
|
87
|
+
)
|
|
88
|
+
else:
|
|
89
|
+
app = create_proxy_app(cfg)
|
|
90
|
+
uvicorn.run(
|
|
91
|
+
app,
|
|
92
|
+
host=cfg.proxy.listen_host,
|
|
93
|
+
port=cfg.proxy.listen_port,
|
|
94
|
+
log_level="info",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
except FileNotFoundError as e:
|
|
98
|
+
click.echo(f"Error: {e}", err=True)
|
|
99
|
+
raise SystemExit(1)
|
|
100
|
+
except Exception as e:
|
|
101
|
+
click.echo(f"Error: {e}", err=True)
|
|
102
|
+
raise SystemExit(1)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _reload_app_factory():
|
|
106
|
+
"""App factory used by uvicorn --reload (needs importable callable)."""
|
|
107
|
+
config_path = os.environ.get(_CONFIG_ENV_VAR)
|
|
108
|
+
cfg = load_config(config_path)
|
|
109
|
+
backend = os.environ.get("LDAPGATE_BACKEND_URL")
|
|
110
|
+
if backend:
|
|
111
|
+
cfg.proxy.backend_url = backend
|
|
112
|
+
return create_proxy_app(cfg)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def main():
|
|
116
|
+
"""Entry point for ldapgate CLI."""
|
|
117
|
+
cli()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
if __name__ == "__main__":
|
|
121
|
+
main()
|