Skip to content

Commit 171d4b9

Browse files
committed
feat: OpenLDAP backend
1 parent b6804c5 commit 171d4b9

23 files changed

+1315
-1
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
### Added
8+
9+
- Add support for OpenLDAP backend to user-info-fetcher ([#779]).
10+
11+
[#779]: https://github.com/stackabletech/opa-operator/pull/779
12+
713
## [25.11.0] - 2025-11-07
814

915
## [25.11.0-rc1] - 2025-11-06

deploy/helm/opa-operator/crds/crds.yaml

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ spec:
8484
- experimentalActiveDirectory
8585
- required:
8686
- experimentalEntra
87+
- required:
88+
- experimentalOpenLdap
8789
properties:
8890
experimentalActiveDirectory:
8991
description: Backend that fetches user information from Active Directory
@@ -245,6 +247,151 @@ spec:
245247
- clientCredentialsSecret
246248
- tenantId
247249
type: object
250+
experimentalOpenLdap:
251+
description: Backend that fetches user information from OpenLDAP
252+
properties:
253+
bindCredentials:
254+
description: |-
255+
Credentials for binding to the LDAP server.
256+
257+
The bind account is used to search for users and groups in the LDAP directory.
258+
properties:
259+
scope:
260+
description: |-
261+
[Scope](https://docs.stackable.tech/home/nightly/secret-operator/scope) of the
262+
[SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass).
263+
nullable: true
264+
properties:
265+
listenerVolumes:
266+
default: []
267+
description: |-
268+
The listener volume scope allows Node and Service scopes to be inferred from the applicable listeners.
269+
This must correspond to Volume names in the Pod that mount Listeners.
270+
items:
271+
type: string
272+
type: array
273+
node:
274+
default: false
275+
description: |-
276+
The node scope is resolved to the name of the Kubernetes Node object that the Pod is running on.
277+
This will typically be the DNS name of the node.
278+
type: boolean
279+
pod:
280+
default: false
281+
description: |-
282+
The pod scope is resolved to the name of the Kubernetes Pod.
283+
This allows the secret to differentiate between StatefulSet replicas.
284+
type: boolean
285+
services:
286+
default: []
287+
description: |-
288+
The service scope allows Pod objects to specify custom scopes.
289+
This should typically correspond to Service objects that the Pod participates in.
290+
items:
291+
type: string
292+
type: array
293+
type: object
294+
secretClass:
295+
description: '[SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) containing the LDAP bind credentials.'
296+
type: string
297+
required:
298+
- secretClass
299+
type: object
300+
customAttributeMappings:
301+
additionalProperties:
302+
type: string
303+
default: {}
304+
description: Custom attributes, and their LDAP attribute names.
305+
type: object
306+
groupMemberAttribute:
307+
default: member
308+
description: |-
309+
LDAP attribute on group objects that contains member references.
310+
311+
Common values:
312+
- `member`: For `groupOfNames` objects (uses full DN)
313+
- `memberUid`: For `posixGroup` objects (uses username)
314+
315+
Defaults to `member`.
316+
type: string
317+
groupsSearchBase:
318+
description: |-
319+
LDAP search base for groups, e.g. `ou=groups,dc=example,dc=org`.
320+
321+
If not specified, uses the main `searchBase`.
322+
nullable: true
323+
type: string
324+
hostname:
325+
description: Hostname of the LDAP server, e.g. `my.ldap.server`.
326+
type: string
327+
port:
328+
description: Port of the LDAP server. If TLS is used defaults to `636`, otherwise to `389`.
329+
format: uint16
330+
maximum: 65535.0
331+
minimum: 0.0
332+
nullable: true
333+
type: integer
334+
searchBase:
335+
default: ''
336+
description: LDAP search base, e.g. `ou=users,dc=example,dc=org`.
337+
type: string
338+
tls:
339+
description: Use a TLS connection. If not specified no TLS will be used.
340+
nullable: true
341+
properties:
342+
verification:
343+
description: The verification method used to verify the certificates of the server and/or the client.
344+
oneOf:
345+
- required:
346+
- none
347+
- required:
348+
- server
349+
properties:
350+
none:
351+
description: Use TLS but don't verify certificates.
352+
type: object
353+
server:
354+
description: Use TLS and a CA certificate to verify the server.
355+
properties:
356+
caCert:
357+
description: CA cert to verify the server.
358+
oneOf:
359+
- required:
360+
- webPki
361+
- required:
362+
- secretClass
363+
properties:
364+
secretClass:
365+
description: |-
366+
Name of the [SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) which will provide the CA certificate.
367+
Note that a SecretClass does not need to have a key but can also work with just a CA certificate,
368+
so if you got provided with a CA cert but don't have access to the key you can still use this method.
369+
type: string
370+
webPki:
371+
description: |-
372+
Use TLS and the CA certificates trusted by the common web browsers to verify the server.
373+
This can be useful when you e.g. use public AWS S3 or other public available services.
374+
type: object
375+
type: object
376+
required:
377+
- caCert
378+
type: object
379+
type: object
380+
required:
381+
- verification
382+
type: object
383+
userIdAttribute:
384+
default: entryUUID
385+
description: LDAP attribute used for the user's unique identifier. Defaults to `entryUUID`.
386+
type: string
387+
userNameAttribute:
388+
default: uid
389+
description: LDAP attribute used for the username. Defaults to `uid`.
390+
type: string
391+
required:
392+
- bindCredentials
393+
- hostname
394+
type: object
248395
experimentalXfscAas:
249396
description: |-
250397
Backend that fetches user information from the Gaia-X

docs/modules/opa/pages/usage-guide/user-info-fetcher.adoc

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ Currently the following backends are supported:
5454
* xref:#backend-keycloak[]
5555
* xref:#backend-activedirectory[]
5656
* xref:#backend-entra[]
57+
* xref:#backend-openldap[]
5758
5859
[#backends]
5960
== Backends
@@ -206,6 +207,66 @@ spec:
206207
<2> The Entra tenant ID
207208
<3> A secret containing the `clientId` and `clientSecret` keys
208209

210+
[#backend-openldap]
211+
=== OpenLDAP
212+
213+
WARNING: The OpenLDAP backend is experimental, and subject to change.
214+
215+
Fetches user attributes and groups over LDAP from OpenLDAP servers using simple bind authentication.
216+
217+
OpenLDAP supports different group schemas:
218+
219+
* *groupOfNames* (default): Groups contain a `member` attribute with full user DNs
220+
* *posixGroup*: Groups contain a `memberUid` attribute with usernames
221+
222+
[source,yaml]
223+
----
224+
spec:
225+
clusterConfig:
226+
userInfo:
227+
backend:
228+
experimentalOpenLdap: # <1>
229+
hostname: openldap.default.svc.cluster.local # <2>
230+
port: 1636 # <3>
231+
searchBase: dc=example,dc=org # <4>
232+
bindCredentials: # <5>
233+
secretClass: openldap-bind-credentials # <6>
234+
userIdAttribute: entryUUID # <7>
235+
userNameAttribute: uid # <8>
236+
groupsSearchBase: ou=groups,dc=example,dc=org # <9>
237+
groupMemberAttribute: member # <10>
238+
customAttributeMappings: # <11>
239+
email: mail
240+
displayName: cn
241+
givenName: givenName
242+
surname: sn
243+
tls: # <12>
244+
verification:
245+
server:
246+
caCert:
247+
secretClass: openldap-tls # <13>
248+
cache: # optional, enabled by default
249+
entryTimeToLive: 60s # optional, defaults to 60s
250+
----
251+
<1> Enables the OpenLDAP backend
252+
<2> The hostname of the LDAP server
253+
<3> The port of the LDAP server. Defaults to `636` for LDAPS, or `389` for plain LDAP
254+
<4> The base distinguished name to search. Users outside of this will not be seen
255+
<5> Configuration for LDAP bind credentials
256+
<6> The name of the SecretClass that provides the bind credentials. The secret must contain `user` and `password` keys
257+
<7> LDAP attribute used for the user's unique identifier. Defaults to `entryUUID`
258+
<8> LDAP attribute used for the username. Defaults to `uid`
259+
<9> LDAP search base for groups. If not specified, uses the main `searchBase`
260+
<10> LDAP attribute on group objects that contains member references. Use `member` for `groupOfNames` (default) or `memberUid` for `posixGroup`
261+
<11> Arbitrary LDAP attributes can be requested to be fetched and returned in the user info response. Use this to map custom LDAP attributes to custom attribute names in the response
262+
<12> Optional TLS configuration for secure LDAP connections
263+
<13> The name of the SecretClass that contains the LDAP server's root CA certificate(s)
264+
265+
When retrieving user information from OpenLDAP, the user info fetcher first searches for the user by the `userNameAttribute` (defaults to `uid`) or `userIdAttribute` (defaults to `entryUUID`) depending on the request type. When a user is found, it searches for groups containing the user:
266+
267+
* If `groupMemberAttribute` is `memberUid`: Searches for groups where `memberUid` equals the username (for `posixGroup`)
268+
* Otherwise (e.g. if `groupMemberAttribute` is `member`, which is the default): Searches for groups where `member` equals the user's full DN (for `groupOfNames`)
269+
209270
== User info fetcher API
210271

211272
User information can be retrieved from regorules using the functions `userInfoByUsername(username)` and `userInfoById(id)` in `data.stackable.opa.userinfo.v1`.
@@ -229,7 +290,7 @@ NOTE: The exact formats of `id` and `groups` will vary depending on the xref:#ba
229290
=== Debug request
230291

231292
To debug the user-info-fetcher you can `curl` it's API for a given user.
232-
To achieve this shell into the `user-info-fetcher` container and execute
293+
To achieve this shell into the `opa` container and execute
233294

234295
[source,bash]
235296
----

rust/operator-binary/src/controller.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ use stackable_operator::{
3333
secret_class::{SecretClassVolume, SecretClassVolumeScope},
3434
tls_verification::{TlsClientDetails, TlsClientDetailsError},
3535
},
36+
crd::authentication::ldap,
3637
k8s_openapi::{
3738
DeepMerge,
3839
api::{
@@ -313,6 +314,11 @@ pub enum Error {
313314
))]
314315
UserInfoFetcherTlsVolumeAndMounts { source: TlsClientDetailsError },
315316

317+
#[snafu(display(
318+
"failed to build volume or volume mount spec for the User Info Fetcher LDAP config"
319+
))]
320+
UserInfoFetcherLdapVolumeAndMounts { source: ldap::v1alpha1::Error },
321+
316322
#[snafu(display("failed to configure logging"))]
317323
ConfigureLogging { source: LoggingError },
318324

@@ -1072,6 +1078,12 @@ fn build_server_rolegroup_daemonset(
10721078
.add_volumes_and_mounts(&mut pb, vec![&mut cb_user_info_fetcher])
10731079
.context(UserInfoFetcherTlsVolumeAndMountsSnafu)?;
10741080
}
1081+
user_info_fetcher::v1alpha1::Backend::OpenLdap(openldap) => {
1082+
openldap
1083+
.to_ldap_provider()
1084+
.add_volumes_and_mounts(&mut pb, vec![&mut cb_user_info_fetcher])
1085+
.context(UserInfoFetcherLdapVolumeAndMountsSnafu)?;
1086+
}
10751087
}
10761088

10771089
pb.add_container(cb_user_info_fetcher.build());

0 commit comments

Comments
 (0)