Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions rust/operator-binary/src/controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
68 changes: 59 additions & 9 deletions rust/operator-binary/src/security/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -16,14 +26,24 @@ type Result<T, E = Error> = std::result::Result<T, E>;

#[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(
Expand All @@ -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<bool> {
oidc::check_or_generate_oidc_admin_password(client, nifi)
labels: stackable_operator::kvp::ObjectLabels<'_, v1alpha1::NifiCluster>,
) -> Result<stackable_operator::k8s_openapi::api::core::v1::Secret> {
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::<Secret>(&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(
Expand Down
105 changes: 38 additions & 67 deletions rust/operator-binary/src/security/oidc.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -17,19 +15,6 @@ type Result<T, E = Error> = std::result::Result<T, E>;

#[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<Secret> },

#[snafu(display("invalid well-known OIDC configuration URL"))]
InvalidWellKnownConfigUrl {
source: stackable_operator::crd::authentication::oidc::v1alpha1::Error,
Expand All @@ -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<bool, Error> {
let namespace: &str = &nifi.namespace().context(ObjectHasNoNamespaceSnafu)?;
tracing::debug!("Checking for OIDC admin password configuration");
match client
.get_opt::<Secret>(&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<Secret>) -> 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())
}

Expand Down
Loading