Skip to content

Commit 3a4cc64

Browse files
committed
Allow AD integration to filter LDAP groups
Fixes #691
1 parent 1cf815c commit 3a4cc64

File tree

4 files changed

+54
-6
lines changed

4 files changed

+54
-6
lines changed

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,15 @@ spec:
7777
default: {}
7878
description: Custom attributes, and their LDAP attribute names.
7979
type: object
80+
customGroupAttributeFilters:
81+
additionalProperties:
82+
type: string
83+
default: {}
84+
description: |-
85+
Attributes that groups must have to be returned.
86+
87+
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.
88+
type: object
8089
kerberosSecretClassName:
8190
description: The name of the Kerberos SecretClass.
8291
type: string

rust/crd/src/user_info_fetcher.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,13 @@ pub struct ActiveDirectoryBackend {
112112
/// Custom attributes, and their LDAP attribute names.
113113
#[serde(default)]
114114
pub custom_attribute_mappings: BTreeMap<String, String>,
115+
116+
/// Attributes that groups must have to be returned.
117+
///
118+
/// These fields will be spliced into an LDAP Search Query, so wildcards can be used,
119+
/// but characters with a special meaning in LDAP will need to be escaped.
120+
#[serde(default)]
121+
pub custom_group_attribute_filters: BTreeMap<String, String>,
115122
}
116123

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

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

Lines changed: 37 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+
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+
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+
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+
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+
group_attribute_filters: &BTreeMap<String, String>,
181198
) -> Result<UserInfo, Error> {
182199
let user_sid = user
183200
.bin_attrs
@@ -242,7 +259,8 @@ 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(ldap, base_dn, user, user_sid, group_attribute_filters)
263+
.await?
246264
} else {
247265
tracing::debug!(user.dn, "user has no SID, cannot fetch groups...");
248266
Vec::new()
@@ -257,12 +275,13 @@ async fn user_attributes(
257275
}
258276

259277
/// Gets the distinguished names of all of `user`'s groups, both primary and secondary.
260-
#[tracing::instrument(skip(ldap, base_dn, user, user_sid))]
278+
#[tracing::instrument(skip(ldap, base_dn, user, user_sid, group_attribute_filters))]
261279
async fn user_group_distinguished_names(
262280
ldap: &mut Ldap,
263281
base_dn: &str,
264282
user: &SearchEntry,
265283
user_sid: &SecurityId,
284+
group_attribute_filters: &BTreeMap<String, String>,
266285
) -> Result<Vec<String>, Error> {
267286
// User group memberships are tricky, because users have exactly one *primary* and any number of *secondary* groups.
268287
// Additionally groups can be members of other groups.
@@ -309,10 +328,22 @@ async fn user_group_distinguished_names(
309328
"({LDAP_FIELD_GROUP_MEMBER}{LDAP_MATCHING_RULE_IN_CHAIN}=<SID={primary_group_sid}>)"
310329
);
311330

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

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ async fn get_user_info(
298298
&ad.tls,
299299
&ad.base_distinguished_name,
300300
&ad.custom_attribute_mappings,
301+
&ad.custom_group_attribute_filters,
301302
)
302303
.await
303304
.context(get_user_info_error::ActiveDirectorySnafu),

0 commit comments

Comments
 (0)