diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 39623578..a85729a7 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -106,7 +106,7 @@ use crate::{ NifiAuthenticationConfig, STACKABLE_SERVER_TLS_DIR, STACKABLE_TLS_STORE_PASSWORD, }, authorization::{self, OPA_TLS_MOUNT_PATH, ResolvedNifiAuthorizationConfig}, - build_tls_volume, check_or_generate_oidc_admin_password, check_or_generate_sensitive_key, + build_oidc_admin_password_secret, build_tls_volume, check_or_generate_sensitive_key, tls::{KEYSTORE_NIFI_CONTAINER_MOUNT, KEYSTORE_VOLUME_NAME, TRUSTSTORE_VOLUME_NAME}, }, service::{build_rolegroup_headless_service, build_rolegroup_metrics_service}, @@ -241,6 +241,11 @@ pub enum Error { cm_name: String, }, + #[snafu(display("failed to apply OIDC admin password secret"))] + ApplyOidcAdminPasswordSecret { + source: stackable_operator::cluster_resources::Error, + }, + #[snafu(display("failed to patch service account"))] ApplyServiceAccount { source: stackable_operator::cluster_resources::Error, @@ -443,10 +448,23 @@ pub async fn reconcile_nifi( ) .context(InvalidNifiAuthenticationConfigSnafu)?; - if let NifiAuthenticationConfig::Oidc { .. } = authentication_config { - check_or_generate_oidc_admin_password(client, nifi) + if let NifiAuthenticationConfig::Oidc { .. } = &authentication_config { + let oidc_admin_password_secret = build_oidc_admin_password_secret( + client, + nifi, + build_recommended_labels( + nifi, + &resolved_product_image.app_version_label_value, + "node", + "oidc", + ), + ) + .await + .context(SecuritySnafu)?; + cluster_resources + .add(client, oidc_admin_password_secret) .await - .context(SecuritySnafu)?; + .context(ApplyOidcAdminPasswordSecretSnafu)?; } let authorization_config = ResolvedNifiAuthorizationConfig::from( diff --git a/rust/operator-binary/src/security/mod.rs b/rust/operator-binary/src/security/mod.rs index 3a5c6d9e..963a9251 100644 --- a/rust/operator-binary/src/security/mod.rs +++ b/rust/operator-binary/src/security/mod.rs @@ -1,10 +1,20 @@ -use snafu::{ResultExt, Snafu}; +use std::collections::BTreeMap; + +use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ - builder::pod::volume::SecretFormat, client::Client, k8s_openapi::api::core::v1::Volume, + builder::{meta::ObjectMetaBuilder, pod::volume::SecretFormat}, + client::Client, + k8s_openapi::api::core::v1::{Secret, Volume}, + kube::ResourceExt, shared::time::Duration, }; -use crate::crd::v1alpha1; +use crate::{ + crd::v1alpha1, + security::{ + authentication::STACKABLE_ADMIN_USERNAME, oidc::build_oidc_admin_password_secret_name, + }, +}; pub mod authentication; pub mod authorization; @@ -16,14 +26,24 @@ type Result = std::result::Result; #[derive(Snafu, Debug)] pub enum Error { + #[snafu(display("the NiFi object defines no namespace"))] + ObjectHasNoNamespace, + #[snafu(display("tls failure"))] Tls { source: tls::Error }, #[snafu(display("sensistive key failure"))] SensitiveKey { source: sensitive_key::Error }, - #[snafu(display("failed to ensure OIDC admin password exists"))] - OidcAdminPassword { source: oidc::Error }, + #[snafu(display("failed to fetch or create OIDC admin password secret"))] + OidcAdminPasswordSecret { + source: stackable_operator::client::Error, + }, + + #[snafu(display("failed to build OIDC admin password secret metadata"))] + BuildOidcAdminPasswordSecretMetadata { + source: stackable_operator::builder::meta::Error, + }, } pub async fn check_or_generate_sensitive_key( @@ -35,13 +55,43 @@ pub async fn check_or_generate_sensitive_key( .context(SensitiveKeySnafu) } -pub async fn check_or_generate_oidc_admin_password( +/// Build a Secret containing the OIDC admin password. +/// +/// If the Secret object already exists and contains the expected key, the existing password is preserved. +/// Otherwise a new Secret object is created with a random password. +/// +pub async fn build_oidc_admin_password_secret( client: &Client, nifi: &v1alpha1::NifiCluster, -) -> Result { - oidc::check_or_generate_oidc_admin_password(client, nifi) + labels: stackable_operator::kvp::ObjectLabels<'_, v1alpha1::NifiCluster>, +) -> Result { + tracing::debug!("Checking for OIDC admin password configuration"); + + let namespace: &str = &nifi.namespace().context(ObjectHasNoNamespaceSnafu)?; + let kubernetes_secret_name = build_oidc_admin_password_secret_name(nifi); + + let oidc_admin_pass_secret = client + .get_opt::(&kubernetes_secret_name, namespace) .await - .context(OidcAdminPasswordSnafu) + .context(OidcAdminPasswordSecretSnafu)?; + + let password = oidc::build_oidc_admin_password_secret(oidc_admin_pass_secret); + + Ok(Secret { + metadata: ObjectMetaBuilder::new() + .name_and_namespace(nifi) + .name(build_oidc_admin_password_secret_name(nifi)) + .ownerreference_from_resource(nifi, None, Some(true)) + .context(BuildOidcAdminPasswordSecretMetadataSnafu)? + .with_recommended_labels(labels) + .context(BuildOidcAdminPasswordSecretMetadataSnafu)? + .build(), + string_data: Some(BTreeMap::from([( + STACKABLE_ADMIN_USERNAME.to_string(), + password, + )])), + ..Secret::default() + }) } pub fn build_tls_volume( diff --git a/rust/operator-binary/src/security/oidc.rs b/rust/operator-binary/src/security/oidc.rs index 0d93d942..c0274c13 100644 --- a/rust/operator-binary/src/security/oidc.rs +++ b/rust/operator-binary/src/security/oidc.rs @@ -1,14 +1,12 @@ use std::collections::BTreeMap; use rand::{RngExt, distr::Alphanumeric}; -use snafu::{OptionExt, ResultExt, Snafu}; +use snafu::{ResultExt, Snafu}; use stackable_operator::{ - builder::meta::ObjectMetaBuilder, - client::Client, commons::tls_verification::{CaCert, TlsServerVerification, TlsVerification}, crd::authentication::oidc, - k8s_openapi::api::core::v1::Secret, - kube::{ResourceExt, runtime::reflector::ObjectRef}, + k8s_openapi::{ByteString, api::core::v1::Secret}, + kube::ResourceExt, }; use crate::{crd::v1alpha1, security::authentication::STACKABLE_ADMIN_USERNAME}; @@ -17,19 +15,6 @@ 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 admin password secret {secret:?}, but the key {STACKABLE_ADMIN_USERNAME} is missing", - ))] - MissingAdminPasswordKey { secret: ObjectRef }, - #[snafu(display("invalid well-known OIDC configuration URL"))] InvalidWellKnownConfigUrl { source: stackable_operator::crd::authentication::oidc::v1alpha1::Error, @@ -39,65 +24,51 @@ pub enum Error { SkippingTlsVerificationNotSupported {}, } -/// 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: &v1alpha1::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 - .context(OidcAdminPasswordSecretSnafu)? - { +/// Returns a password to be used by the OIDC admin user. +/// If the Secret containing the password already exists and contains the expected key, the existing password is returned. +/// Otherwise a new random password is generated. +pub(crate) fn build_oidc_admin_password_secret(oidc_admin_secret: Option) -> String { + match oidc_admin_secret { Some(secret) => { - let admin_password_present = secret + let existing_password = secret .data - .iter() - .flat_map(|data| data.keys()) - .any(|key| key == STACKABLE_ADMIN_USERNAME); - - if admin_password_present { - Ok(false) - } else { - MissingAdminPasswordKeySnafu { - secret: ObjectRef::from_obj(&secret), + .as_ref() + .and_then(|data| data.get(STACKABLE_ADMIN_USERNAME)) + .map(decode_admin_password); + + match existing_password { + Some(password) => password, + None => { + tracing::info!( + expected_key = STACKABLE_ADMIN_USERNAME, + "Found existing OIDC admin password secret, but it doesn't contain the expected key, generating new password" + ); + encode_admin_password(15) } - .fail()? } } None => { - tracing::info!("No existing oidc admin password secret found, generating new one"); - let password: String = rand::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) + tracing::info!("No existing OIDC admin password secret found, generating new one"); + encode_admin_password(15) } } } -pub fn build_oidc_admin_password_secret_name(nifi: &v1alpha1::NifiCluster) -> String { +// TODO: maybe switch to get_random_base64() (not public atm) from op-rs which is ASCII clean and thus more suitable for passwords: +// https://github.com/stackabletech/operator-rs/blob/main/crates/stackable-operator/src/commons/random_secret_creation.rs#L127-L127 +fn encode_admin_password(size_bytes: usize) -> String { + rand::rng() + .sample_iter(&Alphanumeric) + .take(size_bytes) + .map(char::from) + .collect() +} + +fn decode_admin_password(encoded: &ByteString) -> String { + String::from_utf8_lossy(&encoded.0).into_owned() +} + +pub(crate) fn build_oidc_admin_password_secret_name(nifi: &v1alpha1::NifiCluster) -> String { format!("{}-oidc-admin-password", nifi.name_any()) }