Skip to content

feat!: Add OIDC AuthClass provider #680

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 54 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
86d0860
doc: Add fixme notes on commons S3 struct regarding Option<>x
sbernauer Oct 24, 2023
ce826fa
test: Add some smoke tests to ldap AuthClass
sbernauer Oct 24, 2023
7464de8
WIP, gl & hf
sbernauer Oct 24, 2023
de36231
Minor adjustments, add endpoint tests
Techassi Oct 24, 2023
107f8ed
Rename `TlsClientUsage` to `TlsClientDetails`
Techassi Oct 24, 2023
cc4b93e
Add doc comments for `OidcAuthenticationProvider`
Techassi Oct 24, 2023
b1442c1
Add OIDC provider to auth class enum
Techassi Oct 24, 2023
245c2ae
Adjust imports / exports
Techassi Oct 24, 2023
f0bff65
refactor: Prevent miss-use by making certain fields private
sbernauer Oct 24, 2023
bff4a25
Add IPv6 hostname test
Techassi Oct 24, 2023
1276fe2
Add scopes and mandatory Vec<String>
sbernauer Oct 25, 2023
172e4ee
refactor: Make tls property in the authentication providers public again
siegfriedweber Oct 25, 2023
5b1473c
feat: Add helper functions for client env vars
sbernauer Oct 26, 2023
5cfc802
fix: Use hex encoding instead of decimal
sbernauer Oct 26, 2023
1b937f8
Add "provider" attribute
sbernauer Oct 27, 2023
03f07c5
Update OIDC auth provider, add common client/product level auth options
Techassi Nov 15, 2023
61d850b
Move TLS related structs to tls module
Techassi Nov 16, 2023
3f7b5b5
Merge branch 'main' into feat/sso-auth-classes
Techassi Nov 16, 2023
06f5209
Add AuthenticationProvider::new
sbernauer Nov 16, 2023
3b9629c
Add UrlExt trait
Techassi Nov 16, 2023
86175d7
Add UrlExt example
Techassi Nov 16, 2023
256ed37
Rename TLS functions
Techassi Nov 16, 2023
33b73de
Comment out LDAP variant
Techassi Nov 16, 2023
5cbfba2
Add resolve_class function impl
Techassi Nov 17, 2023
f91bfba
fix: Change field visibility
sbernauer Nov 17, 2023
c44535a
Add ClientAuthenticationDetails::authentication_class_name
sbernauer Nov 17, 2023
23eee45
Make ClientAuthenticationDetails::config pub
sbernauer Nov 17, 2023
90bdd2d
fix: clientCredentialsSecret field name
sbernauer Nov 17, 2023
8b60bc2
Add ClientAuthenticationDetails::authentication_class
sbernauer Nov 17, 2023
afeeaa9
Revert "Add ClientAuthenticationDetails::authentication_class"
sbernauer Nov 17, 2023
c3df43a
remove description from CRD
sbernauer Nov 17, 2023
4329b1d
Strip internal Rust docs from ClientAuthenticationConfig
sbernauer Nov 20, 2023
0fce5c7
Switch ClientAuthenticationDetails additional options from enum to si…
sbernauer Nov 20, 2023
ffb4d0f
Add ClientAuthenticationDetails::oidc_or_error
sbernauer Nov 20, 2023
30a4236
docs: spelling in CHANGELOG
NickLarsenNZ Nov 22, 2023
d7bc8dc
Add possibility to add extra fields to oidc config
sbernauer Nov 23, 2023
54e1025
feat: Add new principal_claim field to oidc provider
sbernauer Nov 23, 2023
07c83ec
Merge remote-tracking branch 'origin/main' into feat/sso-auth-classes
sbernauer Nov 23, 2023
ff778a4
fix: Docs
sbernauer Nov 27, 2023
f6f5c7e
docs: Fix typo
sbernauer Nov 27, 2023
1dd11ef
Remove ldap::ClientAuthenticationOptions, which seems to be unused
sbernauer Nov 27, 2023
5687af3
fiux tests
sbernauer Nov 27, 2023
91fc0a6
docs: Document ClientAuthenticationDetails::oidc_or_error
sbernauer Nov 27, 2023
8825aae
Add default for T for ClientAuthenticationOptions
sbernauer Nov 27, 2023
a0428b1
docs: Make less bloated for CRD descriptions
sbernauer Nov 27, 2023
6dd55f1
fix typo
sbernauer Nov 27, 2023
75d7cea
re-order imports
sbernauer Nov 27, 2023
151dcc6
Rename extra_fields_for_product to product_specific_fields
sbernauer Nov 27, 2023
bd701f7
Switch oidc from Option<> to an Option<enum>
sbernauer Nov 27, 2023
2918d85
Revert "Switch oidc from Option<> to an Option<enum>"
sbernauer Nov 28, 2023
11185cc
Merge branch 'main' into feat/sso-auth-classes
sbernauer Nov 30, 2023
ab7b08e
add comment on enum
sbernauer Nov 30, 2023
cc4df3a
Merge branch 'main' into feat/sso-auth-classes
sbernauer Dec 4, 2023
3bad60c
docs: Fix wrong mention of flattening
sbernauer Dec 4, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Added

