Skip to content

Commit 4bb9307

Browse files
authored
feat: Add experimental Entra user-info-fetcher backend (#712)
* wip - compiling * wip * wip * cleanup & test * remove comments * adapt entra name * set correct default * merge main * fmt * test fix entra endpoint * fmt * split token and host endpoint * fix group response * clippy * use url in entra endpoint * regenerate charts * extend test, improve erros * use with_context * use with context 2 * clippy * change endpoint to hostname in CRD * consolidate naming * change entrabackend to use url consistently * remove obsolete tests * reduce tests further * add documentation * adapted changelog * fix typo * make port optional * clippy
1 parent bd9464f commit 4bb9307

File tree

8 files changed

+495
-4
lines changed

8 files changed

+495
-4
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ All notable changes to this project will be documented in this file.
1111
- Use `--file-log-rotation-period` (or `FILE_LOG_ROTATION_PERIOD`) to configure the frequency of rotation.
1212
- Use `--console-log-format` (or `CONSOLE_LOG_FORMAT`) to set the format to `plain` (default) or `json`.
1313
- Log the startup event for bundle-builder and user-info-fetcher ([#703]).
14+
- Support experimental user-info-fetcher Entra backend to fetch user groups ([#712]).
1415

1516
### Changed
1617

@@ -42,6 +43,7 @@ All notable changes to this project will be documented in this file.
4243
[#707]: https://github.com/stackabletech/opa-operator/pull/707
4344
[#709]: https://github.com/stackabletech/opa-operator/pull/709
4445
[#710]: https://github.com/stackabletech/opa-operator/pull/710
46+
[#712]: https://github.com/stackabletech/opa-operator/pull/712
4547
[#715]: https://github.com/stackabletech/opa-operator/pull/715
4648

4749
## [25.3.0] - 2025-03-21

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

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ spec:
6464
- experimentalXfscAas
6565
- required:
6666
- experimentalActiveDirectory
67+
- required:
68+
- experimentalEntra
6769
properties:
6870
experimentalActiveDirectory:
6971
description: Backend that fetches user information from Active Directory
@@ -137,6 +139,81 @@ spec:
137139
- kerberosSecretClassName
138140
- ldapServer
139141
type: object
142+
experimentalEntra:
143+
description: Backend that fetches user information from Microsoft Entra
144+
properties:
145+
clientCredentialsSecret:
146+
description: |-
147+
Name of a Secret that contains client credentials of an Entra account with permissions `User.ReadAll` and `GroupMemberShip.ReadAll`.
148+
149+
Must contain the fields `clientId` and `clientSecret`.
150+
type: string
151+
port:
152+
description: Port of the identity provider. If TLS is used defaults to `443`, otherwise to `80`.
153+
format: uint16
154+
minimum: 0.0
155+
nullable: true
156+
type: integer
157+
tenantId:
158+
description: The Microsoft Entra tenant ID.
159+
type: string
160+
tls:
161+
default:
162+
verification:
163+
server:
164+
caCert:
165+
webPki: {}
166+
description: Use a TLS connection. Should usually be set to WebPki.
167+
nullable: true
168+
properties:
169+
verification:
170+
description: The verification method used to verify the certificates of the server and/or the client.
171+
oneOf:
172+
- required:
173+
- none
174+
- required:
175+
- server
176+
properties:
177+
none:
178+
description: Use TLS but don't verify certificates.
179+
type: object
180+
server:
181+
description: Use TLS and a CA certificate to verify the server.
182+
properties:
183+
caCert:
184+
description: CA cert to verify the server.
185+
oneOf:
186+
- required:
187+
- webPki
188+
- required:
189+
- secretClass
190+
properties:
191+
secretClass:
192+
description: Name of the [SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) which will provide the CA certificate. Note that a SecretClass does not need to have a key but can also work with just a CA certificate, so if you got provided with a CA cert but don't have access to the key you can still use this method.
193+
type: string
194+
webPki:
195+
description: Use TLS and the CA certificates trusted by the common web browsers to verify the server. This can be useful when you e.g. use public AWS S3 or other public available services.
196+
type: object
197+
type: object
198+
required:
199+
- caCert
200+
type: object
201+
type: object
202+
required:
203+
- verification
204+
type: object
205+
tokenHostname:
206+
default: login.microsoft.com
207+
description: Hostname of the token provider, defaults to `login.microsoft.com`.
208+
type: string
209+
userInfoHostname:
210+
default: graph.microsoft.com
211+
description: Hostname of the user info provider, defaults to `graph.microsoft.com`.
212+
type: string
213+
required:
214+
- clientCredentialsSecret
215+
- tenantId
216+
type: object
140217
experimentalXfscAas:
141218
description: Backend that fetches user information from the Gaia-X Cross Federation Services Components (XFSC) Authentication & Authorization Service.
142219
properties:

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ Currently the following backends are supported:
5555

5656
* xref:#backend-keycloak[]
5757
* xref:#backend-activedirectory[]
58+
* xref:#backend-entra[]
5859
5960
[#backends]
6061
== Backends
@@ -109,6 +110,29 @@ spec:
109110
<7> The name of the SecretClass that knows how to create Kerberos keytabs trusted by Active Directory
110111
<8> The name of the SecretClass that contains the Active Directory's root CA certificate(s)
111112

113+
[#backend-entra]
114+
=== Entra
115+
116+
WARNING: The Entra backend is experimental, and subject to change.
117+
118+
Fetch groups but not roles for a user from Entra.
119+
120+
NOTE: The client in Entra must use the `client_credentials` flow and requires the `User.ReadAll` and `GroupMemberShip.ReadAll` permissions.
121+
122+
[source,yaml]
123+
----
124+
spec:
125+
clusterConfig:
126+
userInfo:
127+
backend:
128+
experimentalEntra: # <1>
129+
tenantId: 00000000-0000-0000-0000-000000000000 # <2>
130+
clientCredentialsSecret: user-info-fetcher-client-credentials # <3>
131+
----
132+
<1> Enables the Entra backend
133+
<2> The Entra tenant ID
134+
<3> A secret containing the `clientId` and `clientSecret` keys
135+
112136
== User info fetcher API
113137

114138
User information can be retrieved from regorules using the functions `userInfoByUsername(username)` and `userInfoById(id)` in `data.stackable.opa.userinfo.v1`.

rust/operator-binary/src/controller.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ use stackable_operator::{
3131
product_image_selection::ResolvedProductImage,
3232
rbac::build_rbac_resources,
3333
secret_class::{SecretClassVolume, SecretClassVolumeScope},
34-
tls_verification::TlsClientDetailsError,
34+
tls_verification::{TlsClientDetails, TlsClientDetailsError},
3535
},
3636
k8s_openapi::{
3737
DeepMerge,
@@ -1014,6 +1014,29 @@ fn build_server_rolegroup_daemonset(
10141014
.add_volumes_and_mounts(&mut pb, vec![&mut cb_user_info_fetcher])
10151015
.context(UserInfoFetcherTlsVolumeAndMountsSnafu)?;
10161016
}
1017+
user_info_fetcher::v1alpha1::Backend::Entra(entra) => {
1018+
pb.add_volume(
1019+
VolumeBuilder::new(USER_INFO_FETCHER_CREDENTIALS_VOLUME_NAME)
1020+
.secret(SecretVolumeSource {
1021+
secret_name: Some(entra.client_credentials_secret.clone()),
1022+
..Default::default()
1023+
})
1024+
.build(),
1025+
)
1026+
.context(AddVolumeSnafu)?;
1027+
cb_user_info_fetcher
1028+
.add_volume_mount(
1029+
USER_INFO_FETCHER_CREDENTIALS_VOLUME_NAME,
1030+
USER_INFO_FETCHER_CREDENTIALS_DIR,
1031+
)
1032+
.context(AddVolumeMountSnafu)?;
1033+
1034+
TlsClientDetails {
1035+
tls: entra.tls.clone(),
1036+
}
1037+
.add_volumes_and_mounts(&mut pb, vec![&mut cb_user_info_fetcher])
1038+
.context(UserInfoFetcherTlsVolumeAndMountsSnafu)?;
1039+
}
10171040
}
10181041

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

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

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
use std::collections::BTreeMap;
1+
use std::{collections::BTreeMap, str::FromStr};
22

33
use serde::{Deserialize, Serialize};
44
use stackable_operator::{
5-
commons::{networking::HostName, tls_verification::TlsClientDetails},
5+
commons::{
6+
networking::HostName,
7+
tls_verification::{CaCert, Tls, TlsClientDetails, TlsServerVerification, TlsVerification},
8+
},
69
schemars::{self, JsonSchema},
710
time::Duration,
811
versioned::versioned,
@@ -38,6 +41,10 @@ pub mod versioned {
3841
/// Backend that fetches user information from Active Directory
3942
#[serde(rename = "experimentalActiveDirectory")]
4043
ActiveDirectory(v1alpha1::ActiveDirectoryBackend),
44+
45+
/// Backend that fetches user information from Microsoft Entra
46+
#[serde(rename = "experimentalEntra")]
47+
Entra(v1alpha1::EntraBackend),
4148
}
4249

4350
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
@@ -110,6 +117,39 @@ pub mod versioned {
110117
pub additional_group_attribute_filters: BTreeMap<String, String>,
111118
}
112119

120+
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
121+
#[serde(rename_all = "camelCase")]
122+
pub struct EntraBackend {
123+
/// Hostname of the token provider, defaults to `login.microsoft.com`.
124+
#[serde(default = "entra_default_token_hostname")]
125+
pub token_hostname: HostName,
126+
127+
/// Hostname of the user info provider, defaults to `graph.microsoft.com`.
128+
#[serde(default = "entra_default_user_info_hostname")]
129+
pub user_info_hostname: HostName,
130+
131+
/// Port of the identity provider. If TLS is used defaults to `443`, otherwise to `80`.
132+
pub port: Option<u16>,
133+
134+
/// The Microsoft Entra tenant ID.
135+
pub tenant_id: String,
136+
137+
/// Use a TLS connection. Should usually be set to WebPki.
138+
// We do not use the flattened `TlsClientDetails` here since we cannot
139+
// default to WebPki using a default and flatten
140+
// https://github.com/serde-rs/serde/issues/1626
141+
// This means we have to wrap `Tls` in `TlsClientDetails` to use its
142+
// method like `uses_tls()`.
143+
#[serde(default = "default_tls_web_pki")]
144+
pub tls: Option<Tls>,
145+
146+
/// Name of a Secret that contains client credentials of an Entra account with
147+
/// permissions `User.ReadAll` and `GroupMemberShip.ReadAll`.
148+
///
149+
/// Must contain the fields `clientId` and `clientSecret`.
150+
pub client_credentials_secret: String,
151+
}
152+
113153
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
114154
#[serde(rename_all = "camelCase")]
115155
pub struct Cache {
@@ -129,6 +169,22 @@ fn default_root_path() -> String {
129169
"/".to_string()
130170
}
131171

172+
fn entra_default_token_hostname() -> HostName {
173+
HostName::from_str("login.microsoft.com").unwrap()
174+
}
175+
176+
fn entra_default_user_info_hostname() -> HostName {
177+
HostName::from_str("graph.microsoft.com").unwrap()
178+
}
179+
180+
fn default_tls_web_pki() -> Option<Tls> {
181+
Some(Tls {
182+
verification: TlsVerification::Server(TlsServerVerification {
183+
ca_cert: CaCert::WebPki {},
184+
}),
185+
})
186+
}
187+
132188
fn aas_default_port() -> u16 {
133189
5000
134190
}

0 commit comments

Comments
 (0)