From 2f4af4dfe44b19daa0ac241379d487e5321e0b5c Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Fri, 21 Jun 2024 14:20:18 +0200 Subject: [PATCH 01/27] fix: Correctly encode user given content, such as passwords (#627) * fix: Correctly encode user given content, such as passwords * changelog * fix bcrypt problems * Update rust/operator-binary/src/security/authentication.rs Co-authored-by: Malte Sander --------- Co-authored-by: Malte Sander --- CHANGELOG.md | 6 + rust/crd/src/authentication.rs | 184 ++++++++++----- rust/crd/src/lib.rs | 8 +- rust/operator-binary/src/config.rs | 85 +++++-- rust/operator-binary/src/controller.rs | 45 ++-- .../operator-binary/src/reporting_task/mod.rs | 4 +- .../src/security/authentication.rs | 220 +++++++++++------- tests/templates/kuttl/ldap/02-assert.yaml | 4 +- .../kuttl/ldap/12-install-nifi.yaml.j2 | 4 +- tests/templates/kuttl/ldap/20-assert.yaml | 2 +- .../templates/kuttl/ldap/create_ldap_user.sh | 4 +- .../kuttl/smoke/30-install-nifi.yaml.j2 | 3 +- tests/templates/kuttl/smoke/60-assert.yaml | 2 +- 13 files changed, 381 insertions(+), 190 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4293b25..88dd14d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Fixed + +- Use [config-utils](https://github.com/stackabletech/config-utils/) for text-replacement of variables in configs. + This fixes escaping problems, especially when you have special characters in your password ([#627]). + ### Added - Support specifying the SecretClass that is used to obtain TLS certificates ([#622]). @@ -19,6 +24,7 @@ All notable changes to this project will be documented in this file. [#616]: https://github.com/stackabletech/nifi-operator/pull/616 [#622]: https://github.com/stackabletech/nifi-operator/pull/622 +[#627]: https://github.com/stackabletech/nifi-operator/pull/627 [#628]: https://github.com/stackabletech/nifi-operator/pull/628 ## [24.3.0] - 2024-03-20 diff --git a/rust/crd/src/authentication.rs b/rust/crd/src/authentication.rs index 930a8f8c..60cc2208 100644 --- a/rust/crd/src/authentication.rs +++ b/rust/crd/src/authentication.rs @@ -1,95 +1,167 @@ -use serde::{Deserialize, Serialize}; -use snafu::{ResultExt, Snafu}; -use stackable_operator::commons::authentication::AuthenticationClassProvider; +use std::future::Future; + +use snafu::{ensure, ResultExt, Snafu}; +use stackable_operator::commons::authentication::oidc::IdentityProviderHint; +use stackable_operator::commons::authentication::{ + ldap, oidc, static_, AuthenticationClassProvider, ClientAuthenticationDetails, +}; use stackable_operator::kube::ResourceExt; use stackable_operator::{ - client::Client, - commons::authentication::AuthenticationClass, + client::Client, commons::authentication::AuthenticationClass, kube::runtime::reflector::ObjectRef, - schemars::{self, JsonSchema}, }; +use tracing::info; + +// The assumed OIDC provider if no hint is given in the AuthClass +pub const DEFAULT_OIDC_PROVIDER: IdentityProviderHint = IdentityProviderHint::Keycloak; + +const SUPPORTED_OIDC_PROVIDERS: &[IdentityProviderHint] = &[IdentityProviderHint::Keycloak]; #[derive(Snafu, Debug)] pub enum Error { - #[snafu(display("Failed to retrieve AuthenticationClass {authentication_class}"))] - AuthenticationClassRetrieval { + #[snafu(display("failed to retrieve AuthenticationClass"))] + AuthenticationClassRetrievalFailed { source: stackable_operator::client::Error, - authentication_class: ObjectRef, }, + #[snafu(display("The nifi-operator does not support running Nifi without any authentication. Please provide a AuthenticationClass to use."))] NoAuthenticationNotSupported {}, + #[snafu(display("The nifi-operator does not support multiple AuthenticationClasses simultaneously. Please provide a single AuthenticationClass to use."))] MultipleAuthenticationClassesNotSupported {}, + #[snafu(display("The nifi-operator does not support the AuthenticationClass provider [{authentication_class_provider}] from AuthenticationClass [{authentication_class}]."))] AuthenticationClassProviderNotSupported { authentication_class_provider: String, authentication_class: ObjectRef, }, + #[snafu(display("Nifi doesn't support skipping the LDAP TLS verification of the AuthenticationClass {authentication_class}"))] NoLdapTlsVerificationNotSupported { authentication_class: ObjectRef, }, + #[snafu(display("invalid OIDC configuration"))] + OidcConfigurationInvalid { + source: stackable_operator::commons::authentication::Error, + }, + #[snafu(display("the OIDC provider {oidc_provider:?} is not yet supported (AuthenticationClass {auth_class_name:?})"))] + OidcProviderNotSupported { + auth_class_name: String, + oidc_provider: String, + }, } type Result = std::result::Result; -#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct NifiAuthenticationClassRef { - /// Name of the [AuthenticationClass](DOCS_BASE_URL_PLACEHOLDER/concepts/authentication) used to authenticate users. - /// Supported providers are `static` and `ldap`. - /// For `static` the "admin" user needs to be present in the referenced secret, and only this user will be added to NiFi, other users are ignored. - pub authentication_class: String, +#[derive(Clone)] +pub enum AuthenticationClassResolved { + Static { + provider: static_::AuthenticationProvider, + }, + Ldap { + provider: ldap::AuthenticationProvider, + }, + Oidc { + provider: oidc::AuthenticationProvider, + oidc: oidc::ClientAuthenticationOptions<()>, + }, } -/// Retrieve all provided `AuthenticationClass` references. -pub async fn resolve_authentication_classes( - client: &Client, - authentication_class_refs: &Vec, -) -> Result> { - let mut resolved_auth_classes = vec![]; - - match authentication_class_refs.len() { - 0 => NoAuthenticationNotSupportedSnafu.fail()?, - 1 => {} - _ => MultipleAuthenticationClassesNotSupportedSnafu.fail()?, +impl AuthenticationClassResolved { + pub async fn from( + auth_details: &[ClientAuthenticationDetails], + client: &Client, + ) -> Result> { + let resolve_auth_class = |auth_details: ClientAuthenticationDetails| async move { + auth_details.resolve_class(client).await + }; + AuthenticationClassResolved::resolve(auth_details, resolve_auth_class).await } - for auth_class in authentication_class_refs { - let resolved_auth_class = - AuthenticationClass::resolve(client, &auth_class.authentication_class) + /// Retrieve all provided `AuthenticationClass` references. + pub async fn resolve( + auth_details: &[ClientAuthenticationDetails], + resolve_auth_class: impl Fn(ClientAuthenticationDetails) -> R, + ) -> Result> + where + R: Future>, + { + let mut resolved_auth_classes = vec![]; + + match auth_details.len() { + 0 => NoAuthenticationNotSupportedSnafu.fail()?, + 1 => {} + _ => MultipleAuthenticationClassesNotSupportedSnafu.fail()?, + } + + for entry in auth_details { + let auth_class = resolve_auth_class(entry.clone()) .await - .context(AuthenticationClassRetrievalSnafu { - authentication_class: ObjectRef::::new( - &auth_class.authentication_class, - ), - })?; - - let resolved_auth_class_name = resolved_auth_class.name_any(); - - match &resolved_auth_class.spec.provider { - AuthenticationClassProvider::Static(_) => {} - AuthenticationClassProvider::Ldap(ldap) => { - if ldap.tls.uses_tls() && !ldap.tls.uses_tls_verification() { - NoLdapTlsVerificationNotSupportedSnafu { - authentication_class: ObjectRef::::new( - &resolved_auth_class_name, - ), + .context(AuthenticationClassRetrievalFailedSnafu)?; + + let auth_class_name = auth_class.name_any(); + + match &auth_class.spec.provider { + AuthenticationClassProvider::Static(provider) => { + resolved_auth_classes.push(AuthenticationClassResolved::Static { + provider: provider.to_owned(), + }) + } + AuthenticationClassProvider::Ldap(provider) => { + if provider.tls.uses_tls() && !provider.tls.uses_tls_verification() { + NoLdapTlsVerificationNotSupportedSnafu { + authentication_class: ObjectRef::::new( + &auth_class_name, + ), + } + .fail()? } - .fail()? + resolved_auth_classes.push(AuthenticationClassResolved::Ldap { + provider: provider.to_owned(), + }) } - } - _ => AuthenticationClassProviderNotSupportedSnafu { - authentication_class_provider: resolved_auth_class.spec.provider.to_string(), - authentication_class: ObjectRef::::new( - &resolved_auth_class_name, + AuthenticationClassProvider::Oidc(provider) => resolved_auth_classes.push( + AuthenticationClassResolved::from_oidc(&auth_class_name, provider, entry)?, ), + _ => AuthenticationClassProviderNotSupportedSnafu { + authentication_class_provider: auth_class.spec.provider.to_string(), + authentication_class: ObjectRef::::new(&auth_class_name), + } + .fail()?, + }; + } + + Ok(resolved_auth_classes) + } + + fn from_oidc( + auth_class_name: &str, + provider: &oidc::AuthenticationProvider, + auth_details: &ClientAuthenticationDetails, + ) -> Result { + let oidc_provider = match &provider.provider_hint { + None => { + info!("No OIDC provider hint given in AuthClass {auth_class_name}, assuming {default_oidc_provider_name}", + default_oidc_provider_name = serde_json::to_string(&DEFAULT_OIDC_PROVIDER).unwrap()); + DEFAULT_OIDC_PROVIDER } - .fail()?, + Some(oidc_provider) => oidc_provider.to_owned(), }; - resolved_auth_classes.push(resolved_auth_class); - } + ensure!( + SUPPORTED_OIDC_PROVIDERS.contains(&oidc_provider), + OidcProviderNotSupportedSnafu { + auth_class_name, + oidc_provider: serde_json::to_string(&oidc_provider).unwrap(), + } + ); - Ok(resolved_auth_classes) + Ok(AuthenticationClassResolved::Oidc { + provider: provider.to_owned(), + oidc: auth_details + .oidc_or_error(auth_class_name) + .context(OidcConfigurationInvalidSnafu)? + .clone(), + }) + } } diff --git a/rust/crd/src/lib.rs b/rust/crd/src/lib.rs index dc982b1e..cf44be38 100644 --- a/rust/crd/src/lib.rs +++ b/rust/crd/src/lib.rs @@ -2,14 +2,13 @@ pub mod affinity; pub mod authentication; pub mod tls; -use crate::authentication::NifiAuthenticationClassRef; - use affinity::get_affinity; use serde::{Deserialize, Serialize}; use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ commons::{ affinity::StackableAffinity, + authentication::ClientAuthenticationDetails, cluster_operation::ClusterOperation, product_image_selection::ProductImage, resources::{ @@ -18,8 +17,7 @@ use stackable_operator::{ }, }, config::{ - fragment::Fragment, - fragment::{self, ValidationError}, + fragment::{self, Fragment, ValidationError}, merge::Merge, }, k8s_openapi::{api::core::v1::Volume, apimachinery::pkg::api::resource::Quantity}, @@ -114,7 +112,7 @@ pub struct NifiClusterConfig { /// Authentication options for NiFi (required). /// Read more about authentication in the [security documentation](DOCS_BASE_URL_PLACEHOLDER/nifi/usage_guide/security). // We don't add `#[serde(default)]` here, as we require authentication - pub authentication: Vec, + pub authentication: Vec, /// TLS configuration options for the server. #[serde(default)] diff --git a/rust/operator-binary/src/config.rs b/rust/operator-binary/src/config.rs index c4a318a3..41748490 100644 --- a/rust/operator-binary/src/config.rs +++ b/rust/operator-binary/src/config.rs @@ -10,7 +10,10 @@ use stackable_nifi_crd::{ PROTOCOL_PORT, }; use stackable_operator::{ - commons::resources::Resources, + commons::{ + authentication::oidc::{AuthenticationProvider, DEFAULT_OIDC_WELLKNOWN_PATH}, + resources::Resources, + }, memory::{BinaryMultiple, MemoryQuantity}, product_config_utils::{ transform_all_roles_to_config, validate_all_roles_and_groups_config, @@ -22,7 +25,9 @@ use strum::{Display, EnumIter}; use crate::{ operations::graceful_shutdown::graceful_shutdown_config_properties, - security::authentication::{STACKABLE_SERVER_TLS_DIR, STACKABLE_TLS_STORE_PASSWORD}, + security::authentication::{ + NifiAuthenticationConfig, STACKABLE_SERVER_TLS_DIR, STACKABLE_TLS_STORE_PASSWORD, + }, }; pub const NIFI_CONFIG_DIRECTORY: &str = "/stackable/nifi/conf"; @@ -82,6 +87,10 @@ pub enum Error { source: stackable_operator::memory::Error, repo: NifiRepository, }, + #[snafu(display("Invalid OIDC endpoint URL"))] + InvalidOidcEndpoint { + source: stackable_operator::commons::authentication::oidc::Error, + }, } /// Create the NiFi bootstrap.conf @@ -178,6 +187,7 @@ pub fn build_nifi_properties( spec: &NifiSpec, resource_config: &Resources, proxy_hosts: &str, + auth_config: &NifiAuthenticationConfig, overrides: BTreeMap, ) -> Result { let mut properties = BTreeMap::new(); @@ -474,10 +484,10 @@ pub fn build_nifi_properties( ); //############################################# - - // This will be replaced in the init container, so 0.0.0.0 acts mainly as - // marker - properties.insert("nifi.web.https.host".to_string(), "0.0.0.0".to_string()); + properties.insert( + "nifi.web.https.host".to_string(), + "${env:NODE_ADDRESS}".to_string(), + ); properties.insert("nifi.web.https.port".to_string(), HTTPS_PORT.to_string()); properties.insert( "nifi.web.https.network.interface.default".to_string(), @@ -492,9 +502,10 @@ pub fn build_nifi_properties( properties.insert("nifi.web.proxy.context.path".to_string(), "".to_string()); properties.insert("nifi.web.proxy.host".to_string(), proxy_hosts.to_string()); - // security properties - // this property is later set from a secret, so can remain empty here - properties.insert("nifi.sensitive.props.key".to_string(), "".to_string()); + properties.insert( + "nifi.sensitive.props.key".to_string(), + "${file:UTF-8:/stackable/sensitiveproperty/nifiSensitivePropsKey}".to_string(), + ); properties.insert( "nifi.sensitive.props.key.protected".to_string(), "".to_string(), @@ -560,10 +571,49 @@ pub fn build_nifi_properties( "nifi.cluster.protocol.is.secure".to_string(), "true".to_string(), ); + + if let NifiAuthenticationConfig::Oidc { provider, oidc } = auth_config { + let endpoint_url = provider + .endpoint_url() + .context(InvalidOidcEndpointSnafu)? + .to_string(); + properties.insert( + "nifi.security.user.oidc.discovery.url".to_string(), + format!("{endpoint_url}/{DEFAULT_OIDC_WELLKNOWN_PATH}"), + ); + + let (oidc_client_id_env, oidc_client_secret_env) = + AuthenticationProvider::client_credentials_env_names( + &oidc.client_credentials_secret_ref, + ); + properties.insert( + "nifi.security.user.oidc.client.id".to_string(), + format!("${{env:{oidc_client_id_env}}}").to_string(), + ); + + properties.insert( + "nifi.security.user.oidc.client.secret".to_string(), + format!("${{env:{oidc_client_secret_env}}}").to_string(), + ); + + let scopes = provider.scopes.join(","); + properties.insert( + "nifi.security.user.oidc.additional.scopes".to_string(), + format!("[{scopes}]").to_string(), + ); + + properties.insert( + "nifi.security.user.oidc.claim.identifying.user".to_string(), + provider.principal_claim.to_string(), + ); + } + // cluster node properties (only configure for cluster nodes) properties.insert("nifi.cluster.is.node".to_string(), "true".to_string()); - // this will be overwritten to the correct FQDN in the container start command - properties.insert("nifi.cluster.node.address".to_string(), "".to_string()); + properties.insert( + "nifi.cluster.node.address".to_string(), + "${env:NODE_ADDRESS}".to_string(), + ); properties.insert( "nifi.cluster.node.protocol.port".to_string(), PROTOCOL_PORT.to_string(), @@ -581,10 +631,13 @@ pub fn build_nifi_properties( // this will be replaced via a container command script properties.insert( "nifi.zookeeper.connect.string".to_string(), - "xxxxxx".to_string(), + "${env:ZOOKEEPER_HOSTS}".to_string(), ); // this will be replaced via a container command script - properties.insert("nifi.zookeeper.root.node".to_string(), "xxxxxx".to_string()); + properties.insert( + "nifi.zookeeper.root.node".to_string(), + "${env:ZOOKEEPER_CHROOT}".to_string(), + ); // override with config overrides properties.extend(overrides); @@ -593,8 +646,6 @@ pub fn build_nifi_properties( } pub fn build_state_management_xml() -> String { - // The "xxxxxx" Connect String is a placeholder and will be replaced via container - // command script format!( " @@ -609,8 +660,8 @@ pub fn build_state_management_xml() -> String { zk-provider org.apache.nifi.controller.state.providers.zookeeper.ZooKeeperStateProvider - xxxxxx - yyyyyy + ${{env:ZOOKEEPER_HOSTS}} + ${{env:ZOOKEEPER_CHROOT}} 10 seconds Open diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 361f7a74..ef6151dc 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -14,7 +14,7 @@ use product_config::{ }; use snafu::{OptionExt, ResultExt, Snafu}; use stackable_nifi_crd::{ - authentication::resolve_authentication_classes, Container, CurrentlySupportedListenerClasses, + authentication::AuthenticationClassResolved, Container, CurrentlySupportedListenerClasses, NifiCluster, NifiConfig, NifiConfigFragment, NifiRole, NifiStatus, APP_NAME, BALANCE_PORT, BALANCE_PORT_NAME, HTTPS_PORT, HTTPS_PORT_NAME, MAX_NIFI_LOG_FILES_SIZE, MAX_PREPARE_LOG_FILE_SIZE, METRICS_PORT, METRICS_PORT_NAME, PROTOCOL_PORT, PROTOCOL_PORT_NAME, @@ -459,7 +459,7 @@ pub async fn reconcile_nifi(nifi: Arc, ctx: Arc) -> Result Result { tracing::debug!("building rolegroup configmaps"); - let (login_identity_provider_xml, authorizers_xml) = nifi_auth_config.get_auth_config(); + let (login_identity_provider_xml, authorizers_xml) = nifi_auth_config + .get_auth_config() + .context(InvalidNifiAuthenticationConfigSnafu)?; let jvm_sec_props: BTreeMap> = rolegroup_config .get(&PropertyNameKind::File( @@ -746,6 +748,7 @@ async fn build_node_rolegroup_config_map( &nifi.spec, &merged_config.resources, proxy_hosts, + nifi_auth_config, rolegroup_config .get(&PropertyNameKind::File(NIFI_PROPERTIES.to_string())) .with_context(|| ProductConfigKindNotSpecifiedSnafu { @@ -863,9 +866,6 @@ async fn build_node_rolegroup_statefulset( tracing::debug!("Building statefulset"); let role_group = role.role_groups.get(&rolegroup_ref.role_group); - let zookeeper_host = "ZOOKEEPER_HOSTS"; - let zookeeper_chroot = "ZOOKEEPER_CHROOT"; - // get env vars and env overrides let mut env_vars: Vec = rolegroup_config .get(&PropertyNameKind::Env) @@ -894,12 +894,12 @@ async fn build_node_rolegroup_statefulset( }); env_vars.push(zookeeper_env_var( - zookeeper_host, + "ZOOKEEPER_HOSTS", &nifi.spec.cluster_config.zookeeper_config_map_name, )); env_vars.push(zookeeper_env_var( - zookeeper_chroot, + "ZOOKEEPER_CHROOT", &nifi.spec.cluster_config.zookeeper_config_map_name, )); @@ -911,7 +911,7 @@ async fn build_node_rolegroup_statefulset( .metadata .namespace .as_ref() - .with_context(|| ObjectHasNoNamespaceSnafu {})? + .context(ObjectHasNoNamespaceSnafu)? ); let sensitive_key_secret = &nifi.spec.cluster_config.sensitive_properties.key_secret; @@ -943,24 +943,21 @@ async fn build_node_rolegroup_statefulset( "echo Replacing config directory".to_string(), "cp /conf/* /stackable/nifi/conf".to_string(), "ln -sf /stackable/log_config/logback.xml /stackable/nifi/conf/logback.xml".to_string(), - "echo Replacing nifi.cluster.node.address in nifi.properties".to_string(), - format!("sed -i \"s/nifi.cluster.node.address=/nifi.cluster.node.address={}/g\" /stackable/nifi/conf/nifi.properties", node_address), - "echo Replacing nifi.web.https.host in nifi.properties".to_string(), - format!("sed -i \"s/nifi.web.https.host=0.0.0.0/nifi.web.https.host={}/g\" /stackable/nifi/conf/nifi.properties", node_address), - "echo Replacing nifi.sensitive.props.key in nifi.properties".to_string(), - "sed -i \"s|nifi.sensitive.props.key=|nifi.sensitive.props.key=$(cat /stackable/sensitiveproperty/nifiSensitivePropsKey)|g\" /stackable/nifi/conf/nifi.properties".to_string(), - "echo Replacing 'nifi.zookeeper.connect.string=xxxxxx' in /stackable/nifi/conf/nifi.properties".to_string(), - format!("sed -i \"s|nifi.zookeeper.connect.string=xxxxxx|nifi.zookeeper.connect.string=${{{}}}|g\" /stackable/nifi/conf/nifi.properties", zookeeper_host), - "echo Replacing 'nifi.zookeeper.root.node=xxxxxx' in /stackable/nifi/conf/nifi.properties".to_string(), - format!("sed -i \"s|nifi.zookeeper.root.node=xxxxxx|nifi.zookeeper.root.node=${{{}}}|g\" /stackable/nifi/conf/nifi.properties", zookeeper_chroot), - "echo Replacing connect string 'xxxxxx' in /stackable/nifi/conf/state-management.xml".to_string(), - format!("sed -i \"s|xxxxxx|${{{}}}|g\" /stackable/nifi/conf/state-management.xml", zookeeper_host), - "echo Replacing root node 'yyyyyy' in /stackable/nifi/conf/state-management.xml".to_string(), - format!("sed -i \"s|yyyyyy|${{{}}}|g\" /stackable/nifi/conf/state-management.xml",zookeeper_chroot) + format!("export NODE_ADDRESS=\"{node_address}\""), ]); + // This commands needs to go first, as they might set env variables needed by the templating prepare_args.extend_from_slice(nifi_auth_config.get_additional_container_args().as_slice()); + prepare_args.extend(vec![ + "echo Templating config files".to_string(), + "config-utils template /stackable/nifi/conf/nifi.properties".to_string(), + "config-utils template /stackable/nifi/conf/state-management.xml".to_string(), + "config-utils template /stackable/nifi/conf/login-identity-providers.xml".to_string(), + "config-utils template /stackable/nifi/conf/authorizers.xml".to_string(), + "config-utils template /stackable/nifi/conf/security.properties".to_string(), + ]); + let mut container_prepare = ContainerBuilder::new(&prepare_container_name).with_context(|_| { IllegalContainerNameSnafu { @@ -1404,7 +1401,7 @@ async fn get_proxy_hosts( .addresses .unwrap_or_default() }) - .map(|node_address| format!("{}:{}", node_address.address, external_port)), + .map(|node_address| format!("{}:{external_port}", node_address.address)), ); } diff --git a/rust/operator-binary/src/reporting_task/mod.rs b/rust/operator-binary/src/reporting_task/mod.rs index 0dc38aa9..0657c179 100644 --- a/rust/operator-binary/src/reporting_task/mod.rs +++ b/rust/operator-binary/src/reporting_task/mod.rs @@ -46,7 +46,7 @@ use stackable_operator::{ }; use crate::security::{ - authentication::{NifiAuthenticationConfig, STACKABLE_ADMIN_USER_NAME}, + authentication::{NifiAuthenticationConfig, STACKABLE_ADMIN_USERNAME}, build_tls_volume, }; @@ -249,7 +249,7 @@ fn build_reporting_task_job( let user_name_command = if admin_username_file.is_empty() { // In case of the username being simple (e.g admin for SingleUser) just use it as is - format!("-u {STACKABLE_ADMIN_USER_NAME}") + format!("-u {STACKABLE_ADMIN_USERNAME}") } else { // If the username is a bind dn (e.g. cn=integrationtest,ou=my users,dc=example,dc=org) we have to extract the cn/dn/uid (in this case integrationtest) format!( diff --git a/rust/operator-binary/src/security/authentication.rs b/rust/operator-binary/src/security/authentication.rs index a5137869..a71a8e57 100644 --- a/rust/operator-binary/src/security/authentication.rs +++ b/rust/operator-binary/src/security/authentication.rs @@ -1,20 +1,16 @@ use indoc::{formatdoc, indoc}; use snafu::{OptionExt, ResultExt, Snafu}; +use stackable_nifi_crd::authentication::AuthenticationClassResolved; use stackable_operator::builder::pod::{container::ContainerBuilder, PodBuilder}; +use stackable_operator::commons::authentication::oidc::{self, ClientAuthenticationOptions}; use stackable_operator::commons::authentication::{ldap, static_}; -use stackable_operator::commons::authentication::{ - AuthenticationClass, AuthenticationClassProvider, -}; + use stackable_operator::k8s_openapi::api::core::v1::{KeyToPath, SecretVolumeSource, Volume}; -pub const STACKABLE_ADMIN_USER_NAME: &str = "admin"; +pub const STACKABLE_ADMIN_USERNAME: &str = "admin"; const STACKABLE_USER_VOLUME_MOUNT_PATH: &str = "/stackable/users"; -const STACKABLE_SINGLE_USER_PASSWORD_PLACEHOLDER: &str = "xxx_singleuser_password_xxx"; -const STACKABLE_LDAP_BIND_USER_NAME_PLACEHOLDER: &str = "xxx_ldap_bind_username_xxx"; -const STACKABLE_LDAP_BIND_USER_PASSWORD_PLACEHOLDER: &str = "xxx_ldap_bind_password_xxx"; - pub const LOGIN_IDENTITY_PROVIDERS_XML_FILE_NAME: &str = "login-identity-providers.xml"; pub const AUTHORIZERS_XML_FILE_NAME: &str = "authorizers.xml"; @@ -31,22 +27,38 @@ pub enum Error { authentication_class_provider: String, }, - #[snafu(display( - "there was an error adding LDAP Volumes and VolumeMounts to the Pod and containers" - ))] + #[snafu(display("Failed to add LDAP volumes and volumeMounts to the Pod and containers"))] AddLdapVolumes { source: stackable_operator::commons::authentication::ldap::Error, }, + + #[snafu(display("Failed to add OIDC volumes and volumeMounts to the Pod and containers"))] + AddOidcVolumes { + source: stackable_operator::commons::authentication::tls::TlsClientDetailsError, + }, + + #[snafu(display( + "The LDAP AuthenticationClass is missing the bind credentials. Currently the NiFi operator only supports connecting to LDAP servers using bind credentials" + ))] + LdapAuthenticationClassMissingBindCredentials {}, } #[allow(clippy::large_enum_variant)] pub enum NifiAuthenticationConfig { - SingleUser(static_::AuthenticationProvider), - Ldap(ldap::AuthenticationProvider), + SingleUser { + provider: static_::AuthenticationProvider, + }, + Ldap { + provider: ldap::AuthenticationProvider, + }, + Oidc { + provider: oidc::AuthenticationProvider, + oidc: ClientAuthenticationOptions, + }, } impl NifiAuthenticationConfig { - pub fn get_auth_config(&self) -> (String, String) { + pub fn get_auth_config(&self) -> Result<(String, String), Error> { let mut login_identity_provider_xml = indoc! {r#" @@ -59,15 +71,16 @@ impl NifiAuthenticationConfig { .to_string(); match &self { - Self::SingleUser(_) => { + Self::SingleUser { .. } => { login_identity_provider_xml.push_str(&formatdoc! {r#" login-identity-provider org.apache.nifi.authentication.single.user.SingleUserLoginIdentityProvider - {STACKABLE_ADMIN_USER_NAME} - {STACKABLE_SINGLE_USER_PASSWORD_PLACEHOLDER} + {STACKABLE_ADMIN_USERNAME} + ${{env:STACKABLE_ADMIN_PASSWORD}} - "#}); + "#, + }); authorizers_xml.push_str(indoc! {r#" @@ -76,9 +89,12 @@ impl NifiAuthenticationConfig { "#}); } - Self::Ldap(ldap) => { - login_identity_provider_xml.push_str(&get_ldap_login_identity_provider(ldap)); - authorizers_xml.push_str(&get_ldap_authorizer(ldap)); + Self::Ldap { provider } => { + login_identity_provider_xml.push_str(&get_ldap_login_identity_provider(&provider)?); + authorizers_xml.push_str(&get_ldap_authorizer(&provider)?); + } + Self::Oidc { .. } => { + authorizers_xml.push_str(&get_oidc_authorizer()?); } } @@ -89,19 +105,19 @@ impl NifiAuthenticationConfig { "#}); - (login_identity_provider_xml, authorizers_xml) + Ok((login_identity_provider_xml, authorizers_xml)) } pub fn get_user_and_password_file_paths(&self) -> (String, String) { let mut admin_username_file = String::new(); let mut admin_password_file = String::new(); match &self { - Self::SingleUser(_) => { + Self::SingleUser { .. } | Self::Oidc { .. } => { admin_password_file = - format!("{STACKABLE_USER_VOLUME_MOUNT_PATH}/{STACKABLE_ADMIN_USER_NAME}"); + format!("{STACKABLE_USER_VOLUME_MOUNT_PATH}/{STACKABLE_ADMIN_USERNAME}"); } - Self::Ldap(ldap) => { - if let Some((user_path, password_path)) = ldap.bind_credentials_mount_paths() { + Self::Ldap { provider } => { + if let Some((user_path, password_path)) = provider.bind_credentials_mount_paths() { admin_username_file = user_path; admin_password_file = password_path; } @@ -113,30 +129,26 @@ impl NifiAuthenticationConfig { pub fn get_additional_container_args(&self) -> Vec { let mut commands = Vec::new(); match &self { - Self::SingleUser(_) => { + Self::SingleUser { .. } => { let (_, admin_password_file) = self.get_user_and_password_file_paths(); commands.extend(vec![ - format!("echo 'Replacing {STACKABLE_ADMIN_USER_NAME} password in {LOGIN_IDENTITY_PROVIDERS_XML_FILE_NAME} (if configured)'"), - format!("sed -i \"s|{STACKABLE_SINGLE_USER_PASSWORD_PLACEHOLDER}|$(cat {admin_password_file} | java -jar /bin/stackable-bcrypt.jar)|g\" /stackable/nifi/conf/{LOGIN_IDENTITY_PROVIDERS_XML_FILE_NAME}"), - ] - ); + format!("export STACKABLE_ADMIN_PASSWORD=\"$(cat {admin_password_file} | java -jar /bin/stackable-bcrypt.jar)\""), + ]); } - Self::Ldap(ldap) => { - if let Some((username_path, password_path)) = ldap.bind_credentials_mount_paths() { - commands.extend(vec![ - format!("echo Replacing ldap bind username and password in {LOGIN_IDENTITY_PROVIDERS_XML_FILE_NAME}"), - format!("sed -i \"s|{STACKABLE_LDAP_BIND_USER_NAME_PLACEHOLDER}|$(cat {username_path})|g\" /stackable/nifi/conf/{LOGIN_IDENTITY_PROVIDERS_XML_FILE_NAME}"), - format!("sed -i \"s|{STACKABLE_LDAP_BIND_USER_PASSWORD_PLACEHOLDER}|$(cat {password_path})|g\" /stackable/nifi/conf/{LOGIN_IDENTITY_PROVIDERS_XML_FILE_NAME}"), - format!("sed -i \"s|{STACKABLE_LDAP_BIND_USER_NAME_PLACEHOLDER}|$(cat {username_path})|g\" /stackable/nifi/conf/{AUTHORIZERS_XML_FILE_NAME}"), - ] - ); - } - if let Some(ca_path) = ldap.tls.tls_ca_cert_mount_path() { + Self::Ldap { provider } => { + if let Some(ca_path) = provider.tls.tls_ca_cert_mount_path() { commands.extend(vec![ "echo Adding LDAP tls cert to global truststore".to_string(), format!("keytool -importcert -file {ca_path} -keystore {STACKABLE_SERVER_TLS_DIR}/truststore.p12 -storetype pkcs12 -noprompt -alias ldap_ca_cert -storepass {STACKABLE_TLS_STORE_PASSWORD}"), - ] - ); + ]); + } + } + Self::Oidc { provider, .. } => { + if let Some(ca_path) = provider.tls.tls_ca_cert_mount_path() { + commands.extend(vec![ + "echo Adding OIDC tls cert to global truststore".to_string(), + format!("keytool -importcert -file {ca_path} -keystore {STACKABLE_SERVER_TLS_DIR}/truststore.p12 -storetype pkcs12 -noprompt -alias oidc_ca_cert -storepass {STACKABLE_TLS_STORE_PASSWORD}"), + ]); } } } @@ -151,15 +163,15 @@ impl NifiAuthenticationConfig { container_builders: Vec<&mut ContainerBuilder>, ) -> Result<(), Error> { match &self { - Self::SingleUser(provider) => { + Self::SingleUser { provider } => { let admin_volume = Volume { - name: STACKABLE_ADMIN_USER_NAME.to_string(), + name: STACKABLE_ADMIN_USERNAME.to_string(), secret: Some(SecretVolumeSource { secret_name: Some(provider.user_credentials_secret.name.to_string()), optional: Some(false), items: Some(vec![KeyToPath { - key: STACKABLE_ADMIN_USER_NAME.to_string(), - path: STACKABLE_ADMIN_USER_NAME.to_string(), + key: STACKABLE_ADMIN_USERNAME.to_string(), + path: STACKABLE_ADMIN_USERNAME.to_string(), ..KeyToPath::default() }]), ..SecretVolumeSource::default() @@ -169,46 +181,51 @@ impl NifiAuthenticationConfig { pod_builder.add_volume(admin_volume); for cb in container_builders { - cb.add_volume_mount( - STACKABLE_ADMIN_USER_NAME, - STACKABLE_USER_VOLUME_MOUNT_PATH, - ); + cb.add_volume_mount(STACKABLE_ADMIN_USERNAME, STACKABLE_USER_VOLUME_MOUNT_PATH); } } - Self::Ldap(ldap) => { - ldap.add_volumes_and_mounts(pod_builder, container_builders) + Self::Ldap { provider } => { + provider + .add_volumes_and_mounts(pod_builder, container_builders) .context(AddLdapVolumesSnafu)?; } + Self::Oidc { provider, .. } => { + provider + .tls + .add_volumes_and_mounts(pod_builder, container_builders) + .context(AddOidcVolumesSnafu)?; + } } Ok(()) } - pub fn try_from(auth_classes: Vec) -> Result { + pub fn try_from( + auth_classes_resolved: Vec, + ) -> Result { // Currently only one auth mechanism is supported in NiFi. This is checked in // rust/crd/src/authentication.rs and just a fail-safe here. For Future changes, // this is not just a "from" without error handling - let auth_class = auth_classes + let auth_class_resolved = auth_classes_resolved .first() .context(SingleAuthenticationMechanismSupportedSnafu)?; - match &auth_class.spec.provider { - AuthenticationClassProvider::Static(static_provider) => { - Ok(Self::SingleUser(static_provider.clone())) - } - AuthenticationClassProvider::Ldap(ldap_provider) => { - Ok(Self::Ldap(ldap_provider.clone())) - } - AuthenticationClassProvider::Tls(_) | AuthenticationClassProvider::Oidc(_) => { - Err(Error::AuthenticationClassProviderNotSupported { - authentication_class_provider: auth_class.spec.provider.to_string(), - }) - } + match &auth_class_resolved { + AuthenticationClassResolved::Static { provider } => Ok(Self::SingleUser { + provider: provider.clone(), + }), + AuthenticationClassResolved::Ldap { provider } => Ok(Self::Ldap { + provider: provider.clone(), + }), + AuthenticationClassResolved::Oidc { provider, oidc } => Ok(Self::Oidc { + provider: provider.clone(), + oidc: oidc.clone(), + }), } } } -fn get_ldap_login_identity_provider(ldap: &ldap::AuthenticationProvider) -> String { +fn get_ldap_login_identity_provider(ldap: &ldap::AuthenticationProvider) -> Result { let mut search_filter = ldap.search_filter.clone(); // If no search_filter is specified we will set a default filter that just searches for the user logging in using the specified uid field name @@ -217,14 +234,18 @@ fn get_ldap_login_identity_provider(ldap: &ldap::AuthenticationProvider) -> Stri .push_str(format!("{uidField}={{0}}", uidField = ldap.ldap_field_names.uid).as_str()); } - formatdoc! {r#" + let (username_file, password_file) = ldap + .bind_credentials_mount_paths() + .context(LdapAuthenticationClassMissingBindCredentialsSnafu)?; + + Ok(formatdoc! {r#" login-identity-provider org.apache.nifi.ldap.LdapProvider {authentication_strategy} - {STACKABLE_LDAP_BIND_USER_NAME_PLACEHOLDER} - {STACKABLE_LDAP_BIND_USER_PASSWORD_PLACEHOLDER} + ${{file:UTF-8:{username_file}}} + ${{file:UTF-8:{password_file}}} THROW 10 secs @@ -266,11 +287,52 @@ fn get_ldap_login_identity_provider(ldap: &ldap::AuthenticationProvider) -> Stri port = ldap.port(), search_base = ldap.search_base, keystore_path = STACKABLE_SERVER_TLS_DIR, - } + }) +} + +fn get_ldap_authorizer(ldap: &ldap::AuthenticationProvider) -> Result { + let (username_file, _) = ldap + .bind_credentials_mount_paths() + .context(LdapAuthenticationClassMissingBindCredentialsSnafu)?; + + Ok(formatdoc! {r#" + + file-user-group-provider + org.apache.nifi.authorization.FileUserGroupProvider + ./conf/users.xml + + + + ${{file:UTF-8:{username_file}}} + + + CN=generated certificate for pod + + + + file-access-policy-provider + org.apache.nifi.authorization.FileAccessPolicyProvider + file-user-group-provider + ./conf/authorizations.xml + + + + ${{file:UTF-8:{username_file}}} + + + CN=generated certificate for pod + + + + authorizer + org.apache.nifi.authorization.StandardManagedAuthorizer + file-access-policy-provider + + "#}) } -fn get_ldap_authorizer(_ldap: &ldap::AuthenticationProvider) -> String { - formatdoc! {r#" +fn get_oidc_authorizer() -> Result { + Ok(formatdoc! {r#" file-user-group-provider org.apache.nifi.authorization.FileUserGroupProvider @@ -278,10 +340,11 @@ fn get_ldap_authorizer(_ldap: &ldap::AuthenticationProvider) -> String { - {STACKABLE_LDAP_BIND_USER_NAME_PLACEHOLDER} + admin CN=generated certificate for pod + CN=api_client, OU=**, O=**, ST=**, C=** @@ -292,9 +355,10 @@ fn get_ldap_authorizer(_ldap: &ldap::AuthenticationProvider) -> String { - {STACKABLE_LDAP_BIND_USER_NAME_PLACEHOLDER} + admin + CN=api_client, OU=dastc, O=**, L=**, ST=**, C=** CN=generated certificate for pod @@ -303,5 +367,5 @@ fn get_ldap_authorizer(_ldap: &ldap::AuthenticationProvider) -> String { org.apache.nifi.authorization.StandardManagedAuthorizer file-access-policy-provider - "#} + "#}) } diff --git a/tests/templates/kuttl/ldap/02-assert.yaml b/tests/templates/kuttl/ldap/02-assert.yaml index 9801dd92..46bea753 100644 --- a/tests/templates/kuttl/ldap/02-assert.yaml +++ b/tests/templates/kuttl/ldap/02-assert.yaml @@ -2,5 +2,5 @@ apiVersion: kuttl.dev/v1beta1 kind: TestAssert commands: - - script: kubectl exec -n $NAMESPACE openldap-0 -- ldapsearch -H ldap://localhost:1389 -D "cn=integrationtest,ou=my users,dc=example,dc=org" -w integrationtest -b "ou=my users,dc=example,dc=org" > /dev/null - - script: kubectl exec -n $NAMESPACE openldap-0 -- bash -c LDAPTLS_CACERT=/tls/ca.crt ldapsearch -Z -H ldaps://localhost:1636 -D "cn=integrationtest,ou=my users,dc=example,dc=org" -w integrationtest -b "ou=my users,dc=example,dc=org" > /dev/null + - script: kubectl exec -n $NAMESPACE openldap-0 -- ldapsearch -H ldap://localhost:1389 -D "cn=integrationtest,ou=my users,dc=example,dc=org" -w 'bindPasswordWithSpecialCharacter\@<&>"'"'" -b "ou=my users,dc=example,dc=org" > /dev/null + - script: kubectl exec -n $NAMESPACE openldap-0 -- bash -c LDAPTLS_CACERT=/tls/ca.crt ldapsearch -Z -H ldaps://localhost:1636 -D "cn=integrationtest,ou=my users,dc=example,dc=org" -w 'bindPasswordWithSpecialCharacter\@<&>"'"'" -b "ou=my users,dc=example,dc=org" > /dev/null diff --git a/tests/templates/kuttl/ldap/12-install-nifi.yaml.j2 b/tests/templates/kuttl/ldap/12-install-nifi.yaml.j2 index 6bbd18d9..c480a8ea 100644 --- a/tests/templates/kuttl/ldap/12-install-nifi.yaml.j2 +++ b/tests/templates/kuttl/ldap/12-install-nifi.yaml.j2 @@ -14,7 +14,8 @@ metadata: secrets.stackable.tech/class: nifi-with-ldap-bind stringData: user: cn=integrationtest,ou=my users,dc=example,dc=org - password: integrationtest + password: > + bindPasswordWithSpecialCharacter\@<&>"' --- apiVersion: nifi.stackable.tech/v1alpha1 kind: NifiCluster @@ -42,6 +43,7 @@ spec: vectorAggregatorConfigMapName: vector-aggregator-discovery {% endif %} zookeeperConfigMapName: nifi-with-ldap-znode + listenerClass: external-unstable nodes: config: logging: diff --git a/tests/templates/kuttl/ldap/20-assert.yaml b/tests/templates/kuttl/ldap/20-assert.yaml index ceaeaf2a..9ffd54c0 100644 --- a/tests/templates/kuttl/ldap/20-assert.yaml +++ b/tests/templates/kuttl/ldap/20-assert.yaml @@ -3,4 +3,4 @@ apiVersion: kuttl.dev/v1beta1 kind: TestAssert timeout: 300 commands: - - script: kubectl exec -n $NAMESPACE test-nifi-0 -- python /tmp/test_nifi.py -u integrationtest -p integrationtest -n $NAMESPACE -c 2 + - script: kubectl exec -n $NAMESPACE test-nifi-0 -- python /tmp/test_nifi.py -u integrationtest -p 'bindPasswordWithSpecialCharacter\@<&>"'"'" -n $NAMESPACE -c 2 diff --git a/tests/templates/kuttl/ldap/create_ldap_user.sh b/tests/templates/kuttl/ldap/create_ldap_user.sh index 2353d60d..6c51cd03 100755 --- a/tests/templates/kuttl/ldap/create_ldap_user.sh +++ b/tests/templates/kuttl/ldap/create_ldap_user.sh @@ -4,7 +4,7 @@ # ldapsearch -H ldap://localhost:1389 -D "cn=admin,dc=example,dc=org" -w admin -b "ou=my users,dc=example,dc=org" # To check the new user -# ldapsearch -H ldap://localhost:1389 -D "cn=integrationtest,ou=my users,dc=example,dc=org" -w integrationtest -b "ou=my users,dc=example,dc=org" +# ldapsearch -H ldap://localhost:1389 -D "cn=integrationtest,ou=my users,dc=example,dc=org" -w 'bindPasswordWithSpecialCharacter\@<&>"'"'" -b "ou=my users,dc=example,dc=org" cat << 'EOF' | ldapadd -H ldap://localhost:1389 -D "cn=admin,dc=example,dc=org" -w admin dn: ou=my users,dc=example,dc=org @@ -33,4 +33,4 @@ shadowMax: 0 shadowWarning: 0 EOF -ldappasswd -H ldap://localhost:1389 -D "cn=admin,dc=example,dc=org" -w admin -s integrationtest "cn=integrationtest,ou=my users,dc=example,dc=org" +ldappasswd -H ldap://localhost:1389 -D "cn=admin,dc=example,dc=org" -w admin -s 'bindPasswordWithSpecialCharacter\@<&>"'"'" "cn=integrationtest,ou=my users,dc=example,dc=org" diff --git a/tests/templates/kuttl/smoke/30-install-nifi.yaml.j2 b/tests/templates/kuttl/smoke/30-install-nifi.yaml.j2 index 81457eb0..5ef028fb 100644 --- a/tests/templates/kuttl/smoke/30-install-nifi.yaml.j2 +++ b/tests/templates/kuttl/smoke/30-install-nifi.yaml.j2 @@ -14,7 +14,8 @@ kind: Secret metadata: name: simple-nifi-admin-credentials stringData: - admin: supersecretpassword + admin: > + passwordWithSpecialCharacter\@<&>"' --- apiVersion: v1 kind: Secret diff --git a/tests/templates/kuttl/smoke/60-assert.yaml b/tests/templates/kuttl/smoke/60-assert.yaml index fc283dbc..1d531af0 100644 --- a/tests/templates/kuttl/smoke/60-assert.yaml +++ b/tests/templates/kuttl/smoke/60-assert.yaml @@ -3,5 +3,5 @@ apiVersion: kuttl.dev/v1beta1 kind: TestAssert timeout: 300 commands: - - script: kubectl exec -n $NAMESPACE test-nifi-0 -- python /tmp/test_nifi.py -u admin -p supersecretpassword -n $NAMESPACE -c 3 + - script: kubectl exec -n $NAMESPACE test-nifi-0 -- python /tmp/test_nifi.py -u admin -p 'passwordWithSpecialCharacter\@<&>"'"'" -n $NAMESPACE -c 3 - script: kubectl exec -n $NAMESPACE test-nifi-0 -- python /tmp/test_nifi_metrics.py -n $NAMESPACE From a31fde1b90e1191ff8a06de1615ca1dc027c9f6a Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Fri, 28 Jun 2024 20:41:01 +0200 Subject: [PATCH 02/27] add vscode debugging profile --- .vscode/launch.json | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..c310a034 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'stackable-nifi-operator'", + "cargo": { + "args": [ + "build", + "--bin=stackable-nifi-operator", + "--package=stackable-nifi-operator" + ], + "filter": { + "name": "stackable-nifi-operator", + "kind": "bin" + } + }, + "args": ["run"], + "cwd": "${workspaceFolder}" + } + ] +} From c612bf400e1ebbc96174c46d6f36393a25832cb0 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Fri, 28 Jun 2024 20:42:36 +0200 Subject: [PATCH 03/27] wip: add integration test for oidc --- tests/templates/kuttl/oidc/00-assert.yaml.j2 | 10 + ...tor-aggregator-discovery-configmap.yaml.j2 | 9 + .../templates/kuttl/oidc/00-patch-ns.yaml.j2 | 9 + tests/templates/kuttl/oidc/01-assert.yaml | 14 ++ .../kuttl/oidc/01-install-keycloak.yaml | 15 ++ .../templates/kuttl/oidc/01_keycloak.yaml.j2 | 195 ++++++++++++++++++ tests/templates/kuttl/oidc/03-assert.yaml | 12 ++ .../kuttl/oidc/03-install-test-nifi.yaml | 22 ++ tests/templates/kuttl/oidc/10-assert.yaml | 12 ++ .../kuttl/oidc/10-install-zk.yaml.j2 | 28 +++ .../11-create-authentication-classes.yaml.j2 | 8 + .../oidc/11_authentication-classes.yaml.j2 | 27 +++ tests/templates/kuttl/oidc/12-assert.yaml | 12 ++ .../templates/kuttl/oidc/12-install-nifi.yaml | 5 + tests/templates/kuttl/oidc/12_nifi.yaml.j2 | 50 +++++ tests/test-definition.yaml | 6 + 16 files changed, 434 insertions(+) create mode 100644 tests/templates/kuttl/oidc/00-assert.yaml.j2 create mode 100644 tests/templates/kuttl/oidc/00-install-vector-aggregator-discovery-configmap.yaml.j2 create mode 100644 tests/templates/kuttl/oidc/00-patch-ns.yaml.j2 create mode 100644 tests/templates/kuttl/oidc/01-assert.yaml create mode 100644 tests/templates/kuttl/oidc/01-install-keycloak.yaml create mode 100644 tests/templates/kuttl/oidc/01_keycloak.yaml.j2 create mode 100644 tests/templates/kuttl/oidc/03-assert.yaml create mode 100644 tests/templates/kuttl/oidc/03-install-test-nifi.yaml create mode 100644 tests/templates/kuttl/oidc/10-assert.yaml create mode 100644 tests/templates/kuttl/oidc/10-install-zk.yaml.j2 create mode 100644 tests/templates/kuttl/oidc/11-create-authentication-classes.yaml.j2 create mode 100644 tests/templates/kuttl/oidc/11_authentication-classes.yaml.j2 create mode 100644 tests/templates/kuttl/oidc/12-assert.yaml create mode 100644 tests/templates/kuttl/oidc/12-install-nifi.yaml create mode 100644 tests/templates/kuttl/oidc/12_nifi.yaml.j2 diff --git a/tests/templates/kuttl/oidc/00-assert.yaml.j2 b/tests/templates/kuttl/oidc/00-assert.yaml.j2 new file mode 100644 index 00000000..50b1d4c3 --- /dev/null +++ b/tests/templates/kuttl/oidc/00-assert.yaml.j2 @@ -0,0 +1,10 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +{% if lookup('env', 'VECTOR_AGGREGATOR') %} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: vector-aggregator-discovery +{% endif %} diff --git a/tests/templates/kuttl/oidc/00-install-vector-aggregator-discovery-configmap.yaml.j2 b/tests/templates/kuttl/oidc/00-install-vector-aggregator-discovery-configmap.yaml.j2 new file mode 100644 index 00000000..2d6a0df5 --- /dev/null +++ b/tests/templates/kuttl/oidc/00-install-vector-aggregator-discovery-configmap.yaml.j2 @@ -0,0 +1,9 @@ +{% if lookup('env', 'VECTOR_AGGREGATOR') %} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: vector-aggregator-discovery +data: + ADDRESS: {{ lookup('env', 'VECTOR_AGGREGATOR') }} +{% endif %} diff --git a/tests/templates/kuttl/oidc/00-patch-ns.yaml.j2 b/tests/templates/kuttl/oidc/00-patch-ns.yaml.j2 new file mode 100644 index 00000000..67185acf --- /dev/null +++ b/tests/templates/kuttl/oidc/00-patch-ns.yaml.j2 @@ -0,0 +1,9 @@ +{% if test_scenario['values']['openshift'] == 'true' %} +# see https://github.com/stackabletech/issues/issues/566 +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: kubectl patch namespace $NAMESPACE -p '{"metadata":{"labels":{"pod-security.kubernetes.io/enforce":"privileged"}}}' + timeout: 120 +{% endif %} diff --git a/tests/templates/kuttl/oidc/01-assert.yaml b/tests/templates/kuttl/oidc/01-assert.yaml new file mode 100644 index 00000000..5f3fae52 --- /dev/null +++ b/tests/templates/kuttl/oidc/01-assert.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +metadata: + name: test-keycloak +timeout: 480 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: keycloak +status: + readyReplicas: 1 + replicas: 1 diff --git a/tests/templates/kuttl/oidc/01-install-keycloak.yaml b/tests/templates/kuttl/oidc/01-install-keycloak.yaml new file mode 100644 index 00000000..4e07a328 --- /dev/null +++ b/tests/templates/kuttl/oidc/01-install-keycloak.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: | + INSTANCE_NAME=keycloak \ + REALM=test \ + USERNAME=jane.doe \ + FIRST_NAME=Jane \ + LAST_NAME=Doe \ + EMAIL=jane.doe@stackable.tech \ + PASSWORD=T8mn72D9 \ + CLIENT_ID=nifi \ + CLIENT_SECRET=R1bxHUD569vHeQdw \ + envsubst < 01_keycloak.yaml | kubectl apply -n $NAMESPACE -f - diff --git a/tests/templates/kuttl/oidc/01_keycloak.yaml.j2 b/tests/templates/kuttl/oidc/01_keycloak.yaml.j2 new file mode 100644 index 00000000..7a52415d --- /dev/null +++ b/tests/templates/kuttl/oidc/01_keycloak.yaml.j2 @@ -0,0 +1,195 @@ +# The environment variables must be replaced. +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: $INSTANCE_NAME-realms +data: + test-realm.json: | + { + "realm": "$REALM", + "enabled": true, + "users": [ + { + "enabled": true, + "username": "$USERNAME", + "firstName" : "$FIRST_NAME", + "lastName" : "$LAST_NAME", + "email" : "$EMAIL", + "credentials": [ + { + "type": "password", + "value": "$PASSWORD" + } + ], + "realmRoles": [ + "user" + ] + } + ], + "roles": { + "realm": [ + { + "name": "user", + "description": "User privileges" + } + ] + }, + "clients": [ + { + "clientId": "$CLIENT_ID", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "$CLIENT_SECRET", + "redirectUris": [ + "*" + ], + "webOrigins": [ + "*" + ], + "standardFlowEnabled": true, + "protocol": "openid-connect" + } + ] + } +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: $INSTANCE_NAME + labels: + app: $INSTANCE_NAME +spec: + replicas: 1 + selector: + matchLabels: + app: $INSTANCE_NAME + template: + metadata: + labels: + app: $INSTANCE_NAME + spec: + serviceAccountName: keycloak + containers: + - name: keycloak + image: quay.io/keycloak/keycloak:23.0.4 + args: + - start-dev + - --import-realm +{% if test_scenario['values']['oidc-use-tls'] == 'true' %} + - --https-certificate-file=/tls/tls.crt + - --https-certificate-key-file=/tls/tls.key +{% endif %} + env: + - name: KEYCLOAK_ADMIN + value: admin + - name: KEYCLOAK_ADMIN_PASSWORD + value: admin + ports: +{% if test_scenario['values']['oidc-use-tls'] == 'true' %} + - name: https + containerPort: 8443 +{% else %} + - name: http + containerPort: 8080 +{% endif %} + volumeMounts: + - name: realms + mountPath: /opt/keycloak/data/import + - name: tls + mountPath: /tls + readinessProbe: + httpGet: + path: /realms/$REALM +{% if test_scenario['values']['oidc-use-tls'] == 'true' %} + port: 8443 + scheme: HTTPS +{% else %} + port: 8080 + scheme: HTTP +{% endif %} + volumes: + - name: realms + configMap: + name: $INSTANCE_NAME-realms + - ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/class: tls + secrets.stackable.tech/scope: service=$INSTANCE_NAME + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: "1" + storageClassName: secrets.stackable.tech + volumeMode: Filesystem + name: tls +--- +apiVersion: v1 +kind: Service +metadata: + name: $INSTANCE_NAME +spec: + selector: + app: $INSTANCE_NAME + ports: + - protocol: TCP +{% if test_scenario['values']['oidc-use-tls'] == 'true' %} + port: 8443 +{% else %} + port: 8080 +{% endif %} +--- +apiVersion: authentication.stackable.tech/v1alpha1 +kind: AuthenticationClass +metadata: + name: $INSTANCE_NAME-$NAMESPACE +spec: + provider: + oidc: + hostname: $INSTANCE_NAME.$NAMESPACE.svc.cluster.local + port: 8443 + rootPath: /realms/$REALM + scopes: + - email + - openid + - profile + principalClaim: preferred_username + providerHint: Keycloak + tls: + verification: + server: + caCert: + secretClass: tls +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: keycloak +--- +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: keycloak +{% if test_scenario['values']['openshift'] == 'true' %} +rules: +- apiGroups: ["security.openshift.io"] + resources: ["securitycontextconstraints"] + resourceNames: ["privileged"] + verbs: ["use"] +{% endif %} +--- +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: keycloak +subjects: + - kind: ServiceAccount + name: keycloak +roleRef: + kind: Role + name: keycloak + apiGroup: rbac.authorization.k8s.io diff --git a/tests/templates/kuttl/oidc/03-assert.yaml b/tests/templates/kuttl/oidc/03-assert.yaml new file mode 100644 index 00000000..d511ff46 --- /dev/null +++ b/tests/templates/kuttl/oidc/03-assert.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 300 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: test-nifi +status: + readyReplicas: 1 + replicas: 1 diff --git a/tests/templates/kuttl/oidc/03-install-test-nifi.yaml b/tests/templates/kuttl/oidc/03-install-test-nifi.yaml new file mode 100644 index 00000000..5e2edf95 --- /dev/null +++ b/tests/templates/kuttl/oidc/03-install-test-nifi.yaml @@ -0,0 +1,22 @@ +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: test-nifi + labels: + app: test-nifi +spec: + replicas: 1 + selector: + matchLabels: + app: test-nifi + template: + metadata: + labels: + app: test-nifi + spec: + containers: + - name: test-nifi + image: docker.stackable.tech/stackable/testing-tools:0.2.0-stackable0.0.0-dev + command: ["sleep", "infinity"] + terminationGracePeriodSeconds: 1 diff --git a/tests/templates/kuttl/oidc/10-assert.yaml b/tests/templates/kuttl/oidc/10-assert.yaml new file mode 100644 index 00000000..e0766c49 --- /dev/null +++ b/tests/templates/kuttl/oidc/10-assert.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 600 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: test-zk-server-default +status: + readyReplicas: 1 + replicas: 1 diff --git a/tests/templates/kuttl/oidc/10-install-zk.yaml.j2 b/tests/templates/kuttl/oidc/10-install-zk.yaml.j2 new file mode 100644 index 00000000..df1776b8 --- /dev/null +++ b/tests/templates/kuttl/oidc/10-install-zk.yaml.j2 @@ -0,0 +1,28 @@ +--- +apiVersion: zookeeper.stackable.tech/v1alpha1 +kind: ZookeeperCluster +metadata: + name: test-zk +spec: + image: + productVersion: "{{ test_scenario['values']['zookeeper-latest'] }}" + pullPolicy: IfNotPresent +{% if lookup('env', 'VECTOR_AGGREGATOR') %} + clusterConfig: + vectorAggregatorConfigMapName: vector-aggregator-discovery +{% endif %} + servers: + config: + logging: + enableVectorAgent: {{ lookup('env', 'VECTOR_AGGREGATOR') | length > 0 }} + roleGroups: + default: + replicas: 1 +--- +apiVersion: zookeeper.stackable.tech/v1alpha1 +kind: ZookeeperZnode +metadata: + name: nifi-with-ldap-znode +spec: + clusterRef: + name: test-zk diff --git a/tests/templates/kuttl/oidc/11-create-authentication-classes.yaml.j2 b/tests/templates/kuttl/oidc/11-create-authentication-classes.yaml.j2 new file mode 100644 index 00000000..9af5805a --- /dev/null +++ b/tests/templates/kuttl/oidc/11-create-authentication-classes.yaml.j2 @@ -0,0 +1,8 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +metadata: + name: create-ldap-user +commands: + # We need to replace $NAMESPACE (by KUTTL) in the create-authentication-classes.yaml(.j2) + - script: envsubst < 11_authentication-classes.yaml | kubectl apply -n $NAMESPACE -f - diff --git a/tests/templates/kuttl/oidc/11_authentication-classes.yaml.j2 b/tests/templates/kuttl/oidc/11_authentication-classes.yaml.j2 new file mode 100644 index 00000000..47d2ca1c --- /dev/null +++ b/tests/templates/kuttl/oidc/11_authentication-classes.yaml.j2 @@ -0,0 +1,27 @@ +--- +apiVersion: authentication.stackable.tech/v1alpha1 +kind: AuthenticationClass +metadata: + name: nifi-oidc-auth-class-$NAMESPACE +spec: + provider: + oidc: + hostname: keycloak.$NAMESPACE.svc.cluster.local + rootPath: /realms/test + principalClaim: preferred_username + scopes: + - openid + - email + - profile + providerHint: Keycloak +{% if test_scenario['values']['oidc-use-tls'] == 'true' %} + port: 8443 + tls: + verification: + server: + caCert: + secretClass: tls +{% else %} + port: 8080 + tls: null +{% endif %} diff --git a/tests/templates/kuttl/oidc/12-assert.yaml b/tests/templates/kuttl/oidc/12-assert.yaml new file mode 100644 index 00000000..03958264 --- /dev/null +++ b/tests/templates/kuttl/oidc/12-assert.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 1200 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: test-nifi-node-default +status: + readyReplicas: 2 + replicas: 2 diff --git a/tests/templates/kuttl/oidc/12-install-nifi.yaml b/tests/templates/kuttl/oidc/12-install-nifi.yaml new file mode 100644 index 00000000..edef731d --- /dev/null +++ b/tests/templates/kuttl/oidc/12-install-nifi.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: envsubst < 12_nifi.yaml | kubectl apply -n $NAMESPACE -f - diff --git a/tests/templates/kuttl/oidc/12_nifi.yaml.j2 b/tests/templates/kuttl/oidc/12_nifi.yaml.j2 new file mode 100644 index 00000000..338596ae --- /dev/null +++ b/tests/templates/kuttl/oidc/12_nifi.yaml.j2 @@ -0,0 +1,50 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: nifi-sensitive-property-key +stringData: + nifiSensitivePropsKey: mYsUp3rS3cr3tk3y +--- +apiVersion: v1 +kind: Secret +metadata: + name: nifi-oidc-client +stringData: + clientId: nifi + clientSecret: R1bxHUD569vHeQdw +--- +apiVersion: nifi.stackable.tech/v1alpha1 +kind: NifiCluster +metadata: + name: test-nifi +spec: + image: +{% if test_scenario['values']['nifi'].find(",") > 0 %} + custom: "{{ test_scenario['values']['nifi'].split(',')[1] }}" + productVersion: "{{ test_scenario['values']['nifi'].split(',')[0] }}" +{% else %} + productVersion: "{{ test_scenario['values']['nifi'] }}" +{% endif %} + pullPolicy: IfNotPresent + clusterConfig: + authentication: + - authenticationClass: nifi-oidc-auth-class-$NAMESPACE + oidc: + clientCredentialsSecret: nifi-oidc-client + sensitiveProperties: + keySecret: nifi-sensitive-property-key +{% if lookup('env', 'VECTOR_AGGREGATOR') %} + vectorAggregatorConfigMapName: vector-aggregator-discovery +{% endif %} + zookeeperConfigMapName: nifi-with-ldap-znode + listenerClass: external-unstable + nodes: + config: + logging: + enableVectorAgent: {{ lookup('env', 'VECTOR_AGGREGATOR') }} + gracefulShutdownTimeout: 1s # let the tests run faster + roleGroups: + default: + config: {} + replicas: 2 diff --git a/tests/test-definition.yaml b/tests/test-definition.yaml index db38cdbe..a825acc0 100644 --- a/tests/test-definition.yaml +++ b/tests/test-definition.yaml @@ -84,6 +84,12 @@ tests: - nifi-latest - zookeeper-latest - openshift + - name: oidc + dimensions: + - nifi + - zookeeper-latest + - oidc-use-tls + - openshift suites: - name: nightly patch: From 7b89547d02d2e7e9aa50f6d63744e5b0ae5462cd Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Wed, 24 Jul 2024 16:30:17 +0200 Subject: [PATCH 04/27] wip: debug oidc & jwts --- rust/operator-binary/src/config.rs | 22 +++-- rust/operator-binary/src/controller.rs | 19 ++-- .../src/security/authentication.rs | 6 +- .../templates/kuttl/oidc/01_keycloak.yaml.j2 | 54 +++++++++++- .../kuttl/oidc/03-install-test-nifi.yaml | 3 + .../kuttl/oidc/10-install-zk.yaml.j2 | 2 +- .../11-create-authentication-classes.yaml.j2 | 3 +- .../oidc/11_authentication-classes.yaml.j2 | 10 ++- tests/templates/kuttl/oidc/12_nifi.yaml.j2 | 2 +- tests/test-definition.yaml | 88 ++++++++++--------- 10 files changed, 143 insertions(+), 66 deletions(-) diff --git a/rust/operator-binary/src/config.rs b/rust/operator-binary/src/config.rs index 41748490..d978c7ec 100644 --- a/rust/operator-binary/src/config.rs +++ b/rust/operator-binary/src/config.rs @@ -555,10 +555,18 @@ pub fn build_nifi_properties( "nifi.security.truststorePasswd".to_string(), STACKABLE_TLS_STORE_PASSWORD.to_string(), ); - properties.insert( - "nifi.security.user.login.identity.provider".to_string(), - "login-identity-provider".to_string(), - ); + + if let NifiAuthenticationConfig::Oidc { .. } = auth_config { + properties.insert( + "nifi.security.user.login.identity.provider".to_string(), + "".to_string(), + ); + } else { + properties.insert( + "nifi.security.user.login.identity.provider".to_string(), + "login-identity-provider".to_string(), + ); + } properties.insert( "nifi.security.user.authorizer".to_string(), "authorizer".to_string(), @@ -599,13 +607,17 @@ pub fn build_nifi_properties( let scopes = provider.scopes.join(","); properties.insert( "nifi.security.user.oidc.additional.scopes".to_string(), - format!("[{scopes}]").to_string(), + format!("{scopes}").to_string(), ); properties.insert( "nifi.security.user.oidc.claim.identifying.user".to_string(), provider.principal_claim.to_string(), ); + properties.insert( + "nifi.security.user.oidc.truststore.strategy".to_string(), + "NIFI".to_string(), + ); } // cluster node properties (only configure for cluster nodes) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index ef6151dc..fe902cbf 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -31,7 +31,10 @@ use stackable_operator::{ }, client::Client, cluster_resources::{ClusterResourceApplyStrategy, ClusterResources}, - commons::{product_image_selection::ResolvedProductImage, rbac::build_rbac_resources}, + commons::{ + authentication::oidc::AuthenticationProvider, + product_image_selection::ResolvedProductImage, rbac::build_rbac_resources, + }, config::fragment, k8s_openapi::{ api::{ @@ -46,11 +49,11 @@ use stackable_operator::{ DeepMerge, }, kube::{ - api::ListParams, runtime::controller::Action, runtime::reflector::ObjectRef, Resource, - ResourceExt, + api::ListParams, + runtime::{controller::Action, reflector::ObjectRef}, + Resource, ResourceExt, }, - kvp::Labels, - kvp::{Label, ObjectLabels}, + kvp::{Label, Labels, ObjectLabels}, logging::controller::ReconcilerError, product_logging::{ self, @@ -903,6 +906,12 @@ async fn build_node_rolegroup_statefulset( &nifi.spec.cluster_config.zookeeper_config_map_name, )); + if let NifiAuthenticationConfig::Oidc { oidc, .. } = nifi_auth_config { + env_vars.extend(AuthenticationProvider::client_credentials_env_var_mounts( + oidc.client_credentials_secret_ref.clone(), + )); + } + let node_address = format!( "$POD_NAME.{}-node-{}.{}.svc.cluster.local", rolegroup_ref.cluster.name, diff --git a/rust/operator-binary/src/security/authentication.rs b/rust/operator-binary/src/security/authentication.rs index a71a8e57..0711141c 100644 --- a/rust/operator-binary/src/security/authentication.rs +++ b/rust/operator-binary/src/security/authentication.rs @@ -340,7 +340,7 @@ fn get_oidc_authorizer() -> Result { - admin + nifi-admin CN=generated certificate for pod @@ -353,9 +353,9 @@ fn get_oidc_authorizer() -> Result { file-user-group-provider ./conf/authorizations.xml - + - admin + nifi-admin CN=api_client, OU=dastc, O=**, L=**, ST=**, C=** diff --git a/tests/templates/kuttl/oidc/01_keycloak.yaml.j2 b/tests/templates/kuttl/oidc/01_keycloak.yaml.j2 index 7a52415d..1eed0303 100644 --- a/tests/templates/kuttl/oidc/01_keycloak.yaml.j2 +++ b/tests/templates/kuttl/oidc/01_keycloak.yaml.j2 @@ -10,6 +10,19 @@ data: "realm": "$REALM", "enabled": true, "users": [ + { + "enabled": true, + "username": "nifi-admin", + "credentials": [ + { + "type": "password", + "value": "nifi-admin" + } + ], + "realmRoles": [ + "user" + ] + }, { "enabled": true, "username": "$USERNAME", @@ -116,8 +129,9 @@ spec: volumeClaimTemplate: metadata: annotations: - secrets.stackable.tech/class: tls - secrets.stackable.tech/scope: service=$INSTANCE_NAME + secrets.stackable.tech/class: keycloak-tls + # secrets.stackable.tech/scope: service=$INSTANCE_NAME + secrets.stackable.tech/scope: service=$INSTANCE_NAME-nodeport spec: accessModes: - ReadWriteOnce @@ -143,6 +157,27 @@ spec: port: 8080 {% endif %} --- +apiVersion: v1 +kind: Service +metadata: + name: $INSTANCE_NAME-nodeport + labels: + app: $INSTANCE_NAME +spec: + type: NodePort + selector: + app: keycloak + ports: +{% if test_scenario['values']['oidc-use-tls'] == 'true' %} + - name: https + nodePort: 32539 + port: 8443 +{% else %} + - name: http + nodePort: 32539 + port: 8080 +{% endif %} +--- apiVersion: authentication.stackable.tech/v1alpha1 kind: AuthenticationClass metadata: @@ -163,7 +198,7 @@ spec: verification: server: caCert: - secretClass: tls + secretClass: keycloak-tls --- apiVersion: v1 kind: ServiceAccount @@ -193,3 +228,16 @@ roleRef: kind: Role name: keycloak apiGroup: rbac.authorization.k8s.io +--- +apiVersion: secrets.stackable.tech/v1alpha1 +kind: SecretClass +metadata: + name: keycloak-tls +spec: + backend: + autoTls: + ca: + autoGenerate: true + secret: + name: keycloak-tls-ca + namespace: $NAMESPACE diff --git a/tests/templates/kuttl/oidc/03-install-test-nifi.yaml b/tests/templates/kuttl/oidc/03-install-test-nifi.yaml index 5e2edf95..6b15718c 100644 --- a/tests/templates/kuttl/oidc/03-install-test-nifi.yaml +++ b/tests/templates/kuttl/oidc/03-install-test-nifi.yaml @@ -19,4 +19,7 @@ spec: - name: test-nifi image: docker.stackable.tech/stackable/testing-tools:0.2.0-stackable0.0.0-dev command: ["sleep", "infinity"] + env: + - name: OIDC_USE_TLS + value: "{{ test_scenario['values']['oidc-use-tls'] }}" terminationGracePeriodSeconds: 1 diff --git a/tests/templates/kuttl/oidc/10-install-zk.yaml.j2 b/tests/templates/kuttl/oidc/10-install-zk.yaml.j2 index df1776b8..8eb3bbd5 100644 --- a/tests/templates/kuttl/oidc/10-install-zk.yaml.j2 +++ b/tests/templates/kuttl/oidc/10-install-zk.yaml.j2 @@ -22,7 +22,7 @@ spec: apiVersion: zookeeper.stackable.tech/v1alpha1 kind: ZookeeperZnode metadata: - name: nifi-with-ldap-znode + name: nifi-with-oidc-znode spec: clusterRef: name: test-zk diff --git a/tests/templates/kuttl/oidc/11-create-authentication-classes.yaml.j2 b/tests/templates/kuttl/oidc/11-create-authentication-classes.yaml.j2 index 9af5805a..28895f6c 100644 --- a/tests/templates/kuttl/oidc/11-create-authentication-classes.yaml.j2 +++ b/tests/templates/kuttl/oidc/11-create-authentication-classes.yaml.j2 @@ -1,8 +1,7 @@ --- apiVersion: kuttl.dev/v1beta1 kind: TestStep -metadata: - name: create-ldap-user commands: # We need to replace $NAMESPACE (by KUTTL) in the create-authentication-classes.yaml(.j2) + - script: ls - script: envsubst < 11_authentication-classes.yaml | kubectl apply -n $NAMESPACE -f - diff --git a/tests/templates/kuttl/oidc/11_authentication-classes.yaml.j2 b/tests/templates/kuttl/oidc/11_authentication-classes.yaml.j2 index 47d2ca1c..32850665 100644 --- a/tests/templates/kuttl/oidc/11_authentication-classes.yaml.j2 +++ b/tests/templates/kuttl/oidc/11_authentication-classes.yaml.j2 @@ -6,7 +6,9 @@ metadata: spec: provider: oidc: - hostname: keycloak.$NAMESPACE.svc.cluster.local + # hostname: keycloak.$NAMESPACE.svc.cluster.local + hostname: "172.18.0.2" + port: 32539 rootPath: /realms/test principalClaim: preferred_username scopes: @@ -15,13 +17,13 @@ spec: - profile providerHint: Keycloak {% if test_scenario['values']['oidc-use-tls'] == 'true' %} - port: 8443 + # port: 8443 tls: verification: server: caCert: - secretClass: tls + secretClass: keycloak-tls {% else %} - port: 8080 + # port: 8080 tls: null {% endif %} diff --git a/tests/templates/kuttl/oidc/12_nifi.yaml.j2 b/tests/templates/kuttl/oidc/12_nifi.yaml.j2 index 338596ae..dded8835 100644 --- a/tests/templates/kuttl/oidc/12_nifi.yaml.j2 +++ b/tests/templates/kuttl/oidc/12_nifi.yaml.j2 @@ -37,7 +37,7 @@ spec: {% if lookup('env', 'VECTOR_AGGREGATOR') %} vectorAggregatorConfigMapName: vector-aggregator-discovery {% endif %} - zookeeperConfigMapName: nifi-with-ldap-znode + zookeeperConfigMapName: nifi-with-oidc-znode listenerClass: external-unstable nodes: config: diff --git a/tests/test-definition.yaml b/tests/test-definition.yaml index 69e8bad1..16ab5652 100644 --- a/tests/test-definition.yaml +++ b/tests/test-definition.yaml @@ -12,10 +12,10 @@ dimensions: - 3.9.2 - name: nifi values: - - 1.21.0 - - 1.25.0 + # - 1.21.0 + # - 1.25.0 - 1.27.0 - - 2.0.0-M4 + # - 2.0.0-M4 # Alternatively, if you want to use a custom image, append a comma and the full image name to the product version # as in the example below. # - 1.27.0,docker.stackable.tech/sandbox/nifi:1.27.0-stackable0.0.0-dev @@ -36,8 +36,12 @@ dimensions: # - 1.27.0,docker.stackable.tech/sandbox/nifi:1.27.0-stackable0.0.0-dev - name: ldap-use-tls values: - - "false" + # - "false" - "true" + - name: oidc-use-tls + values: + - "false" + # - "true" - name: openshift values: - "false" @@ -47,44 +51,44 @@ dimensions: - "cluster-internal" - "external-unstable" tests: - - name: upgrade - dimensions: - - nifi_old - - nifi_new - - zookeeper-latest - - openshift - - name: orphaned_resources - dimensions: - - nifi - - zookeeper-latest - - openshift - - name: smoke - dimensions: - - nifi - - zookeeper - - listener-class - - openshift - - name: resources - dimensions: - - nifi - - zookeeper-latest - - openshift - - name: ldap - dimensions: - - nifi - - zookeeper-latest - - ldap-use-tls - - openshift - - name: logging - dimensions: - - nifi - - zookeeper-latest - - openshift - - name: cluster_operation - dimensions: - - nifi-latest - - zookeeper-latest - - openshift + # - name: upgrade + # dimensions: + # - nifi_old + # - nifi_new + # - zookeeper-latest + # - openshift + # - name: orphaned_resources + # dimensions: + # - nifi + # - zookeeper-latest + # - openshift + # - name: smoke + # dimensions: + # - nifi + # - zookeeper + # - listener-class + # - openshift + # - name: resources + # dimensions: + # - nifi + # - zookeeper-latest + # - openshift + # - name: ldap + # dimensions: + # - nifi + # - zookeeper-latest + # - ldap-use-tls + # - openshift + # - name: logging + # dimensions: + # - nifi + # - zookeeper-latest + # - openshift + # - name: cluster_operation + # dimensions: + # - nifi-latest + # - zookeeper-latest + # - openshift - name: oidc dimensions: - nifi From d96503c3ddb40d1283449b34f201ec9b26a9551a Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Tue, 30 Jul 2024 10:39:06 +0200 Subject: [PATCH 05/27] wip --- rust/crd/src/authentication.rs | 23 +++-- rust/operator-binary/src/config.rs | 4 +- rust/operator-binary/src/controller.rs | 11 ++- .../src/security/authentication.rs | 77 +++++++-------- rust/operator-binary/src/security/mod.rs | 13 +++ rust/operator-binary/src/security/oidc.rs | 93 +++++++++++++++++++ static-auth-class.yaml | 16 ++++ static-nifi.yaml | 25 +++++ static-zk.yaml | 19 ++++ .../templates/kuttl/oidc/01_keycloak.yaml.j2 | 13 --- .../kuttl/oidc/03-install-test-nifi.yaml | 6 ++ tests/templates/kuttl/oidc/12_nifi.yaml.j2 | 2 +- tests/templates/kuttl/oidc/20-assert.yaml | 8 ++ tests/templates/kuttl/oidc/20-login.yaml | 7 ++ tests/templates/kuttl/oidc/login.py | 48 ++++++++++ 15 files changed, 296 insertions(+), 69 deletions(-) create mode 100644 rust/operator-binary/src/security/oidc.rs create mode 100644 static-auth-class.yaml create mode 100644 static-nifi.yaml create mode 100644 static-zk.yaml create mode 100644 tests/templates/kuttl/oidc/20-assert.yaml create mode 100644 tests/templates/kuttl/oidc/20-login.yaml create mode 100644 tests/templates/kuttl/oidc/login.py diff --git a/rust/crd/src/authentication.rs b/rust/crd/src/authentication.rs index 60cc2208..e5ecb32d 100644 --- a/rust/crd/src/authentication.rs +++ b/rust/crd/src/authentication.rs @@ -12,6 +12,8 @@ use stackable_operator::{ }; use tracing::info; +use crate::NifiCluster; + // The assumed OIDC provider if no hint is given in the AuthClass pub const DEFAULT_OIDC_PROVIDER: IdentityProviderHint = IdentityProviderHint::Keycloak; @@ -64,29 +66,31 @@ pub enum AuthenticationClassResolved { Oidc { provider: oidc::AuthenticationProvider, oidc: oidc::ClientAuthenticationOptions<()>, + nifi: NifiCluster, }, } impl AuthenticationClassResolved { pub async fn from( - auth_details: &[ClientAuthenticationDetails], + nifi: &NifiCluster, client: &Client, ) -> Result> { let resolve_auth_class = |auth_details: ClientAuthenticationDetails| async move { auth_details.resolve_class(client).await }; - AuthenticationClassResolved::resolve(auth_details, resolve_auth_class).await + AuthenticationClassResolved::resolve(nifi, resolve_auth_class).await } /// Retrieve all provided `AuthenticationClass` references. pub async fn resolve( - auth_details: &[ClientAuthenticationDetails], + nifi: &NifiCluster, resolve_auth_class: impl Fn(ClientAuthenticationDetails) -> R, ) -> Result> where R: Future>, { let mut resolved_auth_classes = vec![]; + let auth_details = &nifi.spec.cluster_config.authentication; match auth_details.len() { 0 => NoAuthenticationNotSupportedSnafu.fail()?, @@ -120,9 +124,14 @@ impl AuthenticationClassResolved { provider: provider.to_owned(), }) } - AuthenticationClassProvider::Oidc(provider) => resolved_auth_classes.push( - AuthenticationClassResolved::from_oidc(&auth_class_name, provider, entry)?, - ), + AuthenticationClassProvider::Oidc(provider) => { + resolved_auth_classes.push(AuthenticationClassResolved::from_oidc( + &auth_class_name, + provider, + entry, + nifi.clone(), + )?) + } _ => AuthenticationClassProviderNotSupportedSnafu { authentication_class_provider: auth_class.spec.provider.to_string(), authentication_class: ObjectRef::::new(&auth_class_name), @@ -138,6 +147,7 @@ impl AuthenticationClassResolved { auth_class_name: &str, provider: &oidc::AuthenticationProvider, auth_details: &ClientAuthenticationDetails, + nifi: NifiCluster, ) -> Result { let oidc_provider = match &provider.provider_hint { None => { @@ -162,6 +172,7 @@ impl AuthenticationClassResolved { .oidc_or_error(auth_class_name) .context(OidcConfigurationInvalidSnafu)? .clone(), + nifi: nifi, }) } } diff --git a/rust/operator-binary/src/config.rs b/rust/operator-binary/src/config.rs index d978c7ec..c278d903 100644 --- a/rust/operator-binary/src/config.rs +++ b/rust/operator-binary/src/config.rs @@ -559,7 +559,7 @@ pub fn build_nifi_properties( if let NifiAuthenticationConfig::Oidc { .. } = auth_config { properties.insert( "nifi.security.user.login.identity.provider".to_string(), - "".to_string(), + "login-identity-provider".to_string(), ); } else { properties.insert( @@ -580,7 +580,7 @@ pub fn build_nifi_properties( "true".to_string(), ); - if let NifiAuthenticationConfig::Oidc { provider, oidc } = auth_config { + if let NifiAuthenticationConfig::Oidc { provider, oidc, .. } = auth_config { let endpoint_url = provider .endpoint_url() .context(InvalidOidcEndpointSnafu)? diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index fe902cbf..2a6aa05f 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -89,7 +89,7 @@ use crate::{ LOGIN_IDENTITY_PROVIDERS_XML_FILE_NAME, STACKABLE_SERVER_TLS_DIR, STACKABLE_TLS_STORE_PASSWORD, }, - build_tls_volume, check_or_generate_sensitive_key, + build_tls_volume, check_or_generate_oidc_admin_password, check_or_generate_sensitive_key, tls::{KEYSTORE_NIFI_CONTAINER_MOUNT, KEYSTORE_VOLUME_NAME, TRUSTSTORE_VOLUME_NAME}, }, OPERATOR_NAME, @@ -462,12 +462,19 @@ pub async fn reconcile_nifi(nifi: Arc, ctx: Arc) -> Result { + Self::SingleUser { .. } | Self::Oidc { .. } => { login_identity_provider_xml.push_str(&formatdoc! {r#" login-identity-provider @@ -93,9 +97,6 @@ impl NifiAuthenticationConfig { login_identity_provider_xml.push_str(&get_ldap_login_identity_provider(&provider)?); authorizers_xml.push_str(&get_ldap_authorizer(&provider)?); } - Self::Oidc { .. } => { - authorizers_xml.push_str(&get_oidc_authorizer()?); - } } login_identity_provider_xml.push_str(indoc! {r#" @@ -144,6 +145,10 @@ impl NifiAuthenticationConfig { } } Self::Oidc { provider, .. } => { + let (_, admin_password_file) = self.get_user_and_password_file_paths(); + commands.extend(vec![ + format!("export STACKABLE_ADMIN_PASSWORD=\"$(cat {admin_password_file} | java -jar /bin/stackable-bcrypt.jar)\""), + ]); if let Some(ca_path) = provider.tls.tls_ca_cert_mount_path() { commands.extend(vec![ "echo Adding OIDC tls cert to global truststore".to_string(), @@ -189,7 +194,27 @@ impl NifiAuthenticationConfig { .add_volumes_and_mounts(pod_builder, container_builders) .context(AddLdapVolumesSnafu)?; } - Self::Oidc { provider, .. } => { + Self::Oidc { provider, nifi, .. } => { + let admin_volume = Volume { + name: STACKABLE_ADMIN_USERNAME.to_string(), + secret: Some(SecretVolumeSource { + secret_name: Some(build_oidc_admin_password_secret_name(nifi)), + optional: Some(false), + items: Some(vec![KeyToPath { + key: STACKABLE_ADMIN_USERNAME.to_string(), + path: STACKABLE_ADMIN_USERNAME.to_string(), + ..KeyToPath::default() + }]), + ..SecretVolumeSource::default() + }), + ..Volume::default() + }; + pod_builder.add_volume(admin_volume); + + if let Some(prepare_container) = container_builders.first() { + prepare_container.add_volume_mount(STACKABLE_ADMIN_USERNAME, STACKABLE_USER_VOLUME_MOUNT_PATH); + } + provider .tls .add_volumes_and_mounts(pod_builder, container_builders) @@ -217,9 +242,10 @@ impl NifiAuthenticationConfig { AuthenticationClassResolved::Ldap { provider } => Ok(Self::Ldap { provider: provider.clone(), }), - AuthenticationClassResolved::Oidc { provider, oidc } => Ok(Self::Oidc { + AuthenticationClassResolved::Oidc { provider, oidc, nifi } => Ok(Self::Oidc { provider: provider.clone(), oidc: oidc.clone(), + nifi: nifi.clone(), }), } } @@ -330,42 +356,3 @@ fn get_ldap_authorizer(ldap: &ldap::AuthenticationProvider) -> Result "#}) } - -fn get_oidc_authorizer() -> Result { - Ok(formatdoc! {r#" - - file-user-group-provider - org.apache.nifi.authorization.FileUserGroupProvider - ./conf/users.xml - - - - nifi-admin - - - CN=generated certificate for pod - CN=api_client, OU=**, O=**, ST=**, C=** - - - - file-access-policy-provider - org.apache.nifi.authorization.FileAccessPolicyProvider - file-user-group-provider - ./conf/authorizations.xml - - - - nifi-admin - - - CN=api_client, OU=dastc, O=**, L=**, ST=**, C=** - CN=generated certificate for pod - - - - authorizer - org.apache.nifi.authorization.StandardManagedAuthorizer - file-access-policy-provider - - "#}) -} diff --git a/rust/operator-binary/src/security/mod.rs b/rust/operator-binary/src/security/mod.rs index 5944e198..11090bd7 100644 --- a/rust/operator-binary/src/security/mod.rs +++ b/rust/operator-binary/src/security/mod.rs @@ -4,6 +4,7 @@ use stackable_operator::client::Client; use stackable_operator::{builder::pod::volume::SecretFormat, k8s_openapi::api::core::v1::Volume}; pub mod authentication; +pub mod oidc; pub mod sensitive_key; pub mod tls; @@ -16,6 +17,9 @@ pub enum Error { #[snafu(display("sensistive key failure"))] SensitiveKey { source: sensitive_key::Error }, + + #[snafu(display("failed to ensure OIDC admin password exists"))] + OidcAdminPassword { source: oidc::Error }, } pub async fn check_or_generate_sensitive_key(client: &Client, nifi: &NifiCluster) -> Result { @@ -24,6 +28,15 @@ pub async fn check_or_generate_sensitive_key(client: &Client, nifi: &NifiCluster .context(SensitiveKeySnafu) } +pub async fn check_or_generate_oidc_admin_password( + client: &Client, + nifi: &NifiCluster, +) -> Result { + oidc::check_or_generate_oidc_admin_password(client, nifi) + .await + .context(OidcAdminPasswordSnafu) +} + pub fn build_tls_volume( nifi: &NifiCluster, volume_name: &str, diff --git a/rust/operator-binary/src/security/oidc.rs b/rust/operator-binary/src/security/oidc.rs new file mode 100644 index 00000000..d5e7162c --- /dev/null +++ b/rust/operator-binary/src/security/oidc.rs @@ -0,0 +1,93 @@ +use std::collections::{BTreeMap, HashSet}; + +use rand::{distributions::Alphanumeric, Rng}; +use snafu::{OptionExt, ResultExt, Snafu}; +use stackable_nifi_crd::NifiCluster; +use stackable_operator::{ + builder::meta::ObjectMetaBuilder, client::Client, k8s_openapi::api::core::v1::Secret, + kube::ResourceExt, +}; + +use super::authentication::STACKABLE_ADMIN_USERNAME; + +const STACKABLE_OIDC_ADMIN_PASSWORD_KEY: &str = STACKABLE_ADMIN_USERNAME; + +type Result = std::result::Result; + +#[derive(Snafu, Debug)] +pub enum Error { + #[snafu(display("the NiFi object defines no namespace"))] + ObjectHasNoNamespace, + + #[snafu(display("failed to fetch or create OIDC admin password secret"))] + OidcAdminPasswordSecret { + source: stackable_operator::client::Error, + }, + + #[snafu(display( + "found existing secret [{}/{}], but key {} is missing", + name, + namespace, + STACKABLE_OIDC_ADMIN_PASSWORD_KEY + ))] + OidcAdminPasswordKeyMissing { name: String, namespace: String }, +} + +pub(crate) async fn check_or_generate_oidc_admin_password( + client: &Client, + nifi: &NifiCluster, +) -> Result { + let namespace: &str = &nifi.namespace().context(ObjectHasNoNamespaceSnafu)?; + + match client + .get_opt::(&build_oidc_admin_password_secret_name(nifi), namespace) + .await + .context(OidcAdminPasswordSecretSnafu)? + { + Some(secret) => { + let keys = secret + .data + .unwrap_or_default() + .into_keys() + .collect::>(); + if keys.contains(STACKABLE_OIDC_ADMIN_PASSWORD_KEY) { + return Ok(false); + } else { + return OidcAdminPasswordKeyMissingSnafu { + name: build_oidc_admin_password_secret_name(nifi), + namespace, + } + .fail()?; + } + } + None => { + tracing::info!("No existing oidc admin password secret found, generating new one"); + let password: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(15) + .map(char::from) + .collect(); + + let mut secret_data = BTreeMap::new(); + secret_data.insert("admin".to_string(), password); + + let new_secret = Secret { + metadata: ObjectMetaBuilder::new() + .namespace(namespace) + .name(build_oidc_admin_password_secret_name(nifi)) + .build(), + string_data: Some(secret_data), + ..Secret::default() + }; + client + .create(&new_secret) + .await + .context(OidcAdminPasswordSecretSnafu)?; + Ok(true) + } + } +} + +pub fn build_oidc_admin_password_secret_name(nifi: &NifiCluster) -> String { + format!("{}-oidc-admin-password", nifi.name_any()) +} diff --git a/static-auth-class.yaml b/static-auth-class.yaml new file mode 100644 index 00000000..b1fea8d7 --- /dev/null +++ b/static-auth-class.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Secret +metadata: + name: simple-admin-credentials +stringData: + admin: admin +--- +apiVersion: authentication.stackable.tech/v1alpha1 +kind: AuthenticationClass +metadata: + name: simple-nifi-users +spec: + provider: + static: + userCredentialsSecret: + name: simple-admin-credentials diff --git a/static-nifi.yaml b/static-nifi.yaml new file mode 100644 index 00000000..71a1fe15 --- /dev/null +++ b/static-nifi.yaml @@ -0,0 +1,25 @@ +apiVersion: nifi.stackable.tech/v1alpha1 +kind: NifiCluster +metadata: + name: simple-nifi +spec: + clusterConfig: + authentication: + - authenticationClass: simple-nifi-users + listenerClass: external-unstable + sensitiveProperties: + autoGenerate: true + keySecret: nifi-sensitive-property-key + zookeeperConfigMapName: simple-nifi-znode + image: + productVersion: 1.27.0 + nodes: + configOverrides: + nifi.properties: + nifi.security.user.oidc.client.id: nifi + nifi.security.user.oidc.client.secret: R1bxHUD569vHeQdw + nifi.security.user.oidc.discovery.url: http://172.18.0.2:32539/realms/test/.well-known/openid-configuration + nifi.security.user.oidc.claim.identifying.user: preferred_username + roleGroups: + default: + replicas: 1 diff --git a/static-zk.yaml b/static-zk.yaml new file mode 100644 index 00000000..3fda990e --- /dev/null +++ b/static-zk.yaml @@ -0,0 +1,19 @@ +apiVersion: zookeeper.stackable.tech/v1alpha1 +kind: ZookeeperCluster +metadata: + name: simple-zk +spec: + image: + productVersion: 3.9.2 + servers: + roleGroups: + default: + replicas: 3 +--- +apiVersion: zookeeper.stackable.tech/v1alpha1 +kind: ZookeeperZnode +metadata: + name: simple-nifi-znode +spec: + clusterRef: + name: simple-zk \ No newline at end of file diff --git a/tests/templates/kuttl/oidc/01_keycloak.yaml.j2 b/tests/templates/kuttl/oidc/01_keycloak.yaml.j2 index 1eed0303..2aa95144 100644 --- a/tests/templates/kuttl/oidc/01_keycloak.yaml.j2 +++ b/tests/templates/kuttl/oidc/01_keycloak.yaml.j2 @@ -10,19 +10,6 @@ data: "realm": "$REALM", "enabled": true, "users": [ - { - "enabled": true, - "username": "nifi-admin", - "credentials": [ - { - "type": "password", - "value": "nifi-admin" - } - ], - "realmRoles": [ - "user" - ] - }, { "enabled": true, "username": "$USERNAME", diff --git a/tests/templates/kuttl/oidc/03-install-test-nifi.yaml b/tests/templates/kuttl/oidc/03-install-test-nifi.yaml index 6b15718c..6f3e1f03 100644 --- a/tests/templates/kuttl/oidc/03-install-test-nifi.yaml +++ b/tests/templates/kuttl/oidc/03-install-test-nifi.yaml @@ -22,4 +22,10 @@ spec: env: - name: OIDC_USE_TLS value: "{{ test_scenario['values']['oidc-use-tls'] }}" + - name: NIFI_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: test-nifi-oidc-admin-password + key: admin + terminationGracePeriodSeconds: 1 diff --git a/tests/templates/kuttl/oidc/12_nifi.yaml.j2 b/tests/templates/kuttl/oidc/12_nifi.yaml.j2 index dded8835..927049b2 100644 --- a/tests/templates/kuttl/oidc/12_nifi.yaml.j2 +++ b/tests/templates/kuttl/oidc/12_nifi.yaml.j2 @@ -47,4 +47,4 @@ spec: roleGroups: default: config: {} - replicas: 2 + replicas: 1 diff --git a/tests/templates/kuttl/oidc/20-assert.yaml b/tests/templates/kuttl/oidc/20-assert.yaml new file mode 100644 index 00000000..88904dbd --- /dev/null +++ b/tests/templates/kuttl/oidc/20-assert.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +metadata: + name: login +timeout: 300 +commands: + - script: kubectl exec -n $NAMESPACE test-nifi-0 -- python /stackable/login.py $NAMESPACE diff --git a/tests/templates/kuttl/oidc/20-login.yaml b/tests/templates/kuttl/oidc/20-login.yaml new file mode 100644 index 00000000..88650713 --- /dev/null +++ b/tests/templates/kuttl/oidc/20-login.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: > + envsubst '$NAMESPACE' < login.py | + kubectl exec -n $NAMESPACE -i test-nifi-0 -- tee /stackable/login.py > /dev/null diff --git a/tests/templates/kuttl/oidc/login.py b/tests/templates/kuttl/oidc/login.py new file mode 100644 index 00000000..aa950374 --- /dev/null +++ b/tests/templates/kuttl/oidc/login.py @@ -0,0 +1,48 @@ +# $NAMESPACE will be replaced with the namespace of the test case. + +import logging +import os +import requests +import sys +from bs4 import BeautifulSoup + +logging.basicConfig( + level="DEBUG", format="%(asctime)s %(levelname)s: %(message)s", stream=sys.stdout +) + +namespace = sys.argv[1] +tls = os.environ["OIDC_USE_TLS"] + +session = requests.Session() + +nifi = f"test-nifi-node-default.{namespace}.svc.cluster.local" +keycloak_service = f"keycloak.{namespace}.svc.cluster.local" + +# Open NiFi web UI which will redirect to OIDC login +login_page = session.get( + f"https://{nifi}:8443/nifi", + verify=False, + headers={"Content-type": "application/json"}, +) +keycloak_base_url = ( + f"https://{keycloak_service}:8443" + if tls == "true" + else f"http://{keycloak_service}:8080" +) +print(login_page.url) +assert login_page.ok, "Redirection from NiFi to Keycloak failed" +#assert login_page.url.startswith( +# f"{keycloak_base_url}/realms/test/protocol/openid-connect/auth?scope=openid+email+profile&response_type=code&redirect_uri=https%3A%2F%2F{nifi}%3A9088%2Fdruid-ext%2Fdruid-pac4j%2Fcallback&state=" +#), "Redirection to Keycloak expected" + +# Login to keycloak with test user +login_page_html = BeautifulSoup(login_page.text, "html.parser") +authenticate_url = login_page_html.form["action"] +welcome_page = session.post( + authenticate_url, data={"username": "nifi-admin", "password": "nifi-admin"} +) +print(welcome_page.url) +assert welcome_page.ok, "Login failed" +assert ( + welcome_page.url == f"https://{nifi}:8443/nifi" +), "Redirection to the NiFi web UI expected" From 6a005ae1e2346889feaa9bdad06a817ef7a2f11c Mon Sep 17 00:00:00 2001 From: Nick Larsen Date: Tue, 30 Jul 2024 11:07:42 +0200 Subject: [PATCH 06/27] wip: map over ContainerBuilders --- rust/operator-binary/src/security/authentication.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/rust/operator-binary/src/security/authentication.rs b/rust/operator-binary/src/security/authentication.rs index 51ff0884..ab8ca6da 100644 --- a/rust/operator-binary/src/security/authentication.rs +++ b/rust/operator-binary/src/security/authentication.rs @@ -211,9 +211,15 @@ impl NifiAuthenticationConfig { }; pod_builder.add_volume(admin_volume); - if let Some(prepare_container) = container_builders.first() { - prepare_container.add_volume_mount(STACKABLE_ADMIN_USERNAME, STACKABLE_USER_VOLUME_MOUNT_PATH); - } + let container_builders = container_builders + .into_iter() + .map(|cb| { + cb.add_volume_mount( + STACKABLE_ADMIN_USERNAME, + STACKABLE_USER_VOLUME_MOUNT_PATH, + ) + }) + .collect(); provider .tls From ec83a524a61160fa159afe924536184d5b1689d3 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Wed, 31 Jul 2024 16:56:46 +0200 Subject: [PATCH 07/27] fix oidc test --- .../templates/kuttl/oidc/01_keycloak.yaml.j2 | 24 +------------------ .../oidc/11_authentication-classes.yaml.j2 | 8 +++---- tests/templates/kuttl/oidc/12-assert.yaml | 11 +++++++-- tests/templates/kuttl/oidc/login.py | 15 ++++++------ 4 files changed, 20 insertions(+), 38 deletions(-) diff --git a/tests/templates/kuttl/oidc/01_keycloak.yaml.j2 b/tests/templates/kuttl/oidc/01_keycloak.yaml.j2 index 2aa95144..a911f9f1 100644 --- a/tests/templates/kuttl/oidc/01_keycloak.yaml.j2 +++ b/tests/templates/kuttl/oidc/01_keycloak.yaml.j2 @@ -117,8 +117,7 @@ spec: metadata: annotations: secrets.stackable.tech/class: keycloak-tls - # secrets.stackable.tech/scope: service=$INSTANCE_NAME - secrets.stackable.tech/scope: service=$INSTANCE_NAME-nodeport + secrets.stackable.tech/scope: service=$INSTANCE_NAME spec: accessModes: - ReadWriteOnce @@ -144,27 +143,6 @@ spec: port: 8080 {% endif %} --- -apiVersion: v1 -kind: Service -metadata: - name: $INSTANCE_NAME-nodeport - labels: - app: $INSTANCE_NAME -spec: - type: NodePort - selector: - app: keycloak - ports: -{% if test_scenario['values']['oidc-use-tls'] == 'true' %} - - name: https - nodePort: 32539 - port: 8443 -{% else %} - - name: http - nodePort: 32539 - port: 8080 -{% endif %} ---- apiVersion: authentication.stackable.tech/v1alpha1 kind: AuthenticationClass metadata: diff --git a/tests/templates/kuttl/oidc/11_authentication-classes.yaml.j2 b/tests/templates/kuttl/oidc/11_authentication-classes.yaml.j2 index 32850665..af320d4d 100644 --- a/tests/templates/kuttl/oidc/11_authentication-classes.yaml.j2 +++ b/tests/templates/kuttl/oidc/11_authentication-classes.yaml.j2 @@ -6,9 +6,7 @@ metadata: spec: provider: oidc: - # hostname: keycloak.$NAMESPACE.svc.cluster.local - hostname: "172.18.0.2" - port: 32539 + hostname: keycloak.$NAMESPACE.svc.cluster.local rootPath: /realms/test principalClaim: preferred_username scopes: @@ -17,13 +15,13 @@ spec: - profile providerHint: Keycloak {% if test_scenario['values']['oidc-use-tls'] == 'true' %} - # port: 8443 + port: 8443 tls: verification: server: caCert: secretClass: keycloak-tls {% else %} - # port: 8080 + port: 8080 tls: null {% endif %} diff --git a/tests/templates/kuttl/oidc/12-assert.yaml b/tests/templates/kuttl/oidc/12-assert.yaml index 03958264..77a8574f 100644 --- a/tests/templates/kuttl/oidc/12-assert.yaml +++ b/tests/templates/kuttl/oidc/12-assert.yaml @@ -8,5 +8,12 @@ kind: StatefulSet metadata: name: test-nifi-node-default status: - readyReplicas: 2 - replicas: 2 + readyReplicas: 1 + replicas: 1 +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: test-nifi-create-reporting-task-1-27-0 +status: + succeeded: 1 \ No newline at end of file diff --git a/tests/templates/kuttl/oidc/login.py b/tests/templates/kuttl/oidc/login.py index aa950374..e6844202 100644 --- a/tests/templates/kuttl/oidc/login.py +++ b/tests/templates/kuttl/oidc/login.py @@ -15,12 +15,12 @@ session = requests.Session() -nifi = f"test-nifi-node-default.{namespace}.svc.cluster.local" +nifi = f"test-nifi-node-default-0.test-nifi-node-default.{namespace}.svc.cluster.local" keycloak_service = f"keycloak.{namespace}.svc.cluster.local" # Open NiFi web UI which will redirect to OIDC login login_page = session.get( - f"https://{nifi}:8443/nifi", + f"https://{nifi}:8443/nifi/login", verify=False, headers={"Content-type": "application/json"}, ) @@ -29,20 +29,19 @@ if tls == "true" else f"http://{keycloak_service}:8080" ) -print(login_page.url) assert login_page.ok, "Redirection from NiFi to Keycloak failed" -#assert login_page.url.startswith( -# f"{keycloak_base_url}/realms/test/protocol/openid-connect/auth?scope=openid+email+profile&response_type=code&redirect_uri=https%3A%2F%2F{nifi}%3A9088%2Fdruid-ext%2Fdruid-pac4j%2Fcallback&state=" -#), "Redirection to Keycloak expected" +assert login_page.url.startswith( + f"{keycloak_base_url}/realms/test/protocol/openid-connect/auth?response_type=code&client_id=nifi&scope=openid%20email%20profile&state=" +), "Redirection to Keycloak expected" # Login to keycloak with test user login_page_html = BeautifulSoup(login_page.text, "html.parser") authenticate_url = login_page_html.form["action"] welcome_page = session.post( - authenticate_url, data={"username": "nifi-admin", "password": "nifi-admin"} + authenticate_url, data={"username": "jane.doe", "password": "T8mn72D9"}, verify=False ) print(welcome_page.url) assert welcome_page.ok, "Login failed" assert ( - welcome_page.url == f"https://{nifi}:8443/nifi" + welcome_page.url == f"https://{nifi}:8443/nifi/" ), "Redirection to the NiFi web UI expected" From 1ac981ba7771a9499eb2d56413f956050a561fab Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Mon, 5 Aug 2024 15:07:57 +0200 Subject: [PATCH 08/27] fix oidc test --- tests/templates/kuttl/oidc/01_keycloak.yaml.j2 | 7 +++++++ ...install-test-nifi.yaml => 03-install-test-nifi.yaml.j2} | 6 ------ tests/templates/kuttl/oidc/login.py | 1 - tests/test-definition.yaml | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) rename tests/templates/kuttl/oidc/{03-install-test-nifi.yaml => 03-install-test-nifi.yaml.j2} (75%) diff --git a/tests/templates/kuttl/oidc/01_keycloak.yaml.j2 b/tests/templates/kuttl/oidc/01_keycloak.yaml.j2 index a911f9f1..a5b3f3b6 100644 --- a/tests/templates/kuttl/oidc/01_keycloak.yaml.j2 +++ b/tests/templates/kuttl/oidc/01_keycloak.yaml.j2 @@ -9,6 +9,9 @@ data: { "realm": "$REALM", "enabled": true, + "attributes": { + "frontendUrl": "keycloak.$NAMESPACE.svc.cluster.local" + }, "users": [ { "enabled": true, @@ -151,7 +154,11 @@ spec: provider: oidc: hostname: $INSTANCE_NAME.$NAMESPACE.svc.cluster.local +{% if test_scenario['values']['oidc-use-tls'] == 'true' %} port: 8443 +{% else %} + port: 8080 +{% endif %} rootPath: /realms/$REALM scopes: - email diff --git a/tests/templates/kuttl/oidc/03-install-test-nifi.yaml b/tests/templates/kuttl/oidc/03-install-test-nifi.yaml.j2 similarity index 75% rename from tests/templates/kuttl/oidc/03-install-test-nifi.yaml rename to tests/templates/kuttl/oidc/03-install-test-nifi.yaml.j2 index 6f3e1f03..6b15718c 100644 --- a/tests/templates/kuttl/oidc/03-install-test-nifi.yaml +++ b/tests/templates/kuttl/oidc/03-install-test-nifi.yaml.j2 @@ -22,10 +22,4 @@ spec: env: - name: OIDC_USE_TLS value: "{{ test_scenario['values']['oidc-use-tls'] }}" - - name: NIFI_ADMIN_PASSWORD - valueFrom: - secretKeyRef: - name: test-nifi-oidc-admin-password - key: admin - terminationGracePeriodSeconds: 1 diff --git a/tests/templates/kuttl/oidc/login.py b/tests/templates/kuttl/oidc/login.py index e6844202..8c43e133 100644 --- a/tests/templates/kuttl/oidc/login.py +++ b/tests/templates/kuttl/oidc/login.py @@ -40,7 +40,6 @@ welcome_page = session.post( authenticate_url, data={"username": "jane.doe", "password": "T8mn72D9"}, verify=False ) -print(welcome_page.url) assert welcome_page.ok, "Login failed" assert ( welcome_page.url == f"https://{nifi}:8443/nifi/" diff --git a/tests/test-definition.yaml b/tests/test-definition.yaml index 16ab5652..936f7157 100644 --- a/tests/test-definition.yaml +++ b/tests/test-definition.yaml @@ -41,7 +41,7 @@ dimensions: - name: oidc-use-tls values: - "false" - # - "true" + - "true" - name: openshift values: - "false" From 46cebd1f47da7c383be268d9841cf25b3a65f7f7 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Mon, 5 Aug 2024 16:07:50 +0200 Subject: [PATCH 09/27] fix clippy, update CRD and remove debug files --- deploy/helm/nifi-operator/crds/crds.yaml | 21 +++++++++++++++++++- rust/crd/src/authentication.rs | 2 +- static-auth-class.yaml | 16 --------------- static-nifi.yaml | 25 ------------------------ static-zk.yaml | 19 ------------------ 5 files changed, 21 insertions(+), 62 deletions(-) delete mode 100644 static-auth-class.yaml delete mode 100644 static-nifi.yaml delete mode 100644 static-zk.yaml diff --git a/deploy/helm/nifi-operator/crds/crds.yaml b/deploy/helm/nifi-operator/crds/crds.yaml index 9d87b0f7..a9eb41ea 100644 --- a/deploy/helm/nifi-operator/crds/crds.yaml +++ b/deploy/helm/nifi-operator/crds/crds.yaml @@ -33,8 +33,27 @@ spec: items: properties: authenticationClass: - description: Name of the [AuthenticationClass](https://docs.stackable.tech/home/nightly/concepts/authentication) used to authenticate users. Supported providers are `static` and `ldap`. For `static` the "admin" user needs to be present in the referenced secret, and only this user will be added to NiFi, other users are ignored. + description: A name/key which references an authentication class. To get the concrete [`AuthenticationClass`], we must resolve it. This resolution can be achieved by using [`ClientAuthenticationDetails::resolve_class`]. type: string + oidc: + description: |- + This field contains authentication provider specific configuration. + + Use [`ClientAuthenticationDetails::oidc_or_error`] to get the value or report an error to the user. + nullable: true + properties: + clientCredentialsSecret: + description: A reference to the OIDC client credentials secret. The secret contains the client id and secret. + type: string + extraScopes: + default: [] + description: An optional list of extra scopes which get merged with the scopes defined in the [`AuthenticationClass`]. + items: + type: string + type: array + required: + - clientCredentialsSecret + type: object required: - authenticationClass type: object diff --git a/rust/crd/src/authentication.rs b/rust/crd/src/authentication.rs index e5ecb32d..1578f96e 100644 --- a/rust/crd/src/authentication.rs +++ b/rust/crd/src/authentication.rs @@ -172,7 +172,7 @@ impl AuthenticationClassResolved { .oidc_or_error(auth_class_name) .context(OidcConfigurationInvalidSnafu)? .clone(), - nifi: nifi, + nifi, }) } } diff --git a/static-auth-class.yaml b/static-auth-class.yaml deleted file mode 100644 index b1fea8d7..00000000 --- a/static-auth-class.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: simple-admin-credentials -stringData: - admin: admin ---- -apiVersion: authentication.stackable.tech/v1alpha1 -kind: AuthenticationClass -metadata: - name: simple-nifi-users -spec: - provider: - static: - userCredentialsSecret: - name: simple-admin-credentials diff --git a/static-nifi.yaml b/static-nifi.yaml deleted file mode 100644 index 71a1fe15..00000000 --- a/static-nifi.yaml +++ /dev/null @@ -1,25 +0,0 @@ -apiVersion: nifi.stackable.tech/v1alpha1 -kind: NifiCluster -metadata: - name: simple-nifi -spec: - clusterConfig: - authentication: - - authenticationClass: simple-nifi-users - listenerClass: external-unstable - sensitiveProperties: - autoGenerate: true - keySecret: nifi-sensitive-property-key - zookeeperConfigMapName: simple-nifi-znode - image: - productVersion: 1.27.0 - nodes: - configOverrides: - nifi.properties: - nifi.security.user.oidc.client.id: nifi - nifi.security.user.oidc.client.secret: R1bxHUD569vHeQdw - nifi.security.user.oidc.discovery.url: http://172.18.0.2:32539/realms/test/.well-known/openid-configuration - nifi.security.user.oidc.claim.identifying.user: preferred_username - roleGroups: - default: - replicas: 1 diff --git a/static-zk.yaml b/static-zk.yaml deleted file mode 100644 index 3fda990e..00000000 --- a/static-zk.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: zookeeper.stackable.tech/v1alpha1 -kind: ZookeeperCluster -metadata: - name: simple-zk -spec: - image: - productVersion: 3.9.2 - servers: - roleGroups: - default: - replicas: 3 ---- -apiVersion: zookeeper.stackable.tech/v1alpha1 -kind: ZookeeperZnode -metadata: - name: simple-nifi-znode -spec: - clusterRef: - name: simple-zk \ No newline at end of file From 5a6ad477281979e316953b6f747d1135ce610d81 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Mon, 5 Aug 2024 16:11:55 +0200 Subject: [PATCH 10/27] run cargo fmt --- rust/operator-binary/src/security/authentication.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rust/operator-binary/src/security/authentication.rs b/rust/operator-binary/src/security/authentication.rs index ab8ca6da..114c0fd7 100644 --- a/rust/operator-binary/src/security/authentication.rs +++ b/rust/operator-binary/src/security/authentication.rs @@ -248,7 +248,11 @@ impl NifiAuthenticationConfig { AuthenticationClassResolved::Ldap { provider } => Ok(Self::Ldap { provider: provider.clone(), }), - AuthenticationClassResolved::Oidc { provider, oidc, nifi } => Ok(Self::Oidc { + AuthenticationClassResolved::Oidc { + provider, + oidc, + nifi, + } => Ok(Self::Oidc { provider: provider.clone(), oidc: oidc.clone(), nifi: nifi.clone(), From f6b5dcb19b82aad1c1373975a443719dc8713a1d Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Tue, 6 Aug 2024 11:17:20 +0200 Subject: [PATCH 11/27] address clippy and yamllint feedback --- rust/operator-binary/src/config.rs | 2 +- rust/operator-binary/src/security/authentication.rs | 4 ++-- rust/operator-binary/src/security/oidc.rs | 4 ++-- tests/templates/kuttl/oidc/12-assert.yaml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/rust/operator-binary/src/config.rs b/rust/operator-binary/src/config.rs index c278d903..980a9767 100644 --- a/rust/operator-binary/src/config.rs +++ b/rust/operator-binary/src/config.rs @@ -607,7 +607,7 @@ pub fn build_nifi_properties( let scopes = provider.scopes.join(","); properties.insert( "nifi.security.user.oidc.additional.scopes".to_string(), - format!("{scopes}").to_string(), + scopes.to_string(), ); properties.insert( diff --git a/rust/operator-binary/src/security/authentication.rs b/rust/operator-binary/src/security/authentication.rs index 114c0fd7..acc74ad3 100644 --- a/rust/operator-binary/src/security/authentication.rs +++ b/rust/operator-binary/src/security/authentication.rs @@ -94,8 +94,8 @@ impl NifiAuthenticationConfig { "#}); } Self::Ldap { provider } => { - login_identity_provider_xml.push_str(&get_ldap_login_identity_provider(&provider)?); - authorizers_xml.push_str(&get_ldap_authorizer(&provider)?); + login_identity_provider_xml.push_str(&get_ldap_login_identity_provider(provider)?); + authorizers_xml.push_str(&get_ldap_authorizer(provider)?); } } diff --git a/rust/operator-binary/src/security/oidc.rs b/rust/operator-binary/src/security/oidc.rs index d5e7162c..f278c9bb 100644 --- a/rust/operator-binary/src/security/oidc.rs +++ b/rust/operator-binary/src/security/oidc.rs @@ -51,13 +51,13 @@ pub(crate) async fn check_or_generate_oidc_admin_password( .into_keys() .collect::>(); if keys.contains(STACKABLE_OIDC_ADMIN_PASSWORD_KEY) { - return Ok(false); + Ok(false) } else { return OidcAdminPasswordKeyMissingSnafu { name: build_oidc_admin_password_secret_name(nifi), namespace, } - .fail()?; + .fail(); } } None => { diff --git a/tests/templates/kuttl/oidc/12-assert.yaml b/tests/templates/kuttl/oidc/12-assert.yaml index 77a8574f..114d8328 100644 --- a/tests/templates/kuttl/oidc/12-assert.yaml +++ b/tests/templates/kuttl/oidc/12-assert.yaml @@ -16,4 +16,4 @@ kind: Job metadata: name: test-nifi-create-reporting-task-1-27-0 status: - succeeded: 1 \ No newline at end of file + succeeded: 1 From 9baf17e6cb59d19b6b379a4786427f58bc357031 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Tue, 6 Aug 2024 11:44:42 +0200 Subject: [PATCH 12/27] remove unneccessary return --- rust/operator-binary/src/security/oidc.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rust/operator-binary/src/security/oidc.rs b/rust/operator-binary/src/security/oidc.rs index f278c9bb..61aadf70 100644 --- a/rust/operator-binary/src/security/oidc.rs +++ b/rust/operator-binary/src/security/oidc.rs @@ -53,11 +53,11 @@ pub(crate) async fn check_or_generate_oidc_admin_password( if keys.contains(STACKABLE_OIDC_ADMIN_PASSWORD_KEY) { Ok(false) } else { - return OidcAdminPasswordKeyMissingSnafu { + OidcAdminPasswordKeyMissingSnafu { name: build_oidc_admin_password_secret_name(nifi), namespace, } - .fail(); + .fail()? } } None => { From e0077736ac5235858016f91a41d8959d7e566ed3 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Tue, 6 Aug 2024 11:46:10 +0200 Subject: [PATCH 13/27] reenable all tests --- tests/test-definition.yaml | 86 +++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/tests/test-definition.yaml b/tests/test-definition.yaml index 936f7157..584756cc 100644 --- a/tests/test-definition.yaml +++ b/tests/test-definition.yaml @@ -12,10 +12,10 @@ dimensions: - 3.9.2 - name: nifi values: - # - 1.21.0 - # - 1.25.0 + - 1.21.0 + - 1.25.0 - 1.27.0 - # - 2.0.0-M4 + - 2.0.0-M4 # Alternatively, if you want to use a custom image, append a comma and the full image name to the product version # as in the example below. # - 1.27.0,docker.stackable.tech/sandbox/nifi:1.27.0-stackable0.0.0-dev @@ -36,7 +36,7 @@ dimensions: # - 1.27.0,docker.stackable.tech/sandbox/nifi:1.27.0-stackable0.0.0-dev - name: ldap-use-tls values: - # - "false" + - "false" - "true" - name: oidc-use-tls values: @@ -51,44 +51,44 @@ dimensions: - "cluster-internal" - "external-unstable" tests: - # - name: upgrade - # dimensions: - # - nifi_old - # - nifi_new - # - zookeeper-latest - # - openshift - # - name: orphaned_resources - # dimensions: - # - nifi - # - zookeeper-latest - # - openshift - # - name: smoke - # dimensions: - # - nifi - # - zookeeper - # - listener-class - # - openshift - # - name: resources - # dimensions: - # - nifi - # - zookeeper-latest - # - openshift - # - name: ldap - # dimensions: - # - nifi - # - zookeeper-latest - # - ldap-use-tls - # - openshift - # - name: logging - # dimensions: - # - nifi - # - zookeeper-latest - # - openshift - # - name: cluster_operation - # dimensions: - # - nifi-latest - # - zookeeper-latest - # - openshift + - name: upgrade + dimensions: + - nifi_old + - nifi_new + - zookeeper-latest + - openshift + - name: orphaned_resources + dimensions: + - nifi + - zookeeper-latest + - openshift + - name: smoke + dimensions: + - nifi + - zookeeper + - listener-class + - openshift + - name: resources + dimensions: + - nifi + - zookeeper-latest + - openshift + - name: ldap + dimensions: + - nifi + - zookeeper-latest + - ldap-use-tls + - openshift + - name: logging + dimensions: + - nifi + - zookeeper-latest + - openshift + - name: cluster_operation + dimensions: + - nifi-latest + - zookeeper-latest + - openshift - name: oidc dimensions: - nifi @@ -124,3 +124,5 @@ suites: expr: last - name: ldap-use-tls expr: "true" + - name: oidc-use-tls + expr: "true" From 07e8d1ad80d79f4f0d6c2eb0cad485005c798fdc Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Tue, 6 Aug 2024 16:26:46 +0200 Subject: [PATCH 14/27] add docs and fix oidc test --- .../nifi/pages/usage_guide/security.adoc | 60 ++++++++++++++++++- tests/templates/kuttl/oidc/12_nifi.yaml.j2 | 2 +- tests/test-definition.yaml | 2 + 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/docs/modules/nifi/pages/usage_guide/security.adoc b/docs/modules/nifi/pages/usage_guide/security.adoc index ec376611..67d86a97 100644 --- a/docs/modules/nifi/pages/usage_guide/security.adoc +++ b/docs/modules/nifi/pages/usage_guide/security.adoc @@ -96,9 +96,56 @@ spec: - authenticationClass: ldap # <1> ---- -<1> The reference to an AuthenticationClass called `ldap` +<1> The reference to an `AuthenticationClass` called `ldap` -You can follow the xref:tutorials:authentication_with_openldap.adoc[] tutorial to learn how to set up an AuthenticationClass for an LDAP server, as well as consulting the {crd-docs}/authentication.stackable.tech/authenticationclass/v1alpha1/[AuthenticationClass reference {external-link-icon}^]. +You can follow the xref:tutorials:authentication_with_openldap.adoc[] tutorial to learn how to set up an `AuthenticationClass` for an LDAP server, as well as consulting the {crd-docs}/authentication.stackable.tech/authenticationclass/v1alpha1/[AuthenticationClass reference {external-link-icon}^]. + +[#authentication-oidc] +=== OIDC + +NiFi supports xref:concepts:authentication.adoc[authentication] of users against an OIDC provider. +This requires setting up an `AuthenticationClass` for the OIDC provider and specifying a secret containing OIDC client and OIDC client secret as part of the NiFi configuration. +The `AuthenticationClass` and the OIDC client credentials secret are then referenced in the NifiCluster resource: + +[source,yaml] +---- +apiVersion: nifi.stackable.tech/v1alpha1 +kind: NifiCluster +metadata: + name: test-nifi +spec: + clusterConfig: + authentication: + - authenticationClass: oidc # <1> + oidc: + clientCredentialsSecret: nifi-oidc-client # <2> +---- + +<1> The reference to an AuthenticationClass called `oidc` +<2> The reference to an existing secret called `nifi-oidc-client` + +[source,yaml] +---- +apiVersion: authentication.stackable.tech/v1alpha1 +kind: AuthenticationClass +metadata: + name: oidc +spec: + provider: + oidc: + [...] +---- + +[source,yaml] +---- +apiVersion: v1 +kind: Secret +metadata: + name: nifi-oidc-client +stringData: + clientId: + clientSecret: +---- [#authorization] == Authorization @@ -107,6 +154,7 @@ NiFi supports {nifi-docs-authorization}[multiple authorization methods], the ava Authorization is not fully implemented by the Stackable Operator for Apache NiFi. +[#authorization-single-user] === Single user With this authorization method, a single user has administrator capabilities. @@ -118,6 +166,14 @@ The operator uses the {nifi-docs-fileusergroupprovider}[`FileUserGroupProvider`] This user is then able to create and modify groups and policies in the web interface. These changes local to the Pod running NiFi and are *not* persistent. +[#authorization-oidc] +=== OIDC + +With this authorization method, all authenticated users have administrator capabilities. + +An admin user with an auto-generated password is created that can access the NiFi API. +The password for this user is stored in a Kubernetes secret called `-oidc-admin-password`. + [#encrypting-sensitive-properties] == Encrypting sensitive properties on disk diff --git a/tests/templates/kuttl/oidc/12_nifi.yaml.j2 b/tests/templates/kuttl/oidc/12_nifi.yaml.j2 index 927049b2..957bbbf1 100644 --- a/tests/templates/kuttl/oidc/12_nifi.yaml.j2 +++ b/tests/templates/kuttl/oidc/12_nifi.yaml.j2 @@ -42,7 +42,7 @@ spec: nodes: config: logging: - enableVectorAgent: {{ lookup('env', 'VECTOR_AGGREGATOR') }} + enableVectorAgent: {{ lookup('env', 'VECTOR_AGGREGATOR') | length > 0 }} gracefulShutdownTimeout: 1s # let the tests run faster roleGroups: default: diff --git a/tests/test-definition.yaml b/tests/test-definition.yaml index 584756cc..7ea84cef 100644 --- a/tests/test-definition.yaml +++ b/tests/test-definition.yaml @@ -105,6 +105,8 @@ suites: expr: last - name: ldap-use-tls expr: "true" + - name: oidc-use-tls + expr: "true" - name: smoke-latest select: - smoke From d3573432848bcc6348864b375907c0f9f4415e8d Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Tue, 6 Aug 2024 17:58:52 +0200 Subject: [PATCH 15/27] remove reporting task from oidc test --- tests/templates/kuttl/oidc/12-assert.yaml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/templates/kuttl/oidc/12-assert.yaml b/tests/templates/kuttl/oidc/12-assert.yaml index 114d8328..2f03b6b1 100644 --- a/tests/templates/kuttl/oidc/12-assert.yaml +++ b/tests/templates/kuttl/oidc/12-assert.yaml @@ -10,10 +10,3 @@ metadata: status: readyReplicas: 1 replicas: 1 ---- -apiVersion: batch/v1 -kind: Job -metadata: - name: test-nifi-create-reporting-task-1-27-0 -status: - succeeded: 1 From 458cd2937e5d2ceb992016606f0905426ac07e17 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Tue, 6 Aug 2024 21:40:43 +0200 Subject: [PATCH 16/27] add debug logging --- tests/templates/kuttl/oidc/login.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/templates/kuttl/oidc/login.py b/tests/templates/kuttl/oidc/login.py index 8c43e133..a9ec1ecf 100644 --- a/tests/templates/kuttl/oidc/login.py +++ b/tests/templates/kuttl/oidc/login.py @@ -29,6 +29,8 @@ if tls == "true" else f"http://{keycloak_service}:8080" ) +print("[login] found: ", login_page) +print("[login] expected: ", f"{keycloak_base_url}/realms/test/protocol/openid-connect/auth?response_type=code&client_id=nifi&scope=openid%20email%20profile&state=") assert login_page.ok, "Redirection from NiFi to Keycloak failed" assert login_page.url.startswith( f"{keycloak_base_url}/realms/test/protocol/openid-connect/auth?response_type=code&client_id=nifi&scope=openid%20email%20profile&state=" @@ -40,6 +42,8 @@ welcome_page = session.post( authenticate_url, data={"username": "jane.doe", "password": "T8mn72D9"}, verify=False ) +print("[redirect] found: ", welcome_page) +print("[redirect] expected: ", f"https://{nifi}:8443/nifi/") assert welcome_page.ok, "Login failed" assert ( welcome_page.url == f"https://{nifi}:8443/nifi/" From 024744580fb307fc86ed6b5a452bcbaaf1a406e7 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Tue, 6 Aug 2024 22:39:16 +0200 Subject: [PATCH 17/27] fix test logging --- tests/templates/kuttl/oidc/login.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/templates/kuttl/oidc/login.py b/tests/templates/kuttl/oidc/login.py index a9ec1ecf..3ccc54fe 100644 --- a/tests/templates/kuttl/oidc/login.py +++ b/tests/templates/kuttl/oidc/login.py @@ -29,7 +29,7 @@ if tls == "true" else f"http://{keycloak_service}:8080" ) -print("[login] found: ", login_page) +print("[login] found: ", login_page.url) print("[login] expected: ", f"{keycloak_base_url}/realms/test/protocol/openid-connect/auth?response_type=code&client_id=nifi&scope=openid%20email%20profile&state=") assert login_page.ok, "Redirection from NiFi to Keycloak failed" assert login_page.url.startswith( @@ -42,7 +42,7 @@ welcome_page = session.post( authenticate_url, data={"username": "jane.doe", "password": "T8mn72D9"}, verify=False ) -print("[redirect] found: ", welcome_page) +print("[redirect] found: ", welcome_page.url) print("[redirect] expected: ", f"https://{nifi}:8443/nifi/") assert welcome_page.ok, "Login failed" assert ( From 3087d46432df8b4ff27133f052dd68c6e48192cc Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Wed, 7 Aug 2024 00:07:26 +0200 Subject: [PATCH 18/27] use nifi-latest in oidc test --- tests/templates/kuttl/oidc/12_nifi.yaml.j2 | 8 ++++---- tests/templates/kuttl/oidc/login.py | 6 +----- tests/test-definition.yaml | 2 +- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/tests/templates/kuttl/oidc/12_nifi.yaml.j2 b/tests/templates/kuttl/oidc/12_nifi.yaml.j2 index 957bbbf1..d141c700 100644 --- a/tests/templates/kuttl/oidc/12_nifi.yaml.j2 +++ b/tests/templates/kuttl/oidc/12_nifi.yaml.j2 @@ -20,11 +20,11 @@ metadata: name: test-nifi spec: image: -{% if test_scenario['values']['nifi'].find(",") > 0 %} - custom: "{{ test_scenario['values']['nifi'].split(',')[1] }}" - productVersion: "{{ test_scenario['values']['nifi'].split(',')[0] }}" +{% if test_scenario['values']['nifi-latest'].find(",") > 0 %} + custom: "{{ test_scenario['values']['nifi-latest'].split(',')[1] }}" + productVersion: "{{ test_scenario['values']['nifi-latest'].split(',')[0] }}" {% else %} - productVersion: "{{ test_scenario['values']['nifi'] }}" + productVersion: "{{ test_scenario['values']['nifi-latest'] }}" {% endif %} pullPolicy: IfNotPresent clusterConfig: diff --git a/tests/templates/kuttl/oidc/login.py b/tests/templates/kuttl/oidc/login.py index 3ccc54fe..a27c4c9e 100644 --- a/tests/templates/kuttl/oidc/login.py +++ b/tests/templates/kuttl/oidc/login.py @@ -29,11 +29,9 @@ if tls == "true" else f"http://{keycloak_service}:8080" ) -print("[login] found: ", login_page.url) -print("[login] expected: ", f"{keycloak_base_url}/realms/test/protocol/openid-connect/auth?response_type=code&client_id=nifi&scope=openid%20email%20profile&state=") assert login_page.ok, "Redirection from NiFi to Keycloak failed" assert login_page.url.startswith( - f"{keycloak_base_url}/realms/test/protocol/openid-connect/auth?response_type=code&client_id=nifi&scope=openid%20email%20profile&state=" + f"{keycloak_base_url}/realms/test/protocol/openid-connect/auth?response_type=code&client_id=nifi&scope=" ), "Redirection to Keycloak expected" # Login to keycloak with test user @@ -42,8 +40,6 @@ welcome_page = session.post( authenticate_url, data={"username": "jane.doe", "password": "T8mn72D9"}, verify=False ) -print("[redirect] found: ", welcome_page.url) -print("[redirect] expected: ", f"https://{nifi}:8443/nifi/") assert welcome_page.ok, "Login failed" assert ( welcome_page.url == f"https://{nifi}:8443/nifi/" diff --git a/tests/test-definition.yaml b/tests/test-definition.yaml index 7ea84cef..5453c65a 100644 --- a/tests/test-definition.yaml +++ b/tests/test-definition.yaml @@ -91,7 +91,7 @@ tests: - openshift - name: oidc dimensions: - - nifi + - nifi-latest - zookeeper-latest - oidc-use-tls - openshift From cd493511cee32c124ea6189cf4283bc836ea8442 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Wed, 7 Aug 2024 00:25:23 +0200 Subject: [PATCH 19/27] add comment why nifi-latest is used --- tests/test-definition.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test-definition.yaml b/tests/test-definition.yaml index 5453c65a..a0ec0368 100644 --- a/tests/test-definition.yaml +++ b/tests/test-definition.yaml @@ -91,7 +91,7 @@ tests: - openshift - name: oidc dimensions: - - nifi-latest + - nifi-latest # it looks like there are compatibility issues with NiFi 2.0.0-M4 - zookeeper-latest - oidc-use-tls - openshift From 3c98ad51c5d5cd543ff50c1e90b636885295e881 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Wed, 7 Aug 2024 09:19:48 +0200 Subject: [PATCH 20/27] clean up code and add comment --- rust/operator-binary/src/config.rs | 16 ++++------------ rust/operator-binary/src/security/oidc.rs | 1 + 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/rust/operator-binary/src/config.rs b/rust/operator-binary/src/config.rs index 980a9767..5e93b99c 100644 --- a/rust/operator-binary/src/config.rs +++ b/rust/operator-binary/src/config.rs @@ -555,18 +555,10 @@ pub fn build_nifi_properties( "nifi.security.truststorePasswd".to_string(), STACKABLE_TLS_STORE_PASSWORD.to_string(), ); - - if let NifiAuthenticationConfig::Oidc { .. } = auth_config { - properties.insert( - "nifi.security.user.login.identity.provider".to_string(), - "login-identity-provider".to_string(), - ); - } else { - properties.insert( - "nifi.security.user.login.identity.provider".to_string(), - "login-identity-provider".to_string(), - ); - } + properties.insert( + "nifi.security.user.login.identity.provider".to_string(), + "login-identity-provider".to_string(), + ); properties.insert( "nifi.security.user.authorizer".to_string(), "authorizer".to_string(), diff --git a/rust/operator-binary/src/security/oidc.rs b/rust/operator-binary/src/security/oidc.rs index 61aadf70..bd899418 100644 --- a/rust/operator-binary/src/security/oidc.rs +++ b/rust/operator-binary/src/security/oidc.rs @@ -33,6 +33,7 @@ pub enum Error { OidcAdminPasswordKeyMissing { name: String, namespace: String }, } +/// Generate a secret containing the password for the admin user that can access the API. This admin user is the same as for SingleUser authentication. pub(crate) async fn check_or_generate_oidc_admin_password( client: &Client, nifi: &NifiCluster, From 9d11920857ddf0245d82acd003062922a06a1fcc Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Thu, 15 Aug 2024 14:07:40 +0200 Subject: [PATCH 21/27] address feedback from review --- Cargo.lock | 1 + Cargo.toml | 1 + .../nifi/pages/usage_guide/security.adoc | 26 ++++---- rust/crd/src/authentication.rs | 62 +++---------------- rust/crd/src/lib.rs | 3 + rust/operator-binary/Cargo.toml | 1 + rust/operator-binary/src/config.rs | 27 +++++--- rust/operator-binary/src/controller.rs | 1 - .../src/security/authentication.rs | 14 ++--- rust/operator-binary/src/security/oidc.rs | 2 +- .../templates/kuttl/oidc/01_keycloak.yaml.j2 | 26 -------- .../11-create-authentication-classes.yaml.j2 | 1 - .../oidc/11_authentication-classes.yaml.j2 | 1 - 13 files changed, 51 insertions(+), 115 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a5a4ae1d..537a5e5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2091,6 +2091,7 @@ dependencies = [ "strum", "tokio", "tracing", + "url", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 93f99844..313c1797 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ stackable-operator = { git = "https://github.com/stackabletech/operator-rs.git", strum = { version = "0.26", features = ["derive"] } tokio = { version = "1.39", features = ["full"] } tracing = "0.1" +url = { version = "2.5.2" } xml-rs = "0.8" # [patch."https://github.com/stackabletech/operator-rs.git"] diff --git a/docs/modules/nifi/pages/usage_guide/security.adoc b/docs/modules/nifi/pages/usage_guide/security.adoc index 67d86a97..1ce24d4a 100644 --- a/docs/modules/nifi/pages/usage_guide/security.adoc +++ b/docs/modules/nifi/pages/usage_guide/security.adoc @@ -22,7 +22,7 @@ spec: serverSecretClass: non-default-secret-class # <1> ---- -<1> The name of the `SecretClass` that will be used for certificates for the NiFi UI. +<1> The name of the SecretClass that will be used for certificates for the NiFi UI. == Authentication @@ -49,8 +49,8 @@ spec: name: nifi-admin-credentials # <2> ---- -<1> The name of the `AuthenticationClass` that will be referenced in the NiFi cluster. -<2> The name of the `Secret` containing the admin credentials. +<1> The name of the AuthenticationClass that will be referenced in the NiFi cluster. +<2> The name of the Secret containing the admin credentials. [source,yaml] ---- @@ -63,9 +63,9 @@ stringData: bob: bob # <3> ---- -<1> The name of the `Secret` containing the admin user credentials. -<2> The user and password combination of the admin user. The username *must* be "admin" and cannot be changed. The NiFi pods will not start if they cannot mount the "admin" entry from the secret. The password can be adapted. -<3> The secret maybe used by other products of the Stackable Data Platform that allow more than one user. The Stackable Operator for Apache NiFi will ignore all users except for "admin". +<1> The name of the Secret containing the admin user credentials. +<2> The user and password combination of the admin user. The username *must* be "admin" and cannot be changed. The NiFi pods will not start if they cannot mount the "admin" entry from the Secret. The password can be adapted. +<3> The Secret maybe used by other products of the Stackable Data Platform that allow more than one user. The Stackable Operator for Apache NiFi will ignore all users except for "admin". [source,yaml] ---- @@ -75,7 +75,7 @@ spec: - authenticationClass: simple-nifi-users # <1> ---- -<1> The reference to an `AuthenticationClass`. NiFi only supports one authentication mechanism at a time. +<1> The reference to an AuthenticationClass. NiFi only supports one authentication mechanism at a time. [#authentication-ldap] === LDAP @@ -96,16 +96,16 @@ spec: - authenticationClass: ldap # <1> ---- -<1> The reference to an `AuthenticationClass` called `ldap` +<1> The reference to an AuthenticationClass called `ldap` -You can follow the xref:tutorials:authentication_with_openldap.adoc[] tutorial to learn how to set up an `AuthenticationClass` for an LDAP server, as well as consulting the {crd-docs}/authentication.stackable.tech/authenticationclass/v1alpha1/[AuthenticationClass reference {external-link-icon}^]. +You can follow the xref:tutorials:authentication_with_openldap.adoc[] tutorial to learn how to set up an AuthenticationClass for an LDAP server, as well as consulting the {crd-docs}/authentication.stackable.tech/authenticationclass/v1alpha1/[AuthenticationClass reference {external-link-icon}^]. [#authentication-oidc] === OIDC NiFi supports xref:concepts:authentication.adoc[authentication] of users against an OIDC provider. -This requires setting up an `AuthenticationClass` for the OIDC provider and specifying a secret containing OIDC client and OIDC client secret as part of the NiFi configuration. -The `AuthenticationClass` and the OIDC client credentials secret are then referenced in the NifiCluster resource: +This requires setting up an AuthenticationClass for the OIDC provider and specifying a Secret containing OIDC client and OIDC client Secret as part of the NiFi configuration. +The AuthenticationClass and the OIDC client credentials Secret are then referenced in the NifiCluster resource: [source,yaml] ---- @@ -122,7 +122,7 @@ spec: ---- <1> The reference to an AuthenticationClass called `oidc` -<2> The reference to an existing secret called `nifi-oidc-client` +<2> The reference to an existing Secret called `nifi-oidc-client` [source,yaml] ---- @@ -172,7 +172,7 @@ These changes local to the Pod running NiFi and are *not* persistent. With this authorization method, all authenticated users have administrator capabilities. An admin user with an auto-generated password is created that can access the NiFi API. -The password for this user is stored in a Kubernetes secret called `-oidc-admin-password`. +The password for this user is stored in a Kubernetes Secret called `-oidc-admin-password`. [#encrypting-sensitive-properties] == Encrypting sensitive properties on disk diff --git a/rust/crd/src/authentication.rs b/rust/crd/src/authentication.rs index 1578f96e..c82aa869 100644 --- a/rust/crd/src/authentication.rs +++ b/rust/crd/src/authentication.rs @@ -1,7 +1,6 @@ use std::future::Future; -use snafu::{ensure, ResultExt, Snafu}; -use stackable_operator::commons::authentication::oidc::IdentityProviderHint; +use snafu::{ResultExt, Snafu}; use stackable_operator::commons::authentication::{ ldap, oidc, static_, AuthenticationClassProvider, ClientAuthenticationDetails, }; @@ -10,15 +9,9 @@ use stackable_operator::{ client::Client, commons::authentication::AuthenticationClass, kube::runtime::reflector::ObjectRef, }; -use tracing::info; use crate::NifiCluster; -// The assumed OIDC provider if no hint is given in the AuthClass -pub const DEFAULT_OIDC_PROVIDER: IdentityProviderHint = IdentityProviderHint::Keycloak; - -const SUPPORTED_OIDC_PROVIDERS: &[IdentityProviderHint] = &[IdentityProviderHint::Keycloak]; - #[derive(Snafu, Debug)] pub enum Error { #[snafu(display("failed to retrieve AuthenticationClass"))] @@ -42,15 +35,11 @@ pub enum Error { NoLdapTlsVerificationNotSupported { authentication_class: ObjectRef, }, + #[snafu(display("invalid OIDC configuration"))] OidcConfigurationInvalid { source: stackable_operator::commons::authentication::Error, }, - #[snafu(display("the OIDC provider {oidc_provider:?} is not yet supported (AuthenticationClass {auth_class_name:?})"))] - OidcProviderNotSupported { - auth_class_name: String, - oidc_provider: String, - }, } type Result = std::result::Result; @@ -125,12 +114,14 @@ impl AuthenticationClassResolved { }) } AuthenticationClassProvider::Oidc(provider) => { - resolved_auth_classes.push(AuthenticationClassResolved::from_oidc( - &auth_class_name, - provider, - entry, - nifi.clone(), - )?) + resolved_auth_classes.push(Ok(AuthenticationClassResolved::Oidc { + provider: provider.to_owned(), + oidc: entry + .oidc_or_error(&auth_class_name) + .context(OidcConfigurationInvalidSnafu)? + .clone(), + nifi: nifi.clone(), + })?) } _ => AuthenticationClassProviderNotSupportedSnafu { authentication_class_provider: auth_class.spec.provider.to_string(), @@ -142,37 +133,4 @@ impl AuthenticationClassResolved { Ok(resolved_auth_classes) } - - fn from_oidc( - auth_class_name: &str, - provider: &oidc::AuthenticationProvider, - auth_details: &ClientAuthenticationDetails, - nifi: NifiCluster, - ) -> Result { - let oidc_provider = match &provider.provider_hint { - None => { - info!("No OIDC provider hint given in AuthClass {auth_class_name}, assuming {default_oidc_provider_name}", - default_oidc_provider_name = serde_json::to_string(&DEFAULT_OIDC_PROVIDER).unwrap()); - DEFAULT_OIDC_PROVIDER - } - Some(oidc_provider) => oidc_provider.to_owned(), - }; - - ensure!( - SUPPORTED_OIDC_PROVIDERS.contains(&oidc_provider), - OidcProviderNotSupportedSnafu { - auth_class_name, - oidc_provider: serde_json::to_string(&oidc_provider).unwrap(), - } - ); - - Ok(AuthenticationClassResolved::Oidc { - provider: provider.to_owned(), - oidc: auth_details - .oidc_or_error(auth_class_name) - .context(OidcConfigurationInvalidSnafu)? - .clone(), - nifi, - }) - } } diff --git a/rust/crd/src/lib.rs b/rust/crd/src/lib.rs index 91262b11..c996ab11 100644 --- a/rust/crd/src/lib.rs +++ b/rust/crd/src/lib.rs @@ -64,10 +64,13 @@ const DEFAULT_NODE_GRACEFUL_SHUTDOWN_TIMEOUT: Duration = Duration::from_minutes_ pub enum Error { #[snafu(display("object has no namespace associated"))] NoNamespace, + #[snafu(display("the NiFi role [{role}] is missing from spec"))] MissingNifiRole { role: String }, + #[snafu(display("the NiFi node role group [{role_group}] is missing from spec"))] MissingNifiRoleGroup { role_group: String }, + #[snafu(display("fragment validation failure"))] FragmentValidationFailure { source: ValidationError }, } diff --git a/rust/operator-binary/Cargo.toml b/rust/operator-binary/Cargo.toml index 021e58fb..d0d074e1 100644 --- a/rust/operator-binary/Cargo.toml +++ b/rust/operator-binary/Cargo.toml @@ -27,6 +27,7 @@ product-config.workspace = true strum.workspace = true tokio.workspace = true tracing.workspace = true +url.workspace = true [build-dependencies] built.workspace = true diff --git a/rust/operator-binary/src/config.rs b/rust/operator-binary/src/config.rs index 5e93b99c..3c1d71cc 100644 --- a/rust/operator-binary/src/config.rs +++ b/rust/operator-binary/src/config.rs @@ -70,27 +70,34 @@ impl NifiRepository { #[derive(Snafu, Debug)] pub enum Error { - #[snafu(display("Invalid product config"))] + #[snafu(display("invalid product config"))] InvalidProductConfig { source: stackable_operator::product_config_utils::Error, }, - #[snafu(display("Invalid memory config"))] + + #[snafu(display("invalid memory config"))] InvalidMemoryConfig { source: stackable_operator::memory::Error, }, - #[snafu(display("Failed to transform product configs"))] + + #[snafu(display("failed to transform product configs"))] ProductConfigTransform { source: stackable_operator::product_config_utils::Error, }, + #[snafu(display("failed to calculate storage quota for {repo} repository"))] CalculateStorageQuota { source: stackable_operator::memory::Error, repo: NifiRepository, }, - #[snafu(display("Invalid OIDC endpoint URL"))] + + #[snafu(display("invalid OIDC endpoint URL"))] InvalidOidcEndpoint { source: stackable_operator::commons::authentication::oidc::Error, }, + + #[snafu(display("failed to build the OIDC wellkown path"))] + InvalidOidcWellknownPath { source: url::ParseError }, } /// Create the NiFi bootstrap.conf @@ -572,16 +579,17 @@ pub fn build_nifi_properties( "true".to_string(), ); + // OIDC config if let NifiAuthenticationConfig::Oidc { provider, oidc, .. } = auth_config { let endpoint_url = provider .endpoint_url() .context(InvalidOidcEndpointSnafu)? - .to_string(); + .join(DEFAULT_OIDC_WELLKNOWN_PATH) + .context(InvalidOidcWellknownPathSnafu)?; properties.insert( "nifi.security.user.oidc.discovery.url".to_string(), - format!("{endpoint_url}/{DEFAULT_OIDC_WELLKNOWN_PATH}"), + endpoint_url.to_string(), ); - let (oidc_client_id_env, oidc_client_secret_env) = AuthenticationProvider::client_credentials_env_names( &oidc.client_credentials_secret_ref, @@ -590,18 +598,15 @@ pub fn build_nifi_properties( "nifi.security.user.oidc.client.id".to_string(), format!("${{env:{oidc_client_id_env}}}").to_string(), ); - properties.insert( "nifi.security.user.oidc.client.secret".to_string(), format!("${{env:{oidc_client_secret_env}}}").to_string(), ); - let scopes = provider.scopes.join(","); properties.insert( "nifi.security.user.oidc.additional.scopes".to_string(), scopes.to_string(), ); - properties.insert( "nifi.security.user.oidc.claim.identifying.user".to_string(), provider.principal_claim.to_string(), @@ -631,12 +636,14 @@ pub fn build_nifi_properties( "nifi.cluster.flow.election.max.candidates".to_string(), "".to_string(), ); + // zookeeper properties, used for cluster management // this will be replaced via a container command script properties.insert( "nifi.zookeeper.connect.string".to_string(), "${env:ZOOKEEPER_HOSTS}".to_string(), ); + // this will be replaced via a container command script properties.insert( "nifi.zookeeper.root.node".to_string(), diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 2a6aa05f..bc9ba193 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -469,7 +469,6 @@ pub async fn reconcile_nifi(nifi: Arc, ctx: Arc) -> Result, + mut container_builders: Vec<&mut ContainerBuilder>, ) -> Result<(), Error> { match &self { Self::SingleUser { provider } => { @@ -211,15 +211,9 @@ impl NifiAuthenticationConfig { }; pod_builder.add_volume(admin_volume); - let container_builders = container_builders - .into_iter() - .map(|cb| { - cb.add_volume_mount( - STACKABLE_ADMIN_USERNAME, - STACKABLE_USER_VOLUME_MOUNT_PATH, - ) - }) - .collect(); + container_builders.iter_mut().for_each(|cb| { + cb.add_volume_mount(STACKABLE_ADMIN_USERNAME, STACKABLE_USER_VOLUME_MOUNT_PATH); + }); provider .tls diff --git a/rust/operator-binary/src/security/oidc.rs b/rust/operator-binary/src/security/oidc.rs index bd899418..227d2877 100644 --- a/rust/operator-binary/src/security/oidc.rs +++ b/rust/operator-binary/src/security/oidc.rs @@ -39,7 +39,7 @@ pub(crate) async fn check_or_generate_oidc_admin_password( nifi: &NifiCluster, ) -> Result { let namespace: &str = &nifi.namespace().context(ObjectHasNoNamespaceSnafu)?; - + tracing::debug!("Checking for OIDC admin password configuration"); match client .get_opt::(&build_oidc_admin_password_secret_name(nifi), namespace) .await diff --git a/tests/templates/kuttl/oidc/01_keycloak.yaml.j2 b/tests/templates/kuttl/oidc/01_keycloak.yaml.j2 index a5b3f3b6..6f0d37b6 100644 --- a/tests/templates/kuttl/oidc/01_keycloak.yaml.j2 +++ b/tests/templates/kuttl/oidc/01_keycloak.yaml.j2 @@ -146,32 +146,6 @@ spec: port: 8080 {% endif %} --- -apiVersion: authentication.stackable.tech/v1alpha1 -kind: AuthenticationClass -metadata: - name: $INSTANCE_NAME-$NAMESPACE -spec: - provider: - oidc: - hostname: $INSTANCE_NAME.$NAMESPACE.svc.cluster.local -{% if test_scenario['values']['oidc-use-tls'] == 'true' %} - port: 8443 -{% else %} - port: 8080 -{% endif %} - rootPath: /realms/$REALM - scopes: - - email - - openid - - profile - principalClaim: preferred_username - providerHint: Keycloak - tls: - verification: - server: - caCert: - secretClass: keycloak-tls ---- apiVersion: v1 kind: ServiceAccount metadata: diff --git a/tests/templates/kuttl/oidc/11-create-authentication-classes.yaml.j2 b/tests/templates/kuttl/oidc/11-create-authentication-classes.yaml.j2 index 28895f6c..6efd6383 100644 --- a/tests/templates/kuttl/oidc/11-create-authentication-classes.yaml.j2 +++ b/tests/templates/kuttl/oidc/11-create-authentication-classes.yaml.j2 @@ -3,5 +3,4 @@ apiVersion: kuttl.dev/v1beta1 kind: TestStep commands: # We need to replace $NAMESPACE (by KUTTL) in the create-authentication-classes.yaml(.j2) - - script: ls - script: envsubst < 11_authentication-classes.yaml | kubectl apply -n $NAMESPACE -f - diff --git a/tests/templates/kuttl/oidc/11_authentication-classes.yaml.j2 b/tests/templates/kuttl/oidc/11_authentication-classes.yaml.j2 index af320d4d..eb8e4a98 100644 --- a/tests/templates/kuttl/oidc/11_authentication-classes.yaml.j2 +++ b/tests/templates/kuttl/oidc/11_authentication-classes.yaml.j2 @@ -13,7 +13,6 @@ spec: - openid - email - profile - providerHint: Keycloak {% if test_scenario['values']['oidc-use-tls'] == 'true' %} port: 8443 tls: From d7e9c82de963d5e34e7bd33859158035c2c9bae4 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Thu, 15 Aug 2024 17:34:16 +0200 Subject: [PATCH 22/27] improve oidc integration test --- .../nifi/pages/usage_guide/security.adoc | 11 ++++++ .../templates/kuttl/oidc/01_keycloak.yaml.j2 | 34 ++---------------- tests/templates/kuttl/oidc/03-assert.yaml | 12 ------- .../kuttl/oidc/03-install-test-nifi.yaml.j2 | 25 ------------- .../oidc/11_authentication-classes.yaml.j2 | 4 +-- tests/templates/kuttl/oidc/20-assert.yaml | 11 +++--- .../kuttl/oidc/20-login-test.yaml.j2 | 36 +++++++++++++++++++ tests/templates/kuttl/oidc/20-login.yaml | 7 ---- tests/templates/kuttl/oidc/login.py | 4 +-- 9 files changed, 59 insertions(+), 85 deletions(-) delete mode 100644 tests/templates/kuttl/oidc/03-assert.yaml delete mode 100644 tests/templates/kuttl/oidc/03-install-test-nifi.yaml.j2 create mode 100644 tests/templates/kuttl/oidc/20-login-test.yaml.j2 delete mode 100644 tests/templates/kuttl/oidc/20-login.yaml diff --git a/docs/modules/nifi/pages/usage_guide/security.adoc b/docs/modules/nifi/pages/usage_guide/security.adoc index 1ce24d4a..6e7edfdd 100644 --- a/docs/modules/nifi/pages/usage_guide/security.adoc +++ b/docs/modules/nifi/pages/usage_guide/security.adoc @@ -133,9 +133,20 @@ metadata: spec: provider: oidc: + hostname: keycloak.example.com + rootPath: /realms/test/ # <1> + principalClaim: preferred_username + scopes: + - openid + - email + - profile + port: 8080 + tls: null [...] ---- +<1> A trailing slash in `rootPath` is necessary. + [source,yaml] ---- apiVersion: v1 diff --git a/tests/templates/kuttl/oidc/01_keycloak.yaml.j2 b/tests/templates/kuttl/oidc/01_keycloak.yaml.j2 index 6f0d37b6..1331f2ac 100644 --- a/tests/templates/kuttl/oidc/01_keycloak.yaml.j2 +++ b/tests/templates/kuttl/oidc/01_keycloak.yaml.j2 @@ -72,7 +72,6 @@ spec: labels: app: $INSTANCE_NAME spec: - serviceAccountName: keycloak containers: - name: keycloak image: quay.io/keycloak/keycloak:23.0.4 @@ -119,7 +118,7 @@ spec: volumeClaimTemplate: metadata: annotations: - secrets.stackable.tech/class: keycloak-tls + secrets.stackable.tech/class: keycloak-tls-$NAMESPACE secrets.stackable.tech/scope: service=$INSTANCE_NAME spec: accessModes: @@ -146,39 +145,10 @@ spec: port: 8080 {% endif %} --- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: keycloak ---- -kind: Role -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: keycloak -{% if test_scenario['values']['openshift'] == 'true' %} -rules: -- apiGroups: ["security.openshift.io"] - resources: ["securitycontextconstraints"] - resourceNames: ["privileged"] - verbs: ["use"] -{% endif %} ---- -kind: RoleBinding -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: keycloak -subjects: - - kind: ServiceAccount - name: keycloak -roleRef: - kind: Role - name: keycloak - apiGroup: rbac.authorization.k8s.io ---- apiVersion: secrets.stackable.tech/v1alpha1 kind: SecretClass metadata: - name: keycloak-tls + name: keycloak-tls-$NAMESPACE spec: backend: autoTls: diff --git a/tests/templates/kuttl/oidc/03-assert.yaml b/tests/templates/kuttl/oidc/03-assert.yaml deleted file mode 100644 index d511ff46..00000000 --- a/tests/templates/kuttl/oidc/03-assert.yaml +++ /dev/null @@ -1,12 +0,0 @@ ---- -apiVersion: kuttl.dev/v1beta1 -kind: TestAssert -timeout: 300 ---- -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: test-nifi -status: - readyReplicas: 1 - replicas: 1 diff --git a/tests/templates/kuttl/oidc/03-install-test-nifi.yaml.j2 b/tests/templates/kuttl/oidc/03-install-test-nifi.yaml.j2 deleted file mode 100644 index 6b15718c..00000000 --- a/tests/templates/kuttl/oidc/03-install-test-nifi.yaml.j2 +++ /dev/null @@ -1,25 +0,0 @@ ---- -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: test-nifi - labels: - app: test-nifi -spec: - replicas: 1 - selector: - matchLabels: - app: test-nifi - template: - metadata: - labels: - app: test-nifi - spec: - containers: - - name: test-nifi - image: docker.stackable.tech/stackable/testing-tools:0.2.0-stackable0.0.0-dev - command: ["sleep", "infinity"] - env: - - name: OIDC_USE_TLS - value: "{{ test_scenario['values']['oidc-use-tls'] }}" - terminationGracePeriodSeconds: 1 diff --git a/tests/templates/kuttl/oidc/11_authentication-classes.yaml.j2 b/tests/templates/kuttl/oidc/11_authentication-classes.yaml.j2 index eb8e4a98..8b0afe51 100644 --- a/tests/templates/kuttl/oidc/11_authentication-classes.yaml.j2 +++ b/tests/templates/kuttl/oidc/11_authentication-classes.yaml.j2 @@ -7,7 +7,7 @@ spec: provider: oidc: hostname: keycloak.$NAMESPACE.svc.cluster.local - rootPath: /realms/test + rootPath: /realms/test/ principalClaim: preferred_username scopes: - openid @@ -19,7 +19,7 @@ spec: verification: server: caCert: - secretClass: keycloak-tls + secretClass: keycloak-tls-$NAMESPACE {% else %} port: 8080 tls: null diff --git a/tests/templates/kuttl/oidc/20-assert.yaml b/tests/templates/kuttl/oidc/20-assert.yaml index 88904dbd..061ec316 100644 --- a/tests/templates/kuttl/oidc/20-assert.yaml +++ b/tests/templates/kuttl/oidc/20-assert.yaml @@ -1,8 +1,11 @@ --- apiVersion: kuttl.dev/v1beta1 kind: TestAssert -metadata: - name: login timeout: 300 -commands: - - script: kubectl exec -n $NAMESPACE test-nifi-0 -- python /stackable/login.py $NAMESPACE +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: oidc-login-test +status: + succeeded: 1 diff --git a/tests/templates/kuttl/oidc/20-login-test.yaml.j2 b/tests/templates/kuttl/oidc/20-login-test.yaml.j2 new file mode 100644 index 00000000..bd44baa6 --- /dev/null +++ b/tests/templates/kuttl/oidc/20-login-test.yaml.j2 @@ -0,0 +1,36 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +metadata: + name: oidc-login-test-script +commands: + - script: kubectl create configmap oidc-login-test-script --from-file login.py -n $NAMESPACE +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: oidc-login-test +spec: + template: + spec: + containers: + - name: oidc-login-test + image: docker.stackable.tech/stackable/testing-tools:0.2.0-stackable0.0.0-dev + command: ["python", "/tmp/test-script/login.py"] + env: + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: OIDC_USE_TLS + value: "{{ test_scenario['values']['oidc-use-tls'] }}" + volumeMounts: + - name: test-script + mountPath: /tmp/test-script + restartPolicy: OnFailure + terminationGracePeriodSeconds: 1 + volumes: + - name: test-script + configMap: + name: oidc-login-test-script +--- diff --git a/tests/templates/kuttl/oidc/20-login.yaml b/tests/templates/kuttl/oidc/20-login.yaml deleted file mode 100644 index 88650713..00000000 --- a/tests/templates/kuttl/oidc/20-login.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -apiVersion: kuttl.dev/v1beta1 -kind: TestStep -commands: - - script: > - envsubst '$NAMESPACE' < login.py | - kubectl exec -n $NAMESPACE -i test-nifi-0 -- tee /stackable/login.py > /dev/null diff --git a/tests/templates/kuttl/oidc/login.py b/tests/templates/kuttl/oidc/login.py index a27c4c9e..0b37bd59 100644 --- a/tests/templates/kuttl/oidc/login.py +++ b/tests/templates/kuttl/oidc/login.py @@ -1,5 +1,3 @@ -# $NAMESPACE will be replaced with the namespace of the test case. - import logging import os import requests @@ -10,7 +8,7 @@ level="DEBUG", format="%(asctime)s %(levelname)s: %(message)s", stream=sys.stdout ) -namespace = sys.argv[1] +namespace = os.environ["NAMESPACE"] tls = os.environ["OIDC_USE_TLS"] session = requests.Session() From dce0f7cb365252ab94a7d95621243af4d153602a Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Fri, 16 Aug 2024 16:05:09 +0200 Subject: [PATCH 23/27] fix oidc test for nifi 2.0.0-M4 --- tests/templates/kuttl/oidc/12_nifi.yaml.j2 | 8 ++--- tests/templates/kuttl/oidc/20-assert.yaml | 2 +- .../kuttl/oidc/20-login-test.yaml.j2 | 3 +- tests/templates/kuttl/oidc/21-assert.yaml | 11 +++++++ .../kuttl/oidc/21-login-test-logs.yaml | 7 +++++ tests/templates/kuttl/oidc/login.py | 30 +++++++++++++++---- tests/test-definition.yaml | 2 +- 7 files changed, 50 insertions(+), 13 deletions(-) create mode 100644 tests/templates/kuttl/oidc/21-assert.yaml create mode 100644 tests/templates/kuttl/oidc/21-login-test-logs.yaml diff --git a/tests/templates/kuttl/oidc/12_nifi.yaml.j2 b/tests/templates/kuttl/oidc/12_nifi.yaml.j2 index d141c700..957bbbf1 100644 --- a/tests/templates/kuttl/oidc/12_nifi.yaml.j2 +++ b/tests/templates/kuttl/oidc/12_nifi.yaml.j2 @@ -20,11 +20,11 @@ metadata: name: test-nifi spec: image: -{% if test_scenario['values']['nifi-latest'].find(",") > 0 %} - custom: "{{ test_scenario['values']['nifi-latest'].split(',')[1] }}" - productVersion: "{{ test_scenario['values']['nifi-latest'].split(',')[0] }}" +{% if test_scenario['values']['nifi'].find(",") > 0 %} + custom: "{{ test_scenario['values']['nifi'].split(',')[1] }}" + productVersion: "{{ test_scenario['values']['nifi'].split(',')[0] }}" {% else %} - productVersion: "{{ test_scenario['values']['nifi-latest'] }}" + productVersion: "{{ test_scenario['values']['nifi'] }}" {% endif %} pullPolicy: IfNotPresent clusterConfig: diff --git a/tests/templates/kuttl/oidc/20-assert.yaml b/tests/templates/kuttl/oidc/20-assert.yaml index 061ec316..a255c48d 100644 --- a/tests/templates/kuttl/oidc/20-assert.yaml +++ b/tests/templates/kuttl/oidc/20-assert.yaml @@ -8,4 +8,4 @@ kind: Job metadata: name: oidc-login-test status: - succeeded: 1 + ready: 1 diff --git a/tests/templates/kuttl/oidc/20-login-test.yaml.j2 b/tests/templates/kuttl/oidc/20-login-test.yaml.j2 index bd44baa6..782711f8 100644 --- a/tests/templates/kuttl/oidc/20-login-test.yaml.j2 +++ b/tests/templates/kuttl/oidc/20-login-test.yaml.j2 @@ -22,6 +22,8 @@ spec: valueFrom: fieldRef: fieldPath: metadata.namespace + - name: NIFI_VERSION + value: "{{ test_scenario['values']['nifi'] }}" - name: OIDC_USE_TLS value: "{{ test_scenario['values']['oidc-use-tls'] }}" volumeMounts: @@ -33,4 +35,3 @@ spec: - name: test-script configMap: name: oidc-login-test-script ---- diff --git a/tests/templates/kuttl/oidc/21-assert.yaml b/tests/templates/kuttl/oidc/21-assert.yaml new file mode 100644 index 00000000..f55ee23d --- /dev/null +++ b/tests/templates/kuttl/oidc/21-assert.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 30 +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: oidc-login-test +status: + succeeded: 1 diff --git a/tests/templates/kuttl/oidc/21-login-test-logs.yaml b/tests/templates/kuttl/oidc/21-login-test-logs.yaml new file mode 100644 index 00000000..092debe4 --- /dev/null +++ b/tests/templates/kuttl/oidc/21-login-test-logs.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +metadata: + name: oidc-login-test-logs +commands: + - script: kubectl logs job/oidc-login-test -n $NAMESPACE -f diff --git a/tests/templates/kuttl/oidc/login.py b/tests/templates/kuttl/oidc/login.py index 0b37bd59..2ab1a54e 100644 --- a/tests/templates/kuttl/oidc/login.py +++ b/tests/templates/kuttl/oidc/login.py @@ -2,6 +2,7 @@ import os import requests import sys +import json from bs4 import BeautifulSoup logging.basicConfig( @@ -10,23 +11,40 @@ namespace = os.environ["NAMESPACE"] tls = os.environ["OIDC_USE_TLS"] +nifi_version = os.environ["NIFI_VERSION"] session = requests.Session() nifi = f"test-nifi-node-default-0.test-nifi-node-default.{namespace}.svc.cluster.local" keycloak_service = f"keycloak.{namespace}.svc.cluster.local" -# Open NiFi web UI which will redirect to OIDC login -login_page = session.get( - f"https://{nifi}:8443/nifi/login", - verify=False, - headers={"Content-type": "application/json"}, -) keycloak_base_url = ( f"https://{keycloak_service}:8443" if tls == "true" else f"http://{keycloak_service}:8080" ) + +if nifi_version in ["2.0.0-M4"]: + auth_config_page = session.get( + f"https://{nifi}:8443/nifi-api/authentication/configuration", + verify=False, + headers={"Content-type": "application/json"} + ) + assert auth_config_page.ok, "Could not fetch auth config from NiFi" + auth_config = json.loads(auth_config_page.text) + login_url = auth_config["authenticationConfiguration"]["loginUri"] +else: + login_url = f"https://{nifi}:8443/nifi/login" + +# Open NiFi web UI which will redirect to OIDC login +login_page = session.get( + login_url, + verify=False, + headers={"Content-type": "application/json"}, +) + +print("actual: ", login_page.url) +print("expected: ", f"{keycloak_base_url}/realms/test/protocol/openid-connect/auth?response_type=code&client_id=nifi&scope=") assert login_page.ok, "Redirection from NiFi to Keycloak failed" assert login_page.url.startswith( f"{keycloak_base_url}/realms/test/protocol/openid-connect/auth?response_type=code&client_id=nifi&scope=" diff --git a/tests/test-definition.yaml b/tests/test-definition.yaml index a0ec0368..7ea84cef 100644 --- a/tests/test-definition.yaml +++ b/tests/test-definition.yaml @@ -91,7 +91,7 @@ tests: - openshift - name: oidc dimensions: - - nifi-latest # it looks like there are compatibility issues with NiFi 2.0.0-M4 + - nifi - zookeeper-latest - oidc-use-tls - openshift From 0959c7581714fa1dd72570334f745a5d9d6c0562 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Sat, 17 Aug 2024 09:20:41 +0200 Subject: [PATCH 24/27] increase timeout on test job creation --- tests/templates/kuttl/oidc/20-assert.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/templates/kuttl/oidc/20-assert.yaml b/tests/templates/kuttl/oidc/20-assert.yaml index a255c48d..ac8fb356 100644 --- a/tests/templates/kuttl/oidc/20-assert.yaml +++ b/tests/templates/kuttl/oidc/20-assert.yaml @@ -1,7 +1,7 @@ --- apiVersion: kuttl.dev/v1beta1 kind: TestAssert -timeout: 300 +timeout: 600 --- apiVersion: batch/v1 kind: Job From a4e5fabf892b235f2078bdef70c2f14ed3371e93 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Mon, 26 Aug 2024 10:56:36 +0200 Subject: [PATCH 25/27] fix docs on oidc --- docs/modules/nifi/pages/usage_guide/security.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/nifi/pages/usage_guide/security.adoc b/docs/modules/nifi/pages/usage_guide/security.adoc index 6e7edfdd..b9f733e8 100644 --- a/docs/modules/nifi/pages/usage_guide/security.adoc +++ b/docs/modules/nifi/pages/usage_guide/security.adoc @@ -104,7 +104,7 @@ You can follow the xref:tutorials:authentication_with_openldap.adoc[] tutorial t === OIDC NiFi supports xref:concepts:authentication.adoc[authentication] of users against an OIDC provider. -This requires setting up an AuthenticationClass for the OIDC provider and specifying a Secret containing OIDC client and OIDC client Secret as part of the NiFi configuration. +This requires setting up an AuthenticationClass for the OIDC provider and specifying a Secret containing the OIDC client id and client secret as part of the NiFi configuration. The AuthenticationClass and the OIDC client credentials Secret are then referenced in the NifiCluster resource: [source,yaml] From a07ec4fef135807c513b9a9fc3c759a260c85926 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Tue, 27 Aug 2024 14:46:24 +0200 Subject: [PATCH 26/27] move config for debugger to operator-templating --- .vscode/launch.json | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index c310a034..00000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "type": "lldb", - "request": "launch", - "name": "Debug executable 'stackable-nifi-operator'", - "cargo": { - "args": [ - "build", - "--bin=stackable-nifi-operator", - "--package=stackable-nifi-operator" - ], - "filter": { - "name": "stackable-nifi-operator", - "kind": "bin" - } - }, - "args": ["run"], - "cwd": "${workspaceFolder}" - } - ] -} From 447ed264b68f2fb5eb8d38e0bf86aa9aeee6c73c Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Tue, 27 Aug 2024 15:00:41 +0200 Subject: [PATCH 27/27] add comment to test job assert --- tests/templates/kuttl/oidc/20-assert.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/templates/kuttl/oidc/20-assert.yaml b/tests/templates/kuttl/oidc/20-assert.yaml index ac8fb356..ce21f771 100644 --- a/tests/templates/kuttl/oidc/20-assert.yaml +++ b/tests/templates/kuttl/oidc/20-assert.yaml @@ -8,4 +8,4 @@ kind: Job metadata: name: oidc-login-test status: - ready: 1 + ready: 1 # wait for the test job to start before streaming its logs in the next test step