- Add `oidc::AuthenticationProvider`. This enables users to deploy a new `AuthenticationClass` for OIDC providers like
Keycloak, Okta or Auth0 ([#680]).
- Add a common `ClientAuthenticationDetails` struct, which provides common fields and functions to specify
authentication options on product cluster level. Additionally, the PR also adds `ClientAuthenticationConfig`,
`oidc::ClientAuthenticationOptions`, and `ldap::ClientAuthenticationOptions` ([#680]).

### Changed

- BREAKING: Change the naming of all authentication provider structs. It is now required to import them using the
module. So imports change from `...::authentication::LdapAuthenticationProvider` to
`...::authentication::ldap::AuthenticationProvider` for example ([#680]).
- BREAKING: Move TLS related structs into the `tls` module. Imports need to be adjusted accordingly ([#680]).

[#680]: https://github.com/stackabletech/operator-rs/pull/680

## [0.57.0] - 2023-12-04

### Changed
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] }
tracing = "0.1.37"
tracing-opentelemetry = "0.21.0"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
url = "2.4.1"

[dev-dependencies]
rstest = "0.18.1"
Expand Down
242 changes: 159 additions & 83 deletions src/commons/authentication/ldap.rs
Original file line number Diff line number Diff line change
@@ -1,41 +1,49 @@
use crate::builder::ContainerBuilder;
use crate::commons::authentication::tls::Tls;
use crate::{builder::PodBuilder, commons::secret_class::SecretClassVolume};

use super::tls::{CaCert, TlsServerVerification, TlsVerification};
use k8s_openapi::api::core::v1::{Volume, VolumeMount};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

pub const SECRET_BASE_PATH: &str = "/stackable/secrets";
use crate::{
builder::{ContainerBuilder, PodBuilder, VolumeMountBuilder},
commons::{
authentication::{tls::TlsClientDetails, SECRET_BASE_PATH},
secret_class::SecretClassVolume,
},
};

#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LdapAuthenticationProvider {
pub struct AuthenticationProvider {
/// Hostname of the LDAP server
pub hostname: String,

/// Port of the LDAP server. If TLS is used defaults to 636 otherwise to 389
pub port: Option<u16>,
port: Option<u16>,

/// LDAP search base
#[serde(default)]
pub search_base: String,

/// LDAP query to filter users
#[serde(default)]
pub search_filter: String,

/// The name of the LDAP object fields
#[serde(default)]
pub ldap_field_names: LdapFieldNames,
pub ldap_field_names: FieldNames,

/// In case you need a special account for searching the LDAP server you can specify it here
pub bind_credentials: Option<SecretClassVolume>,
bind_credentials: Option<SecretClassVolume>,

/// Use a TLS connection. If not specified no TLS will be used
pub tls: Option<Tls>,
#[serde(flatten)]
pub tls: TlsClientDetails,
}

impl LdapAuthenticationProvider {
pub const fn default_port(&self) -> u16 {
match self.tls {
None => 389,
Some(_) => 636,
}
impl AuthenticationProvider {
/// Returns the port to be used, which is either user configured or defaulted based upon TLS usage
pub fn port(&self) -> u16 {
self.port
.unwrap_or(if self.tls.uses_tls() { 636 } else { 389 })
}

/// This functions adds
Expand All @@ -46,36 +54,36 @@ impl LdapAuthenticationProvider {
/// This function will handle
///
/// * Bind credentials needed to connect to LDAP server
/// * Tls secret class used to verify the cert of the LDAP server
pub fn add_volumes_and_mounts(
&self,
pod_builder: &mut PodBuilder,
container_builders: Vec<&mut ContainerBuilder>,
) {
let mut mounts: Vec<(String, String)> = Vec::new();
let (volumes, mounts) = self.volumes_and_mounts();
pod_builder.add_volumes(volumes);
for cb in container_builders {
cb.add_volume_mounts(mounts.clone());
}
}

/// It is recommended to use [`Self::add_volumes_and_mounts`], this function returns you the
/// volumes and mounts in case you need to add them by yourself.
pub fn volumes_and_mounts(&self) -> (Vec<Volume>, Vec<VolumeMount>) {
let mut volumes = Vec::new();
let mut mounts = Vec::new();

if let Some(bind_credentials) = &self.bind_credentials {
let secret_class = bind_credentials.secret_class.to_owned();
let secret_class = &bind_credentials.secret_class;
let volume_name = format!("{secret_class}-bind-credentials");

pod_builder.add_volume(bind_credentials.to_volume(&volume_name));
mounts.push((volume_name, secret_class));
volumes.push(bind_credentials.to_volume(&volume_name));
mounts.push(
VolumeMountBuilder::new(volume_name, format!("{SECRET_BASE_PATH}/{secret_class}"))
.build(),
);
}
if let Some(secret_class) = self.tls_ca_cert_secret_class() {
let volume_name = format!("{secret_class}-ca-cert");
let volume = SecretClassVolume {
secret_class: secret_class.to_string(),
scope: None,
}
.to_volume(&volume_name);

pod_builder.add_volume(volume);
mounts.push((volume_name, secret_class));
}
for cb in container_builders {
for (mount, secret_class) in mounts.iter() {
cb.add_volume_mount(mount, format!("{SECRET_BASE_PATH}/{secret_class}"));
}
}
(volumes, mounts)
}

/// Returns the path of the files containing bind user and password.
Expand All @@ -89,66 +97,29 @@ impl LdapAuthenticationProvider {
)
})
}

/// Whether TLS is configured
pub const fn use_tls(&self) -> bool {
self.tls.is_some()
}

/// Whether TLS verification is configured
/// Returns false if TLS itself isn't configured
pub fn use_tls_verification(&self) -> bool {
if let Some(tls) = &self.tls {
tls.verification != TlsVerification::None {}
} else {
false
}
}

/// Returns the path of the ca.crt that should be used to verify the LDAP server certificate
/// if TLS verification with a CA cert from a SecretClass is configured.
pub fn tls_ca_cert_mount_path(&self) -> Option<String> {
self.tls_ca_cert_secret_class()
.map(|secret_class| format!("{SECRET_BASE_PATH}/{secret_class}/ca.crt"))
}

/// Extracts the secret class that provides the CA used to verify the LDAP server certificate.
fn tls_ca_cert_secret_class(&self) -> Option<String> {
if let Some(Tls {
verification:
TlsVerification::Server(TlsServerVerification {
ca_cert: CaCert::SecretClass(secret_class),
}),
}) = &self.tls
{
Some(secret_class.to_owned())
} else {
None
}
}
}

#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LdapFieldNames {
pub struct FieldNames {
/// The name of the username field
#[serde(default = "LdapFieldNames::default_uid")]
#[serde(default = "FieldNames::default_uid")]
pub uid: String,
/// The name of the group field
#[serde(default = "LdapFieldNames::default_group")]
#[serde(default = "FieldNames::default_group")]
pub group: String,
/// The name of the firstname field
#[serde(default = "LdapFieldNames::default_given_name")]
#[serde(default = "FieldNames::default_given_name")]
pub given_name: String,
/// The name of the lastname field
#[serde(default = "LdapFieldNames::default_surname")]
#[serde(default = "FieldNames::default_surname")]
pub surname: String,
/// The name of the email field
#[serde(default = "LdapFieldNames::default_email")]
#[serde(default = "FieldNames::default_email")]
pub email: String,
}

impl LdapFieldNames {
impl FieldNames {
fn default_uid() -> String {
"uid".to_string()
}
Expand All @@ -170,9 +141,9 @@ impl LdapFieldNames {
}
}

impl Default for LdapFieldNames {
impl Default for FieldNames {
fn default() -> Self {
LdapFieldNames {
FieldNames {
uid: Self::default_uid(),
group: Self::default_group(),
given_name: Self::default_given_name(),
Expand All @@ -181,3 +152,108 @@ impl Default for LdapFieldNames {
}
}
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn test_ldap_minimal() {
let ldap = serde_yaml::from_str::<AuthenticationProvider>(
"
hostname: my.ldap.server
",
)
.unwrap();

assert_eq!(ldap.port(), 389);
assert!(!ldap.tls.uses_tls());
assert_eq!(ldap.tls.tls_ca_cert_secret_class(), None);
}

#[test]
fn test_ldap_with_bind_credentials() {
let _ldap = serde_yaml::from_str::<AuthenticationProvider>(
"
hostname: my.ldap.server
port: 389
searchBase: ou=users,dc=example,dc=org
bindCredentials:
secretClass: openldap-bind-credentials
",
)
.unwrap();
}

#[test]
fn test_ldap_full() {
let input = r#"
hostname: my.ldap.server
port: 42
searchBase: ou=users,dc=example,dc=org
bindCredentials:
secretClass: openldap-bind-credentials
tls:
verification:
server:
caCert:
secretClass: ldap-ca-cert
"#;
let deserializer = serde_yaml::Deserializer::from_str(input);
let ldap: AuthenticationProvider =
serde_yaml::with::singleton_map_recursive::deserialize(deserializer).unwrap();

assert_eq!(ldap.port(), 42);
assert!(ldap.tls.uses_tls());
assert_eq!(
ldap.tls.tls_ca_cert_secret_class(),
Some("ldap-ca-cert".to_string())
);
assert_eq!(
ldap.tls.tls_ca_cert_mount_path(),
Some("/stackable/secrets/ldap-ca-cert/ca.crt".to_string())
);
let (tls_volumes, tls_mounts) = ldap.tls.volumes_and_mounts();
assert_eq!(
tls_volumes,
vec![SecretClassVolume {
secret_class: "ldap-ca-cert".to_string(),
scope: None,
}
.to_volume("ldap-ca-cert-ca-cert")]
);
assert_eq!(
tls_mounts,
vec![VolumeMountBuilder::new(
"ldap-ca-cert-ca-cert",
"/stackable/secrets/ldap-ca-cert"
)
.build()]
);

assert_eq!(
ldap.bind_credentials_mount_paths(),
Some((
"/stackable/secrets/openldap-bind-credentials/user".to_string(),
"/stackable/secrets/openldap-bind-credentials/password".to_string()
))
);
let (bind_volumes, bind_mounts) = ldap.volumes_and_mounts();
assert_eq!(
bind_volumes,
vec![SecretClassVolume {
secret_class: "openldap-bind-credentials".to_string(),
scope: None,
}
.to_volume("openldap-bind-credentials-bind-credentials")]
);
assert_eq!(
bind_mounts,
vec![VolumeMountBuilder::new(
"openldap-bind-credentials-bind-credentials",
"/stackable/secrets/openldap-bind-credentials"
)
.build()]
);
}
}
Loading