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.
@@ -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,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+