Skip to content

Commit f70a89a

Browse files
authored
Allow AD integration to filter LDAP groups (#693)
* Allow AD integration to filter LDAP groups Fixes #691 * Rename `customGroupAttributeFilters` to `additionalGroupAttributeFilters` * Docs * Changelog
1 parent e691eb1 commit f70a89a

File tree

6 files changed

+69
-10
lines changed

6 files changed

+69
-10
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file.
99
- Run a `containerdebug` process in the background of each OPA container to collect debugging information ([#666]).
1010
- Added support for OPA `1.0.x` ([#677]) and ([#687]).
1111
- Aggregate emitted Kubernetes events on the CustomResources ([#675]).
12+
- Added support for filtering groups searched by Active Directory ([#693]).
1213

1314
### Removed
1415

@@ -23,6 +24,7 @@ All notable changes to this project will be documented in this file.
2324
[#675]: https://github.com/stackabletech/opa-operator/pull/675
2425
[#677]: https://github.com/stackabletech/opa-operator/pull/677
2526
[#687]: https://github.com/stackabletech/opa-operator/pull/687
27+
[#693]: https://github.com/stackabletech/opa-operator/pull/693
2628

2729
## [24.11.1] - 2025-01-10
2830

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,15 @@ spec:
6868
experimentalActiveDirectory:
6969
description: Backend that fetches user information from Active Directory
7070
properties:
71+
additionalGroupAttributeFilters:
72+
additionalProperties:
73+
type: string
74+
default: {}
75+
description: |-
76+
Attributes that groups must have to be returned.
77+
78+
These fields will be spliced into an LDAP Search Query, so wildcards can be used, but characters with a special meaning in LDAP will need to be escaped.
79+
type: object
7180
baseDistinguishedName:
7281
description: The root Distinguished Name (DN) where users and groups are located.
7382
type: string

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,14 @@ spec:
8989
baseDistinguishedName: DC=sble,DC=test # <3>
9090
customAttributeMappings: # <4>
9191
country: c # <5>
92-
kerberosSecretClassName: kerberos-ad # <6>
92+
additionalGroupAttributeFilters: # <6>
93+
foo: bar
94+
kerberosSecretClassName: kerberos-ad # <7>
9395
tls:
9496
verification:
9597
server:
9698
caCert:
97-
secretClass: tls-ad # <7>
99+
secretClass: tls-ad # <8>
98100
cache: # optional, enabled by default
99101
entryTimeToLive: 60s # optional, defaults to 60s
100102
----
@@ -103,8 +105,9 @@ spec:
103105
<3> The distinguished name to search, users and groups outside of this will not be seen
104106
<4> Arbitrary LDAP attributes can be requested to be fetched
105107
<5> https://learn.microsoft.com/en-us/windows/win32/ad/address-book-properties[`c`] stores the ISO-3166 country code of the user
106-
<6> The name of the SecretClass that knows how to create Kerberos keytabs trusted by Active Directory
107-
<7> The name of the SecretClass that contains the Active Directory's root CA certificate(s)
108+
<6> Groups can be filtered by LDAP attributes to reduce the load in searching for membership. `*` can be used as a wildcard in these filters.
109+
<7> The name of the SecretClass that knows how to create Kerberos keytabs trusted by Active Directory
110+
<8> The name of the SecretClass that contains the Active Directory's root CA certificate(s)
108111

109112
== User info fetcher API
110113

rust/operator-binary/src/crd/user_info_fetcher.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,13 @@ pub mod versioned {
101101
/// Custom attributes, and their LDAP attribute names.
102102
#[serde(default)]
103103
pub custom_attribute_mappings: BTreeMap<String, String>,
104+
105+
/// Attributes that groups must have to be returned.
106+
///
107+
/// These fields will be spliced into an LDAP Search Query, so wildcards can be used,
108+
/// but characters with a special meaning in LDAP will need to be escaped.
109+
#[serde(default)]
110+
pub additional_group_attribute_filters: BTreeMap<String, String>,
104111
}
105112

106113
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]

rust/user-info-fetcher/src/backend/active_directory.rs

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::{
22
collections::{BTreeMap, HashMap},
3-
fmt::Display,
3+
fmt::{Display, Write},
44
io::{Cursor, Read},
55
num::ParseIntError,
66
str::FromStr,
@@ -90,13 +90,19 @@ const LDAP_FIELD_USER_NAME: &str = "userPrincipalName";
9090
const LDAP_FIELD_USER_PRIMARY_GROUP_RID: &str = "primaryGroupID";
9191
const LDAP_FIELD_GROUP_MEMBER: &str = "member";
9292

93-
#[tracing::instrument(skip(tls, base_distinguished_name, custom_attribute_mappings))]
93+
#[tracing::instrument(skip(
94+
tls,
95+
base_distinguished_name,
96+
custom_attribute_mappings,
97+
additional_group_attribute_filters,
98+
))]
9499
pub(crate) async fn get_user_info(
95100
request: &UserInfoRequest,
96101
ldap_server: &str,
97102
tls: &TlsClientDetails,
98103
base_distinguished_name: &str,
99104
custom_attribute_mappings: &BTreeMap<String, String>,
105+
additional_group_attribute_filters: &BTreeMap<String, String>,
100106
) -> Result<UserInfo, Error> {
101107
let ldap_tls = utils::tls::configure_native_tls(tls)
102108
.await
@@ -168,16 +174,27 @@ pub(crate) async fn get_user_info(
168174
base_distinguished_name,
169175
&user,
170176
custom_attribute_mappings,
177+
additional_group_attribute_filters,
171178
)
172179
.await
173180
}
174181

175-
#[tracing::instrument(skip(ldap, base_dn, user, custom_attribute_mappings), fields(user.dn))]
182+
#[tracing::instrument(
183+
skip(
184+
ldap,
185+
base_dn,
186+
user,
187+
custom_attribute_mappings,
188+
additional_group_attribute_filters,
189+
),
190+
fields(user.dn),
191+
)]
176192
async fn user_attributes(
177193
ldap: &mut Ldap,
178194
base_dn: &str,
179195
user: &SearchEntry,
180196
custom_attribute_mappings: &BTreeMap<String, String>,
197+
additional_group_attribute_filters: &BTreeMap<String, String>,
181198
) -> Result<UserInfo, Error> {
182199
let user_sid = user
183200
.bin_attrs
@@ -242,7 +259,14 @@ async fn user_attributes(
242259
})
243260
.collect::<HashMap<_, _>>();
244261
let groups = if let Some(user_sid) = &user_sid {
245-
user_group_distinguished_names(ldap, base_dn, user, user_sid).await?
262+
user_group_distinguished_names(
263+
ldap,
264+
base_dn,
265+
user,
266+
user_sid,
267+
additional_group_attribute_filters,
268+
)
269+
.await?
246270
} else {
247271
tracing::debug!(user.dn, "user has no SID, cannot fetch groups...");
248272
Vec::new()
@@ -257,12 +281,13 @@ async fn user_attributes(
257281
}
258282

259283
/// Gets the distinguished names of all of `user`'s groups, both primary and secondary.
260-
#[tracing::instrument(skip(ldap, base_dn, user, user_sid))]
284+
#[tracing::instrument(skip(ldap, base_dn, user, user_sid, additional_group_attribute_filters))]
261285
async fn user_group_distinguished_names(
262286
ldap: &mut Ldap,
263287
base_dn: &str,
264288
user: &SearchEntry,
265289
user_sid: &SecurityId,
290+
additional_group_attribute_filters: &BTreeMap<String, String>,
266291
) -> Result<Vec<String>, Error> {
267292
// User group memberships are tricky, because users have exactly one *primary* and any number of *secondary* groups.
268293
// Additionally groups can be members of other groups.
@@ -309,10 +334,22 @@ async fn user_group_distinguished_names(
309334
"({LDAP_FIELD_GROUP_MEMBER}{LDAP_MATCHING_RULE_IN_CHAIN}=<SID={primary_group_sid}>)"
310335
);
311336

337+
// Users can also specify custom filters via `group_attribute_filters`
338+
let custom_group_filter =
339+
additional_group_attribute_filters
340+
.iter()
341+
.fold(String::new(), |mut out, (k, v)| {
342+
// NOTE: This is technically an LDAP injection vuln, but these are provided statically by the OPA administrator,
343+
// who would be able to do plenty of other harm... (like providing their own OPA images that do whatever they want).
344+
// We could base64 the value to "defuse" it entirely, but that would also prevent using wildcards.
345+
write!(out, "({k}={v})").expect("string concatenation is infallible");
346+
out
347+
});
348+
312349
// Let's put it all together, and make it go...
313350
let groups_filter =
314351
format!("(|{primary_group_filter}{primary_group_parents_filter}{secondary_groups_filter})");
315-
let groups_query_filter = format!("(&(objectClass=group){groups_filter})");
352+
let groups_query_filter = format!("(&(objectClass=group){custom_group_filter}{groups_filter})");
316353
let requested_group_attrs = [LDAP_FIELD_OBJECT_DISTINGUISHED_NAME];
317354
tracing::debug!(
318355
groups_query_filter,

rust/user-info-fetcher/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ async fn get_user_info(
300300
&ad.tls,
301301
&ad.base_distinguished_name,
302302
&ad.custom_attribute_mappings,
303+
&ad.additional_group_attribute_filters,
303304
)
304305
.await
305306
.context(get_user_info_error::ActiveDirectorySnafu)

0 commit comments

Comments
 (0)