airflow-ldap-auth-manager 0.1.0__py3-none-any.whl
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.
- airflow_ldap_auth_manager/__init__.py +5 -0
- airflow_ldap_auth_manager/ldap_auth_manager.py +733 -0
- airflow_ldap_auth_manager/static/airflow.svg +13 -0
- airflow_ldap_auth_manager/static/style.css +138 -0
- airflow_ldap_auth_manager/templates/ldap_login.html +41 -0
- airflow_ldap_auth_manager-0.1.0.dist-info/METADATA +333 -0
- airflow_ldap_auth_manager-0.1.0.dist-info/RECORD +10 -0
- airflow_ldap_auth_manager-0.1.0.dist-info/WHEEL +5 -0
- airflow_ldap_auth_manager-0.1.0.dist-info/licenses/LICENSE +201 -0
- airflow_ldap_auth_manager-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 35 35" aria-hidden="true">
|
|
2
|
+
<g>
|
|
3
|
+
<path d="M0.861099 35.3873L17.7729 18.0515C17.8788 17.9429 17.8991 17.775 17.8108 17.6516C16.782 16.2156 14.8848 15.9667 14.1814 15.002C12.0981 12.1441 11.5695 10.5266 10.6743 10.6269C10.6117 10.6339 10.556 10.6676 10.512 10.7126L4.4026 16.9752C0.887961 20.5779 0.383943 28.5103 0.291509 35.1524C0.287333 35.4525 0.651482 35.6021 0.861099 35.3873Z" fill="#017cee"/>
|
|
4
|
+
<path d="M35.4734 34.9588L18.1375 18.047C18.0289 17.941 17.861 17.9207 17.7377 18.0091C16.3017 19.0378 16.0528 20.9351 15.088 21.6384C12.2302 23.7217 10.6126 24.2504 10.7129 25.1456C10.72 25.2082 10.7536 25.2639 10.7987 25.3077L17.0613 31.4172C20.664 34.9319 28.5964 35.4359 35.2385 35.5282C35.5386 35.5326 35.6882 35.1684 35.4734 34.9588Z" fill="#00ad46"/>
|
|
5
|
+
<path d="M17.0612 31.4173C15.0932 29.4975 14.1801 25.6994 17.953 17.8671C11.8213 20.6074 9.67257 24.2094 10.7296 25.2407L17.0612 31.4173Z" fill="#04d659" fill-rule="evenodd" clip-rule="evenodd"/>
|
|
6
|
+
<path d="M35.0445 0.346896L18.1327 17.6827C18.0268 17.7913 18.0065 17.9592 18.0948 18.0825C19.1236 19.5186 21.0209 19.7674 21.724 20.7322C23.8075 23.59 24.3362 25.2075 25.2313 25.1074C25.2938 25.1004 25.3496 25.0666 25.3936 25.0216L31.5029 18.759C35.0177 15.1562 35.5217 7.22392 35.6141 0.58175C35.6182 0.281597 35.2541 0.132024 35.0445 0.346896Z" fill="#00c7d4"/>
|
|
7
|
+
<path d="M31.5031 18.759C29.5832 20.7269 25.7851 21.6401 17.9528 17.8671C20.693 23.9988 24.2951 26.1477 25.3263 25.0905L31.5031 18.759Z" fill="#11e1ee" fill-rule="evenodd" clip-rule="evenodd"/>
|
|
8
|
+
<path d="M0.432658 0.775339L17.7685 17.6871C17.8771 17.793 18.045 17.8134 18.1683 17.725C19.6043 16.6963 19.8532 14.799 20.8179 14.0957C23.6759 12.0123 25.2934 11.4837 25.193 10.5885C25.186 10.526 25.1523 10.4702 25.1074 10.4263L18.8447 4.31685C15.242 0.802203 7.30967 0.298184 0.667512 0.205751C0.367359 0.201573 0.217786 0.565722 0.432658 0.775339Z" fill="#e43921"/>
|
|
9
|
+
<path d="M18.8446 4.31675C20.8125 6.23662 21.7257 10.0346 17.9528 17.8669C24.0844 15.1267 26.2333 11.5246 25.1761 10.4934L18.8446 4.31675Z" fill="#ff7557" fill-rule="evenodd" clip-rule="evenodd"/>
|
|
10
|
+
<path d="M4.4028 16.9752C6.32267 15.0072 10.1207 14.0942 17.953 17.867C15.2128 11.7354 11.6107 9.58661 10.5795 10.6437L4.4028 16.9752Z" fill="#0cb6ff" fill-rule="evenodd" clip-rule="evenodd"/>
|
|
11
|
+
<path d="M17.9649 18.6209C18.3825 18.6157 18.7169 18.273 18.7117 17.8553C18.7065 17.4377 18.3638 17.1034 17.9462 17.1085C17.5285 17.1137 17.1942 17.4564 17.1994 17.8741C17.2045 18.2917 17.5473 18.626 17.9649 18.6209Z" fill="#4a4848"/>
|
|
12
|
+
</g>
|
|
13
|
+
</svg>
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--bg: #0b1020;
|
|
3
|
+
--card: #10172a;
|
|
4
|
+
--text: #e5e7eb;
|
|
5
|
+
--muted: #818192;
|
|
6
|
+
--almost-hidden: #4d4d53;
|
|
7
|
+
--field: #0f172a;
|
|
8
|
+
--focus: #60a5fa;
|
|
9
|
+
}
|
|
10
|
+
* {
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
}
|
|
13
|
+
html,
|
|
14
|
+
body {
|
|
15
|
+
height: 100%;
|
|
16
|
+
}
|
|
17
|
+
body {
|
|
18
|
+
margin: 0;
|
|
19
|
+
background: radial-gradient(1200px 800px at 20% -10%, #122042, transparent), var(--bg);
|
|
20
|
+
color: var(--text);
|
|
21
|
+
font: 16px/1.5 system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Helvetica, Arial;
|
|
22
|
+
}
|
|
23
|
+
.wrap {
|
|
24
|
+
min-height: 100%;
|
|
25
|
+
display: flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
justify-content: center;
|
|
28
|
+
padding: 3rem 1rem;
|
|
29
|
+
}
|
|
30
|
+
.card {
|
|
31
|
+
width: 100%;
|
|
32
|
+
max-width: 420px;
|
|
33
|
+
background: var(--card);
|
|
34
|
+
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
35
|
+
border-radius: 16px;
|
|
36
|
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
|
|
37
|
+
padding: 28px;
|
|
38
|
+
}
|
|
39
|
+
.brand {
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
gap: 0.75rem;
|
|
43
|
+
margin-bottom: 0.25rem;
|
|
44
|
+
}
|
|
45
|
+
.brand h1 {
|
|
46
|
+
margin: 0;
|
|
47
|
+
font-size: 1.6rem;
|
|
48
|
+
font-weight: 600;
|
|
49
|
+
letter-spacing: 0.2px;
|
|
50
|
+
}
|
|
51
|
+
.logo {
|
|
52
|
+
width: 36px;
|
|
53
|
+
height: 36px;
|
|
54
|
+
}
|
|
55
|
+
h2 {
|
|
56
|
+
margin: 0.25rem 0 1.25rem;
|
|
57
|
+
font-size: 1.4rem;
|
|
58
|
+
font-weight: 700;
|
|
59
|
+
}
|
|
60
|
+
label {
|
|
61
|
+
display: block;
|
|
62
|
+
font-size: 0.85rem;
|
|
63
|
+
color: var(--muted);
|
|
64
|
+
margin-bottom: 0.35rem;
|
|
65
|
+
}
|
|
66
|
+
input[type="text"],
|
|
67
|
+
input[type="password"] {
|
|
68
|
+
width: 100%;
|
|
69
|
+
background: var(--field);
|
|
70
|
+
color: var(--text);
|
|
71
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
72
|
+
border-radius: 10px;
|
|
73
|
+
padding: 0.8rem 0.9rem;
|
|
74
|
+
outline: 0;
|
|
75
|
+
transition: border 0.15s, box-shadow 0.15s;
|
|
76
|
+
}
|
|
77
|
+
input:focus {
|
|
78
|
+
border-color: var(--focus);
|
|
79
|
+
box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.25);
|
|
80
|
+
}
|
|
81
|
+
.row {
|
|
82
|
+
margin-bottom: 1rem;
|
|
83
|
+
}
|
|
84
|
+
.actions {
|
|
85
|
+
display: flex;
|
|
86
|
+
align-items: center;
|
|
87
|
+
justify-content: space-between;
|
|
88
|
+
gap: 0.75rem;
|
|
89
|
+
margin-top: 0.25rem;
|
|
90
|
+
}
|
|
91
|
+
button {
|
|
92
|
+
width: 100%;
|
|
93
|
+
appearance: none;
|
|
94
|
+
border: 0;
|
|
95
|
+
border-radius: 10px;
|
|
96
|
+
padding: 0.85rem 1rem;
|
|
97
|
+
background: linear-gradient(120deg, #16a34a, #22c55e);
|
|
98
|
+
color: #04140b;
|
|
99
|
+
font-weight: 700;
|
|
100
|
+
cursor: pointer;
|
|
101
|
+
transition: transform 0.06s ease, filter 0.2s;
|
|
102
|
+
}
|
|
103
|
+
button:hover {
|
|
104
|
+
filter: brightness(1.05);
|
|
105
|
+
}
|
|
106
|
+
button:active {
|
|
107
|
+
transform: translateY(1px);
|
|
108
|
+
}
|
|
109
|
+
.error {
|
|
110
|
+
background: #3b0d0d;
|
|
111
|
+
border: 1px solid #ef4444;
|
|
112
|
+
color: #fecaca;
|
|
113
|
+
padding: 0.75rem 0.9rem;
|
|
114
|
+
border-radius: 10px;
|
|
115
|
+
margin: 0.5rem 0 1rem;
|
|
116
|
+
font-size: 0.9rem;
|
|
117
|
+
}
|
|
118
|
+
.muted {
|
|
119
|
+
color: var(--muted);
|
|
120
|
+
font-size: 0.85rem;
|
|
121
|
+
}
|
|
122
|
+
.almost-hidden {
|
|
123
|
+
color: var(--almost-hidden);
|
|
124
|
+
font-size: 0.85rem;
|
|
125
|
+
}
|
|
126
|
+
.foot {
|
|
127
|
+
text-align: center;
|
|
128
|
+
margin-top: 1rem;
|
|
129
|
+
}
|
|
130
|
+
.help {
|
|
131
|
+
text-align: center;
|
|
132
|
+
margin-top: 0.75rem;
|
|
133
|
+
font-size: 0.85rem;
|
|
134
|
+
}
|
|
135
|
+
.css-536swo {
|
|
136
|
+
height: 35px;
|
|
137
|
+
width: 35px;
|
|
138
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8"/>
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
6
|
+
<title>{{ instance_name | e }} · Sign in</title>
|
|
7
|
+
<link rel="icon" href="/static/pin_32.png"/>
|
|
8
|
+
<link rel="stylesheet" href="/auth/static/style.css"/>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div class="wrap">
|
|
12
|
+
<div class="card">
|
|
13
|
+
<div class="brand">
|
|
14
|
+
<img src="/auth/static/airflow.svg" alt="Airflow" class="logo"/>
|
|
15
|
+
<h1>{{ instance_name | e }}</h1>
|
|
16
|
+
</div>
|
|
17
|
+
<h2>Sign in</h2>
|
|
18
|
+
{% if login_tip %}<h4>{{ login_tip | e }}</h4>{% endif %}
|
|
19
|
+
|
|
20
|
+
{% if error %}<div class="error">{{ error }}</div>{% endif %}
|
|
21
|
+
|
|
22
|
+
<form method="post" action="/auth/token">
|
|
23
|
+
<input type="hidden" name="next" value="{{ next | e }}"/>
|
|
24
|
+
<div class="row">
|
|
25
|
+
<label for="username">Username</label>
|
|
26
|
+
<input id="username" name="username" type="text" autocomplete="username" required/>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="row">
|
|
29
|
+
<label for="password">Password</label>
|
|
30
|
+
<input id="password" name="password" type="password" autocomplete="current-password" required/>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="actions">
|
|
33
|
+
<button type="submit">Sign in</button>
|
|
34
|
+
</div>
|
|
35
|
+
</form>
|
|
36
|
+
<div class="foot almost-hidden">Having issues? Contact your admin.</div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
<script>try{document.getElementById('username').focus()}catch(e){}</script>
|
|
40
|
+
</body>
|
|
41
|
+
</html>
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: airflow-ldap-auth-manager
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LDAP-based AuthManager for Apache Airflow 3.x
|
|
5
|
+
Author-email: Emre Can <emredjan@gmail.com>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/emredjan/airflow-ldap-auth-manager
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Framework :: Apache Airflow
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: apache-airflow>=3.1.0
|
|
18
|
+
Requires-Dist: ldap3>=2.9
|
|
19
|
+
Requires-Dist: fastapi>=0.110
|
|
20
|
+
Requires-Dist: jinja2>=3.1
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
> [!NOTE]
|
|
24
|
+
> The code in this repository is written with the help of AI (specifically, ChatGPT 5), including this README (lazy, I know). Although I tried my best to streamline and validate most of it, there may still be some intricacies that need to be sorted out.
|
|
25
|
+
|
|
26
|
+
# Airflow LDAP Auth Manager
|
|
27
|
+
|
|
28
|
+
A drop-in **Auth Manager** for **Apache Airflow 3.x** that authenticates users against **LDAP/Active Directory** and maps LDAP groups to Airflow roles (**admin / editor / viewer**). It supports redundant LDAP servers, secure transport (LDAPS or StartTLS), and secret indirection for bind credentials via Airflow’s **Secrets Backend** (Variables).
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
- **LDAP/AD authentication** using [`ldap3`](https://ldap3.readthedocs.io/)
|
|
33
|
+
- **Group → role mapping** (admin > editor > viewer)
|
|
34
|
+
- **Redundancy & failover** via `ldap3.ServerPool` (ROUND_ROBIN by default)
|
|
35
|
+
- **TLS: LDAPS or StartTLS** (with certificate verification)
|
|
36
|
+
- **Secrets-friendly config:** `bind_dn_secret` and `bind_password_secret` read from Airflow Variables (resolved through your configured secrets backend)
|
|
37
|
+
- **Clean Airflow 3 API:** uses the Airflow SDK (`airflow.sdk.*`)
|
|
38
|
+
- **Simple login UI** with configurable instance name and login tip
|
|
39
|
+
- **Helpful logging:** logs which LDAP server the connection bound to
|
|
40
|
+
|
|
41
|
+
## To-do
|
|
42
|
+
|
|
43
|
+
- [x] Extend the user & group search base config items to allow multiple entries (or LDAP `OR` syntax)
|
|
44
|
+
- [x] Package this and upload to pypi
|
|
45
|
+
|
|
46
|
+
## Requirements
|
|
47
|
+
|
|
48
|
+
- **Python:** 3.12+
|
|
49
|
+
- **Airflow:** 3.1+ (Auth Manager interface & SDK)
|
|
50
|
+
- **Libraries:** `ldap3`, `fastapi`, `jinja2` (`fastapi` & `jinja2` is likely installed already as Airflow dependencies)
|
|
51
|
+
- **Optional (recommended):** a Secrets Backend (Vault, AWS Secrets Manager, GCP SM, Azure KV) configured for Airflow Variables
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
Install from PyPi into the environment where Airflow is installed:
|
|
56
|
+
|
|
57
|
+
```shell
|
|
58
|
+
pip install airflow-ldap-auth-manager
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
Or install after cloning the project:
|
|
63
|
+
|
|
64
|
+
```shell
|
|
65
|
+
git clone <repo url>
|
|
66
|
+
cd airflow-ldap-auth-manager
|
|
67
|
+
pip install .
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Configure Airflow
|
|
71
|
+
|
|
72
|
+
Changes needed in `airflow.cfg`:
|
|
73
|
+
|
|
74
|
+
### Enable the Auth Manager
|
|
75
|
+
|
|
76
|
+
```ini
|
|
77
|
+
[core]
|
|
78
|
+
# Fully qualified path to the auth manager class in this repo
|
|
79
|
+
auth_manager = airflow_ldap_auth_manager.LDAPAuthManager
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
### Make sure JWT settings are configured correctly
|
|
84
|
+
|
|
85
|
+
```ini
|
|
86
|
+
[api_auth]
|
|
87
|
+
jwt_secret = # Needs to be set
|
|
88
|
+
jwt_algorithm = # Either leave empty, or make sure consistent across all machines
|
|
89
|
+
jwt_audience = # Either leave empty, or make sure consistent across all machines
|
|
90
|
+
jwt_issuer = # Either leave empty, or make sure consistent across all machines
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### LDAP settings
|
|
94
|
+
|
|
95
|
+
Add a section for the LDAP auth manager (adjust to your environment):
|
|
96
|
+
|
|
97
|
+
```ini
|
|
98
|
+
[ldap_auth_manager]
|
|
99
|
+
server_uri = ldaps://ldap1.example.com:636,ldaps://ldap2.example.com:636
|
|
100
|
+
bind_dn =
|
|
101
|
+
bind_password =
|
|
102
|
+
bind_dn_secret = secret/path/to/airflow/variable/for/bind_dn
|
|
103
|
+
bind_password_secret = path/to/airflow/variable/for/bind_password
|
|
104
|
+
user_search_base = OU=Users,DC=company_name
|
|
105
|
+
user_search_filter = (|(uid={username})(sAMAccountName={username})(mail={username}))
|
|
106
|
+
group_search_base = OU=Groups,DC=company_name
|
|
107
|
+
group_member_attr = member
|
|
108
|
+
admin_groups = airflow-admins
|
|
109
|
+
editor_groups = airflow-editors
|
|
110
|
+
viewer_groups = airflow-viewers,airflow-auditors
|
|
111
|
+
username_attr = uid
|
|
112
|
+
email_attr = mail
|
|
113
|
+
start_tls = false
|
|
114
|
+
verify_ssl = true
|
|
115
|
+
post_login_redirect = /
|
|
116
|
+
logout_redirect = /
|
|
117
|
+
debug_logging = false
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Environment variable overrides are supported in the standard Airflow fashion, e.g.:`
|
|
121
|
+
|
|
122
|
+
`AIRFLOW__LDAP_AUTH_MANAGER__BIND_DN_SECRET=api_server/ldap_auth_manager/bind_dn`
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
### Branding & login hint (optional)
|
|
126
|
+
|
|
127
|
+
```ini
|
|
128
|
+
[api]
|
|
129
|
+
# Human-friendly name for titles/headers on the login page
|
|
130
|
+
instance_name = Company Airflow
|
|
131
|
+
|
|
132
|
+
[ldap_auth_manager]
|
|
133
|
+
# Optional helper text shown under "Sign in"
|
|
134
|
+
login_tip = Using your Company credentials
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Restart the api-server after changes.
|
|
138
|
+
|
|
139
|
+
## Configuration reference
|
|
140
|
+
|
|
141
|
+
``` ini
|
|
142
|
+
[ldap_auth_manager]
|
|
143
|
+
# LDAP authentication/authorization settings for LDAPAuthManager.
|
|
144
|
+
# This section supports multiple redundant servers, secure transport (LDAPS or StartTLS),
|
|
145
|
+
# and secret indirection for bind credentials via Airflow’s Secrets Backend.
|
|
146
|
+
|
|
147
|
+
# Comma-separated list of LDAP server URIs.
|
|
148
|
+
# - Supports ldap:// and ldaps:// schemes.
|
|
149
|
+
# - When multiple URIs are provided, the manager builds an ldap3 ServerPool with ROUND_ROBIN
|
|
150
|
+
# strategy for load distribution and failover.
|
|
151
|
+
# - For ldaps://, TLS is implicit from connect; for ldap:// + start_tls=true, StartTLS is
|
|
152
|
+
# negotiated before bind.
|
|
153
|
+
# - Mixing ldaps:// with start_tls=true is not meaningful; prefer one approach.
|
|
154
|
+
#
|
|
155
|
+
# Variable: AIRFLOW__LDAP_AUTH_MANAGER__SERVER_URI
|
|
156
|
+
#
|
|
157
|
+
server_uri = ldaps://ldap1.example.com:636,ldaps://ldap2.example.com:636
|
|
158
|
+
|
|
159
|
+
# Name of the Airflow Variable that contains the LDAP bind DN (service account).
|
|
160
|
+
# The Variable is resolved through the configured Secrets Backend (e.g. Vault, AWS SM, etc.).
|
|
161
|
+
# If set, any plaintext `bind_dn` value is ignored. Leave empty to attempt anonymous bind.
|
|
162
|
+
# Remember: This needs to be set in the "Airflow/Variables" section in your secret manager,
|
|
163
|
+
# NOT "Airflow/Config"!
|
|
164
|
+
#
|
|
165
|
+
# Variable: AIRFLOW__LDAP_AUTH_MANAGER__BIND_DN_SECRET
|
|
166
|
+
#
|
|
167
|
+
bind_dn_secret = secret/path/to/airflow/variable/for/bind_dn
|
|
168
|
+
bind_dn =
|
|
169
|
+
|
|
170
|
+
# Name of the Airflow Variable that contains the LDAP bind password (service account).
|
|
171
|
+
# The Variable is resolved through the configured Secrets Backend.
|
|
172
|
+
# If set, any plaintext `bind_password` value is ignored.
|
|
173
|
+
# Remember: This needs to be set in the "Airflow/Variables" section in your secret manager,
|
|
174
|
+
# NOT "Airflow/Config"!
|
|
175
|
+
#
|
|
176
|
+
# Variable: AIRFLOW__LDAP_AUTH_MANAGER__BIND_PASSWORD_SECRET
|
|
177
|
+
#
|
|
178
|
+
bind_password_secret = path/to/airflow/variable/for/bind_password
|
|
179
|
+
bind_password =
|
|
180
|
+
|
|
181
|
+
# Base DN under which user entries are searched.
|
|
182
|
+
# Example (Active Directory): OU=Users,OU=Country,DC=example,DC=com
|
|
183
|
+
# Supports multiple base DNs, separated by newlines, semicolons, or as a JSON array.
|
|
184
|
+
#
|
|
185
|
+
# Variable: AIRFLOW__LDAP_AUTH_MANAGER__USER_SEARCH_BASE
|
|
186
|
+
#
|
|
187
|
+
user_search_base = OU=Users,DC=company_name
|
|
188
|
+
|
|
189
|
+
# LDAP filter template to locate the authenticating user.
|
|
190
|
+
# The literal "{username}" placeholder is replaced with the submitted login identifier.
|
|
191
|
+
# Must be a valid RFC 4515 filter. The username value is safely escaped before substitution.
|
|
192
|
+
# Example: (|(uid={username})(sAMAccountName={username})(mail={username}))
|
|
193
|
+
#
|
|
194
|
+
# Variable: AIRFLOW__LDAP_AUTH_MANAGER__USER_SEARCH_FILTER
|
|
195
|
+
#
|
|
196
|
+
user_search_filter = (|(uid={username})(sAMAccountName={username})(mail={username}))
|
|
197
|
+
|
|
198
|
+
# Base DN under which group entries are searched for authorization.
|
|
199
|
+
#
|
|
200
|
+
# Variable: AIRFLOW__LDAP_AUTH_MANAGER__GROUP_SEARCH_BASE
|
|
201
|
+
#
|
|
202
|
+
group_search_base = OU=Groups,DC=company_name
|
|
203
|
+
|
|
204
|
+
# The group attribute that lists membership (DNs of user entries).
|
|
205
|
+
# Common values:
|
|
206
|
+
# - Active Directory: member
|
|
207
|
+
# - RFC2307/posix groups: memberUid (then matching is by username instead of DN)
|
|
208
|
+
#
|
|
209
|
+
# Variable: AIRFLOW__LDAP_AUTH_MANAGER__GROUP_MEMBER_ATTR
|
|
210
|
+
#
|
|
211
|
+
group_member_attr = member
|
|
212
|
+
|
|
213
|
+
# Comma-separated list of groups that grant the Airflow "admin" role.
|
|
214
|
+
# Values can be group CNs or full DNs under group_search_base (matching is case-insensitive).
|
|
215
|
+
#
|
|
216
|
+
# Variable: AIRFLOW__LDAP_AUTH_MANAGER__ADMIN_GROUPS
|
|
217
|
+
#
|
|
218
|
+
admin_groups = airflow-admins
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# Comma-separated list of groups that grant the Airflow "editor" role.
|
|
222
|
+
# Leave empty to disable this mapping.
|
|
223
|
+
#
|
|
224
|
+
# Variable: AIRFLOW__LDAP_AUTH_MANAGER__EDITOR_GROUPS
|
|
225
|
+
#
|
|
226
|
+
editor_groups = airflow-editors
|
|
227
|
+
|
|
228
|
+
# Comma-separated list of groups that grant the Airflow "viewer" role.
|
|
229
|
+
# Users are mapped to the highest role matched (admin > editor > viewer).
|
|
230
|
+
#
|
|
231
|
+
# Variable: AIRFLOW__LDAP_AUTH_MANAGER__VIEWER_GROUPS
|
|
232
|
+
#
|
|
233
|
+
viewer_groups = airflow-viewers,airflow-auditors
|
|
234
|
+
|
|
235
|
+
# Attribute on the user entry to use as the Airflow username.
|
|
236
|
+
# Typical values: uid, sAMAccountName, userPrincipalName
|
|
237
|
+
#
|
|
238
|
+
# Variable: AIRFLOW__LDAP_AUTH_MANAGER__USERNAME_ATTR
|
|
239
|
+
#
|
|
240
|
+
username_attr = uid
|
|
241
|
+
|
|
242
|
+
# Attribute on the user entry that contains the email address.
|
|
243
|
+
# Typical values: mail, userPrincipalName
|
|
244
|
+
#
|
|
245
|
+
# Variable: AIRFLOW__LDAP_AUTH_MANAGER__EMAIL_ATTR
|
|
246
|
+
#
|
|
247
|
+
email_attr = mail
|
|
248
|
+
|
|
249
|
+
# Whether to perform StartTLS on ldap:// connections before bind.
|
|
250
|
+
# - true : Use StartTLS (only applies to ldap:// URIs).
|
|
251
|
+
# - false : Do not use StartTLS. For ldaps:// URIs, TLS is already implicit.
|
|
252
|
+
#
|
|
253
|
+
# Variable: AIRFLOW__LDAP_AUTH_MANAGER__START_TLS
|
|
254
|
+
#
|
|
255
|
+
start_tls = false
|
|
256
|
+
|
|
257
|
+
# Whether to verify the server certificate for TLS (ldaps or StartTLS).
|
|
258
|
+
# Set to true in production with a valid trust store. Set to false only for testing.
|
|
259
|
+
#
|
|
260
|
+
# Variable: AIRFLOW__LDAP_AUTH_MANAGER__VERIFY_SSL
|
|
261
|
+
#
|
|
262
|
+
verify_ssl = true
|
|
263
|
+
|
|
264
|
+
# Path (relative to the Airflow web root) to redirect a user after successful login.
|
|
265
|
+
# Example: "/" or "/home"
|
|
266
|
+
#
|
|
267
|
+
# Variable: AIRFLOW__LDAP_AUTH_MANAGER__POST_LOGIN_REDIRECT
|
|
268
|
+
#
|
|
269
|
+
post_login_redirect = /
|
|
270
|
+
|
|
271
|
+
# Path (relative to the Airflow web root) to redirect a user after logout.
|
|
272
|
+
# Example: "/" or "/login"
|
|
273
|
+
#
|
|
274
|
+
# Variable: AIRFLOW__LDAP_AUTH_MANAGER__LOGOUT_REDIRECT
|
|
275
|
+
#
|
|
276
|
+
logout_redirect = /
|
|
277
|
+
|
|
278
|
+
# Enable debug logging for LDAP operations.
|
|
279
|
+
# This can be useful to troubleshoot LDAP issues, but beware that it may log
|
|
280
|
+
# sensitive information such as usernames and group names.
|
|
281
|
+
#
|
|
282
|
+
# Variable: AIRFLOW__LDAP_AUTH_MANAGER__DEBUG_LOGGING
|
|
283
|
+
#
|
|
284
|
+
debug_logging = false
|
|
285
|
+
|
|
286
|
+
# Optional login hint shown under "Sign in". Leave empty to hide.
|
|
287
|
+
#
|
|
288
|
+
# Variable: AIRFLOW__LDAP_AUTH_MANAGER__LOGIN_TIP
|
|
289
|
+
#
|
|
290
|
+
login_tip = Using your Company credentials
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
## UI & templates
|
|
295
|
+
|
|
296
|
+
- Login template: `ldap_login.html`
|
|
297
|
+
- Static assets are served under `/auth/static/` (e.g. `/auth/static/style.css`, `/auth/static/airflow.svg`)
|
|
298
|
+
- Form posts to `/auth/token`
|
|
299
|
+
|
|
300
|
+
If you see 404 Not Found for `/auth/static/...`, ensure the StaticFiles mount path in your FastAPI router matches the URLs used in the template.
|
|
301
|
+
|
|
302
|
+
## Logging & diagnostics
|
|
303
|
+
|
|
304
|
+
If `debug_logging = true` in config:
|
|
305
|
+
|
|
306
|
+
After successful bind, the manager logs which LDAP server was selected from the pool, e.g.:
|
|
307
|
+
|
|
308
|
+
```log
|
|
309
|
+
LDAP bound to ldaps://ldap2.example.com:636 (pool_strategy=ROUND_ROBIN, start_tls=False)
|
|
310
|
+
```
|
|
311
|
+
Optionally, it can log the authenticated identity via the LDAP `whoami` extended operation.
|
|
312
|
+
|
|
313
|
+
## Security notes
|
|
314
|
+
|
|
315
|
+
- **Choose one TLS mode:** either `ldaps://...` or `ldap://` with `start_tls=true`. Mixing them is discouraged.
|
|
316
|
+
- Keep `verify_ssl = true` in production and ensure your trust store contains the issuing CA(s).
|
|
317
|
+
- Bind credentials should preferrably be supplied via `*_secret` indirection (Variables → Secrets Backend), not plaintext.
|
|
318
|
+
|
|
319
|
+
## Troubleshooting
|
|
320
|
+
|
|
321
|
+
- **Can’t bind / invalid credentials:** test with `ldapsearch` using the same DN/password and base/filter.
|
|
322
|
+
- **User not found:** verify `user_search_base` and `user_search_filter` (remember `{username}` substitution).
|
|
323
|
+
- **Group mapping not applied:** confirm `group_search_base`, `group_member_attr`, and that your groups are in the configured bases.
|
|
324
|
+
- **TLS errors:** verify certificates and CA chain; ensure the hostname matches the server certificate CN/SAN.
|
|
325
|
+
- **Static assets 404:** check the FastAPI `StaticFiles` mount matches `/auth/static`.
|
|
326
|
+
|
|
327
|
+
## Contributing
|
|
328
|
+
|
|
329
|
+
Issues and PRs are welcome. Please include:
|
|
330
|
+
|
|
331
|
+
- A clear description of the problem or feature
|
|
332
|
+
- Repro steps or tests when possible
|
|
333
|
+
- Your Airflow, Python, and LDAP server versions
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
airflow_ldap_auth_manager/__init__.py,sha256=km5plgI7W8uoEAuH_V5exwjvr-5bA8gLkHUtR8cemiU,106
|
|
2
|
+
airflow_ldap_auth_manager/ldap_auth_manager.py,sha256=cQdzW1JRpVtEgzggk4KFzlbdjg7QgZjLd8UI2uavhBQ,30486
|
|
3
|
+
airflow_ldap_auth_manager/static/airflow.svg,sha256=j44j0nS5tWWfyv5_DlHxkKtBE2NW7uzLtV7hJVU2AY0,2622
|
|
4
|
+
airflow_ldap_auth_manager/static/style.css,sha256=BiaUc2jxy8IyLkrGxnmcMk0HHYsL4UL5KJ5ZEHazOGg,2691
|
|
5
|
+
airflow_ldap_auth_manager/templates/ldap_login.html,sha256=HwPbexYXJm4bipwjXnx07RG2ukXlc6mRt5NPH9ZYbBc,1499
|
|
6
|
+
airflow_ldap_auth_manager-0.1.0.dist-info/licenses/LICENSE,sha256=O8xaKjn1OKIdOMHcaraGjPWIeqBRGqDqsntvtLLbIyc,11556
|
|
7
|
+
airflow_ldap_auth_manager-0.1.0.dist-info/METADATA,sha256=GE4vdj3aoTL4wMYAxlxunLBmzkpOON9xOSf5jL572U4,12284
|
|
8
|
+
airflow_ldap_auth_manager-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
+
airflow_ldap_auth_manager-0.1.0.dist-info/top_level.txt,sha256=gB3iZS02oTp4VzeWiiSlIJfGt0-wA1RCk4CzxrYJ_Cg,26
|
|
10
|
+
airflow_ldap_auth_manager-0.1.0.dist-info/RECORD,,
|