From 88944a6925fb5b8fa722fdf53da9f447790cefb6 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Fri, 28 Mar 2025 10:18:57 +0100 Subject: [PATCH 01/31] add support for authorization with OPA --- rust/operator-binary/src/controller.rs | 49 ++++-- rust/operator-binary/src/crd/mod.rs | 25 ++- .../operator-binary/src/reporting_task/mod.rs | 4 +- .../src/security/authentication.rs | 61 +------ .../src/security/authorization.rs | 155 ++++++++++++++++ rust/operator-binary/src/security/mod.rs | 1 + tests/templates/kuttl/opa/00-assert.yaml.j2 | 10 ++ ...tor-aggregator-discovery-configmap.yaml.j2 | 9 + tests/templates/kuttl/opa/00-patch-ns.yaml.j2 | 9 + tests/templates/kuttl/opa/10-assert.yaml | 12 ++ .../templates/kuttl/opa/10-install-zk.yaml.j2 | 29 +++ tests/templates/kuttl/opa/15-auth_class.yaml | 17 ++ tests/templates/kuttl/opa/19-assert.yaml | 12 ++ .../kuttl/opa/19-install-keycloak.yaml.j2 | 165 ++++++++++++++++++ .../kuttl/opa/19-keycloak-realm-cm.yaml | 78 +++++++++ tests/templates/kuttl/opa/20-assert.yaml | 6 + .../kuttl/opa/20-install-opa.yaml.j2 | 43 +++++ tests/templates/kuttl/opa/25-opa-rego.yaml | 34 ++++ tests/templates/kuttl/opa/30-assert.yaml | 12 ++ .../templates/kuttl/opa/30-install-nifi.yaml | 5 + tests/templates/kuttl/opa/30_nifi.yaml.j2 | 47 +++++ .../kuttl/opa/40-create-configmap.yaml.j2 | 4 + tests/templates/kuttl/opa/41-assert.yaml | 14 ++ .../opa/41-install-test-container.yaml.j2 | 75 ++++++++ tests/templates/kuttl/opa/45-assert.yaml | 8 + tests/templates/kuttl/opa/test_nifi.py | 77 ++++++++ tests/test-definition.yaml | 15 ++ 27 files changed, 901 insertions(+), 75 deletions(-) create mode 100644 rust/operator-binary/src/security/authorization.rs create mode 100644 tests/templates/kuttl/opa/00-assert.yaml.j2 create mode 100644 tests/templates/kuttl/opa/00-install-vector-aggregator-discovery-configmap.yaml.j2 create mode 100644 tests/templates/kuttl/opa/00-patch-ns.yaml.j2 create mode 100644 tests/templates/kuttl/opa/10-assert.yaml create mode 100644 tests/templates/kuttl/opa/10-install-zk.yaml.j2 create mode 100644 tests/templates/kuttl/opa/15-auth_class.yaml create mode 100644 tests/templates/kuttl/opa/19-assert.yaml create mode 100644 tests/templates/kuttl/opa/19-install-keycloak.yaml.j2 create mode 100644 tests/templates/kuttl/opa/19-keycloak-realm-cm.yaml create mode 100644 tests/templates/kuttl/opa/20-assert.yaml create mode 100644 tests/templates/kuttl/opa/20-install-opa.yaml.j2 create mode 100644 tests/templates/kuttl/opa/25-opa-rego.yaml create mode 100644 tests/templates/kuttl/opa/30-assert.yaml create mode 100644 tests/templates/kuttl/opa/30-install-nifi.yaml create mode 100644 tests/templates/kuttl/opa/30_nifi.yaml.j2 create mode 100644 tests/templates/kuttl/opa/40-create-configmap.yaml.j2 create mode 100644 tests/templates/kuttl/opa/41-assert.yaml create mode 100644 tests/templates/kuttl/opa/41-install-test-container.yaml.j2 create mode 100644 tests/templates/kuttl/opa/45-assert.yaml create mode 100755 tests/templates/kuttl/opa/test_nifi.py diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 648feb1c..06c43621 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -95,6 +95,7 @@ use crate::{ LOGIN_IDENTITY_PROVIDERS_XML_FILE_NAME, STACKABLE_SERVER_TLS_DIR, STACKABLE_TLS_STORE_PASSWORD, }, + authorization::NifiAuthorizationConfig, 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}, }, @@ -295,6 +296,11 @@ pub enum Error { source: crate::security::authentication::Error, }, + #[snafu(display("Invalid NiFi Authorization Configuration"))] + InvalidNifiAuthorizationConfig { + source: crate::security::authorization::Error, + }, + #[snafu(display("Failed to resolve NiFi Authentication Configuration"))] FailedResolveNifiAuthenticationConfig { source: crate::crd::authentication::Error, @@ -496,19 +502,22 @@ pub async fn reconcile_nifi( obj_ref: ObjectRef::new(&nifi.name_any()).within(namespace), })?; - let nifi_authentication_config = NifiAuthenticationConfig::try_from( + let authentication_config = NifiAuthenticationConfig::try_from( AuthenticationClassResolved::from(nifi, client) .await .context(FailedResolveNifiAuthenticationConfigSnafu)?, ) .context(InvalidNifiAuthenticationConfigSnafu)?; - if let NifiAuthenticationConfig::Oidc { .. } = nifi_authentication_config { + if let NifiAuthenticationConfig::Oidc { .. } = authentication_config { check_or_generate_oidc_admin_password(client, nifi) .await .context(SecuritySnafu)?; } + let authorization_config = + NifiAuthorizationConfig::from(&nifi.spec.cluster_config.authorization); + let vector_aggregator_address = resolve_vector_aggregator_address(nifi, client) .await .context(ResolveVectorAggregatorAddressSnafu)?; @@ -561,7 +570,8 @@ pub async fn reconcile_nifi( let rg_configmap = build_node_rolegroup_config_map( nifi, &resolved_product_image, - &nifi_authentication_config, + &authentication_config, + &authorization_config, role, &rolegroup, rolegroup_config, @@ -579,7 +589,8 @@ pub async fn reconcile_nifi( role, rolegroup_config, &merged_config, - &nifi_authentication_config, + &authentication_config, + &authorization_config, &version_change, &rbac_sa.name_any(), ) @@ -628,7 +639,7 @@ pub async fn reconcile_nifi( nifi, &resolved_product_image, &client.kubernetes_cluster_info, - &nifi_authentication_config, + &authentication_config, &rbac_sa.name_any(), ) .context(ReportingTaskSnafu)? @@ -736,7 +747,8 @@ pub fn build_node_role_service( async fn build_node_rolegroup_config_map( nifi: &v1alpha1::NifiCluster, resolved_product_image: &ResolvedProductImage, - nifi_auth_config: &NifiAuthenticationConfig, + authentication_config: &NifiAuthenticationConfig, + authorization_config: &NifiAuthorizationConfig, role: &Role, rolegroup: &RoleGroupRef, rolegroup_config: &HashMap>, @@ -746,10 +758,14 @@ async fn build_node_rolegroup_config_map( ) -> Result { tracing::debug!("building rolegroup configmaps"); - let (login_identity_provider_xml, authorizers_xml) = nifi_auth_config - .get_auth_config() + let login_identity_provider_xml = authentication_config + .get_authentication_config() .context(InvalidNifiAuthenticationConfigSnafu)?; + let authorizers_xml = authorization_config + .get_authorizers_config(authentication_config) + .context(InvalidNifiAuthorizationConfigSnafu)?; + let jvm_sec_props: BTreeMap> = rolegroup_config .get(&PropertyNameKind::File( JVM_SECURITY_PROPERTIES_FILE.to_string(), @@ -799,7 +815,7 @@ async fn build_node_rolegroup_config_map( &nifi.spec, &merged_config.resources, proxy_hosts, - nifi_auth_config, + authentication_config, rolegroup_config .get(&PropertyNameKind::File(NIFI_PROPERTIES.to_string())) .with_context(|| ProductConfigKindNotSpecifiedSnafu { @@ -912,7 +928,8 @@ async fn build_node_rolegroup_statefulset( role: &Role, rolegroup_config: &HashMap>, merged_config: &NifiConfig, - nifi_auth_config: &NifiAuthenticationConfig, + authentication_config: &NifiAuthenticationConfig, + authorization_config: &NifiAuthorizationConfig, version_change_state: &VersionChangeState, sa_name: &str, ) -> Result { @@ -963,12 +980,14 @@ async fn build_node_rolegroup_statefulset( &nifi.spec.cluster_config.zookeeper_config_map_name, )); - if let NifiAuthenticationConfig::Oidc { oidc, .. } = nifi_auth_config { + if let NifiAuthenticationConfig::Oidc { oidc, .. } = authentication_config { env_vars.extend(AuthenticationProvider::client_credentials_env_var_mounts( oidc.client_credentials_secret_ref.clone(), )); } + env_vars.extend(authorization_config.get_env_vars()); + let node_address = format!( "$POD_NAME.{}-node-{}.{}.svc.{}", rolegroup_ref.cluster.name, @@ -1014,7 +1033,11 @@ async fn build_node_rolegroup_statefulset( ]); // 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_from_slice( + authentication_config + .get_additional_container_args() + .as_slice(), + ); prepare_args.extend(vec![ "echo Templating config files".to_string(), @@ -1255,7 +1278,7 @@ async fn build_node_rolegroup_statefulset( ); } - nifi_auth_config + authentication_config .add_volumes_and_mounts( &mut pod_builder, vec![&mut container_prepare, container_nifi], diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 983189a6..75c60afb 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -11,7 +11,9 @@ use stackable_operator::{ commons::{ affinity::StackableAffinity, authentication::ClientAuthenticationDetails, + cache::UserInformationCache, cluster_operation::ClusterOperation, + opa::OpaConfig, product_image_selection::ProductImage, resources::{ CpuLimitsFragment, MemoryLimitsFragment, NoRuntimeLimits, NoRuntimeLimitsFragment, @@ -115,10 +117,15 @@ pub mod versioned { #[serde(rename_all = "camelCase")] pub struct NifiClusterConfig { /// Authentication options for NiFi (required). - /// Read more about authentication in the [security documentation](DOCS_BASE_URL_PLACEHOLDER/nifi/usage_guide/security). + /// Read more about authentication in the [security documentation](DOCS_BASE_URL_PLACEHOLDER/nifi/usage_guide/security#_authentication). // We don't add `#[serde(default)]` here, as we require authentication pub authentication: Vec, + /// Authorization options. + /// Learn more in the [NiFi authorization usage guide](DOCS_BASE_URL_PLACEHOLDER/nifi/usage-guide/security#authorization). + #[serde(skip_serializing_if = "Option::is_none")] + pub authorization: Option, + /// Configuration of allowed proxies e.g. load balancers or Kubernetes Ingress. Using a proxy that is not allowed by NiFi results /// in a failed host header check. #[serde(default)] @@ -272,6 +279,22 @@ impl v1alpha1::NifiCluster { } } +#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NifiAuthorization { + #[serde(skip_serializing_if = "Option::is_none")] + pub opa: Option, +} + +#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NifiOpaConfig { + #[serde(flatten)] + pub opa: OpaConfig, + #[serde(default)] + pub cache: UserInformationCache, +} + #[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct HostHeaderCheckConfig { diff --git a/rust/operator-binary/src/reporting_task/mod.rs b/rust/operator-binary/src/reporting_task/mod.rs index 35ab4d03..b8b4e454 100644 --- a/rust/operator-binary/src/reporting_task/mod.rs +++ b/rust/operator-binary/src/reporting_task/mod.rs @@ -130,7 +130,7 @@ pub fn build_maybe_reporting_task( nifi: &v1alpha1::NifiCluster, resolved_product_image: &ResolvedProductImage, cluster_info: &KubernetesClusterInfo, - nifi_auth_config: &NifiAuthenticationConfig, + authentication_config: &NifiAuthenticationConfig, sa_name: &str, ) -> Result> { if resolved_product_image.product_version.starts_with("1.") { @@ -139,7 +139,7 @@ pub fn build_maybe_reporting_task( nifi, resolved_product_image, cluster_info, - nifi_auth_config, + authentication_config, sa_name, )?, build_reporting_task_service(nifi, resolved_product_image)?, diff --git a/rust/operator-binary/src/security/authentication.rs b/rust/operator-binary/src/security/authentication.rs index 09de46ed..accc5f9c 100644 --- a/rust/operator-binary/src/security/authentication.rs +++ b/rust/operator-binary/src/security/authentication.rs @@ -78,17 +78,12 @@ pub enum NifiAuthenticationConfig { } impl NifiAuthenticationConfig { - pub fn get_auth_config(&self) -> Result<(String, String), Error> { + pub fn get_authentication_config(&self) -> Result { let mut login_identity_provider_xml = indoc! {r#" "#} .to_string(); - let mut authorizers_xml = indoc! {r#" - - - "#} - .to_string(); match &self { Self::SingleUser { .. } | Self::Oidc { .. } => { @@ -101,28 +96,17 @@ impl NifiAuthenticationConfig { "#, }); - - authorizers_xml.push_str(indoc! {r#" - - authorizer - org.apache.nifi.authorization.single.user.SingleUserAuthorizer - - "#}); } 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(indoc! {r#" "#}); - authorizers_xml.push_str(indoc! {r#" - - "#}); - Ok((login_identity_provider_xml, authorizers_xml)) + Ok(login_identity_provider_xml) } pub fn get_user_and_password_file_paths(&self) -> (String, String) { @@ -341,44 +325,3 @@ fn get_ldap_login_identity_provider(ldap: &ldap::AuthenticationProvider) -> Resu 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 - - "#}) -} diff --git a/rust/operator-binary/src/security/authorization.rs b/rust/operator-binary/src/security/authorization.rs new file mode 100644 index 00000000..f144cc9d --- /dev/null +++ b/rust/operator-binary/src/security/authorization.rs @@ -0,0 +1,155 @@ +use crate::crd::NifiAuthorization; +use indoc::{formatdoc, indoc}; +use snafu::{OptionExt, Snafu}; +use stackable_operator::{ + commons::authentication::ldap, + k8s_openapi::api::core::v1::{ConfigMapKeySelector, EnvVar, EnvVarSource}, +}; + +use super::authentication::NifiAuthenticationConfig; + +#[derive(Snafu, Debug)] +pub enum Error { + #[snafu(display( + "The LDAP AuthenticationClass is missing the bind credentials. Currently the NiFi operator only supports connecting to LDAP servers using bind credentials" + ))] + LdapAuthenticationClassMissingBindCredentials {}, +} + +pub enum NifiAuthorizationConfig { + Opa { + configmap_name: String, + cache_entry_time_to_live_secs: u64, + cache_max_entries: u32, + }, + Default, +} + +impl NifiAuthorizationConfig { + pub fn from(nifi_authorization: &Option) -> Self { + match nifi_authorization { + Some(authorization_config) => match authorization_config.opa.clone() { + Some(opa_config) => NifiAuthorizationConfig::Opa { + configmap_name: opa_config.opa.config_map_name, + cache_entry_time_to_live_secs: opa_config.cache.entry_time_to_live.as_secs(), + cache_max_entries: opa_config.cache.max_entries, + }, + None => NifiAuthorizationConfig::Default, + }, + None => NifiAuthorizationConfig::Default, + } + } + + pub fn get_authorizers_config( + &self, + authentication_config: &NifiAuthenticationConfig, + ) -> Result { + let mut authorizers_xml = indoc! {r#" + + + "#} + .to_string(); + + match self { + NifiAuthorizationConfig::Opa { + cache_entry_time_to_live_secs, + cache_max_entries, + .. + } => { + authorizers_xml.push_str(&formatdoc! {r#" + + authorizer + org.nifiopa.nifiopa.OpaAuthorizer + {cache_entry_time_to_live_secs} + {cache_max_entries} + ${{env:OPA_BASE_URL}} + nifi/allow + + "#}); + } + NifiAuthorizationConfig::Default => match authentication_config { + NifiAuthenticationConfig::SingleUser { .. } + | NifiAuthenticationConfig::Oidc { .. } => { + authorizers_xml.push_str(indoc! {r#" + + authorizer + org.apache.nifi.authorization.single.user.SingleUserAuthorizer + + "#}); + } + NifiAuthenticationConfig::Ldap { provider } => { + authorizers_xml.push_str(&self.get_default_ldap_authorizer(provider)?); + } + }, + } + + authorizers_xml.push_str(indoc! {r#" + + "#}); + return Ok(authorizers_xml); + } + + fn get_default_ldap_authorizer( + &self, + 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 + + "#}) + } + + pub fn get_env_vars(&self) -> Vec { + match self { + NifiAuthorizationConfig::Opa { configmap_name, .. } => { + vec![EnvVar { + name: "OPA_BASE_URL".to_owned(), + value_from: Some(EnvVarSource { + config_map_key_ref: Some(ConfigMapKeySelector { + key: "OPA".to_owned(), + name: configmap_name.to_owned(), + ..Default::default() + }), + ..Default::default() + }), + ..Default::default() + }] + } + NifiAuthorizationConfig::Default => vec![], + } + } +} diff --git a/rust/operator-binary/src/security/mod.rs b/rust/operator-binary/src/security/mod.rs index 18f892da..eebc0a30 100644 --- a/rust/operator-binary/src/security/mod.rs +++ b/rust/operator-binary/src/security/mod.rs @@ -7,6 +7,7 @@ use stackable_operator::{ use crate::crd::v1alpha1; pub mod authentication; +pub mod authorization; pub mod oidc; pub mod sensitive_key; pub mod tls; diff --git a/tests/templates/kuttl/opa/00-assert.yaml.j2 b/tests/templates/kuttl/opa/00-assert.yaml.j2 new file mode 100644 index 00000000..50b1d4c3 --- /dev/null +++ b/tests/templates/kuttl/opa/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/opa/00-install-vector-aggregator-discovery-configmap.yaml.j2 b/tests/templates/kuttl/opa/00-install-vector-aggregator-discovery-configmap.yaml.j2 new file mode 100644 index 00000000..2d6a0df5 --- /dev/null +++ b/tests/templates/kuttl/opa/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/opa/00-patch-ns.yaml.j2 b/tests/templates/kuttl/opa/00-patch-ns.yaml.j2 new file mode 100644 index 00000000..67185acf --- /dev/null +++ b/tests/templates/kuttl/opa/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/opa/10-assert.yaml b/tests/templates/kuttl/opa/10-assert.yaml new file mode 100644 index 00000000..e0766c49 --- /dev/null +++ b/tests/templates/kuttl/opa/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/opa/10-install-zk.yaml.j2 b/tests/templates/kuttl/opa/10-install-zk.yaml.j2 new file mode 100644 index 00000000..3435e4d8 --- /dev/null +++ b/tests/templates/kuttl/opa/10-install-zk.yaml.j2 @@ -0,0 +1,29 @@ +--- +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: + gracefulShutdownTimeout: 1m + logging: + enableVectorAgent: {{ lookup('env', 'VECTOR_AGGREGATOR') | length > 0 }} + roleGroups: + default: + replicas: 1 +--- +apiVersion: zookeeper.stackable.tech/v1alpha1 +kind: ZookeeperZnode +metadata: + name: nifi-znode +spec: + clusterRef: + name: test-zk diff --git a/tests/templates/kuttl/opa/15-auth_class.yaml b/tests/templates/kuttl/opa/15-auth_class.yaml new file mode 100644 index 00000000..d55ce903 --- /dev/null +++ b/tests/templates/kuttl/opa/15-auth_class.yaml @@ -0,0 +1,17 @@ +--- +apiVersion: authentication.stackable.tech/v1alpha1 +kind: AuthenticationClass +metadata: + name: simple-nifi-users +spec: + provider: + static: + userCredentialsSecret: + name: simple-nifi-admin-credentials +--- +apiVersion: v1 +kind: Secret +metadata: + name: simple-nifi-admin-credentials +stringData: + admin: supersecretpassword diff --git a/tests/templates/kuttl/opa/19-assert.yaml b/tests/templates/kuttl/opa/19-assert.yaml new file mode 100644 index 00000000..943a1340 --- /dev/null +++ b/tests/templates/kuttl/opa/19-assert.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 300 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: keycloak +status: + readyReplicas: 1 + replicas: 1 diff --git a/tests/templates/kuttl/opa/19-install-keycloak.yaml.j2 b/tests/templates/kuttl/opa/19-install-keycloak.yaml.j2 new file mode 100644 index 00000000..e567efe1 --- /dev/null +++ b/tests/templates/kuttl/opa/19-install-keycloak.yaml.j2 @@ -0,0 +1,165 @@ +--- +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: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: | + kubectl apply -n $NAMESPACE -f - << EOF + --- + apiVersion: secrets.stackable.tech/v1alpha1 + kind: SecretClass + metadata: + name: keycloak-tls-$NAMESPACE + spec: + backend: + autoTls: + ca: + autoGenerate: true + secret: + name: keycloak-tls-ca-$NAMESPACE + namespace: $NAMESPACE + --- + apiVersion: apps/v1 + kind: Deployment + metadata: + name: keycloak + labels: + app: keycloak + spec: + replicas: 1 + selector: + matchLabels: + app: keycloak + template: + metadata: + labels: + app: keycloak + spec: + serviceAccountName: keycloak + containers: + - name: keycloak + image: quay.io/keycloak/keycloak:25.0.0 + args: + - start + - --hostname-strict=false + - --https-key-store-file=/tls/keystore.p12 + - --https-key-store-password=changeit + - --import-realm + env: + - name: KEYCLOAK_ADMIN + value: admin + - name: KEYCLOAK_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: keycloak-admin-credentials + key: admin + - name: USER_INFO_FETCHER_CLIENT_ID + valueFrom: + secretKeyRef: + name: user-info-fetcher-client-credentials + key: clientId + - name: USER_INFO_FETCHER_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: user-info-fetcher-client-credentials + key: clientSecret + - name: JAVA_OPTS_KC_HEAP + value: -Xmx500M + ports: + - name: https + containerPort: 8443 + readinessProbe: + httpGet: + scheme: HTTPS + path: /realms/master + port: https + resources: + limits: + cpu: 1 + memory: 1024Mi + requests: + cpu: 500m + memory: 1024Mi + volumeMounts: + - name: data + mountPath: /opt/keycloak/data/ + - name: tls + mountPath: /tls/ + - name: realm-volume + mountPath: /opt/keycloak/data/import + securityContext: + fsGroup: 1000 + runAsGroup: 1000 + runAsUser: 1000 + volumes: + - name: data + emptyDir: {} + - name: tls + ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/class: keycloak-tls-$NAMESPACE + secrets.stackable.tech/format: tls-pkcs12 + secrets.stackable.tech/format.compatibility.tls-pkcs12.password: changeit + secrets.stackable.tech/scope: service=keycloak,node + spec: + storageClassName: secrets.stackable.tech + accessModes: + - ReadWriteOnce + resources: + requests: + storage: "1" + - name: realm-volume + configMap: + name: keycloak-test-realm + --- + apiVersion: v1 + kind: Secret + metadata: + name: keycloak-admin-credentials + stringData: + admin: "adminadmin" + --- + apiVersion: v1 + kind: Service + metadata: + name: keycloak + labels: + app: keycloak + spec: + ports: + - name: https + port: 8443 + targetPort: 8443 + selector: + app: keycloak + EOF diff --git a/tests/templates/kuttl/opa/19-keycloak-realm-cm.yaml b/tests/templates/kuttl/opa/19-keycloak-realm-cm.yaml new file mode 100644 index 00000000..dcf5cb69 --- /dev/null +++ b/tests/templates/kuttl/opa/19-keycloak-realm-cm.yaml @@ -0,0 +1,78 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: user-info-fetcher-client-credentials +stringData: + clientId: user-info-fetcher + clientSecret: user-info-fetcher-client-secret +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: keycloak-test-realm +data: + realm.json: | + { + "realm" : "test", + "enabled" : true, + "groups" : [ { + "name" : "nifi-admin", + "path" : "/nifi-admin" + } ], + "users" : [ { + "username" : "admin", + "enabled" : true, + "emailVerified" : true, + "firstName" : "admin", + "lastName" : "admin", + "email" : "admin@example.com", + "credentials" : [ { + "type" : "password", + "userLabel" : "My password", + "secretData" : "{\"value\":\"JxIyEshkBUrhZX1BEN9JO8EM3ue5/SnGHDfuyTqOH6A=\",\"salt\":\"f6iCn2rWqZQaRnCCsKAoQQ==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "realmRoles" : [ "default-roles-my-dataspace" ], + "groups" : [ "/nifi-admin" ] + }, { + "username" : "service-account-user-info-fetcher", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "serviceAccountClientId" : "user-info-fetcher", + "credentials" : [ ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-my-dataspace" ], + "clientRoles" : { + "realm-management" : [ + "view-users" + ] + }, + "notBefore" : 0, + "groups" : [ ] + } ], + "clients" : [ { + "clientId" : "${USER_INFO_FETCHER_CLIENT_ID}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "secret" : "${USER_INFO_FETCHER_CLIENT_SECRET}", + "redirectUris" : [ "/*" ], + "webOrigins" : [ "/*" ], + "notBefore" : 0, + "bearerOnly" : false, + "serviceAccountsEnabled" : true, + "publicClient" : false, + "frontchannelLogout" : true, + "protocol" : "openid-connect", + "attributes" : { + "oidc.ciba.grant.enabled" : "true", + "oauth2.device.authorization.grant.enabled" : "false" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true + } ] + } diff --git a/tests/templates/kuttl/opa/20-assert.yaml b/tests/templates/kuttl/opa/20-assert.yaml new file mode 100644 index 00000000..e868cdaf --- /dev/null +++ b/tests/templates/kuttl/opa/20-assert.yaml @@ -0,0 +1,6 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 300 +commands: + - script: kubectl -n $NAMESPACE rollout status daemonset opa-server-default --timeout 300s diff --git a/tests/templates/kuttl/opa/20-install-opa.yaml.j2 b/tests/templates/kuttl/opa/20-install-opa.yaml.j2 new file mode 100644 index 00000000..3df2666d --- /dev/null +++ b/tests/templates/kuttl/opa/20-install-opa.yaml.j2 @@ -0,0 +1,43 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: | + kubectl apply -n $NAMESPACE -f - < 0 }} + containers: + opa: + loggers: + decision: + level: INFO + roleGroups: + default: {} diff --git a/tests/templates/kuttl/opa/25-opa-rego.yaml b/tests/templates/kuttl/opa/25-opa-rego.yaml new file mode 100644 index 00000000..2682b1fe --- /dev/null +++ b/tests/templates/kuttl/opa/25-opa-rego.yaml @@ -0,0 +1,34 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: opa-nifi-uif-rego + labels: + opa.stackable.tech/bundle: "true" +data: + roles.rego: | + package nifi + + default allow := { + "allowed": "false", + "dumpCache": true + } + + allow := { + "allowed": "true", + "dumpCache": true + } if { + input.user.name == "CN=generated certificate for pod" + input.resource.id == "/proxy" + } + + allow := { + "allowed": "true", + "dumpCache": true + } if { + group_paths := data.stackable.opa.userinfo.v1.userInfoByUsername(input.user.name).groups + roles := [ trim(group,"/") | group := group_paths[_] ] + some i + roles[i] == "nifi-admin" + } + diff --git a/tests/templates/kuttl/opa/30-assert.yaml b/tests/templates/kuttl/opa/30-assert.yaml new file mode 100644 index 00000000..03958264 --- /dev/null +++ b/tests/templates/kuttl/opa/30-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/opa/30-install-nifi.yaml b/tests/templates/kuttl/opa/30-install-nifi.yaml new file mode 100644 index 00000000..e7126be2 --- /dev/null +++ b/tests/templates/kuttl/opa/30-install-nifi.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: envsubst < 30_nifi.yaml | kubectl apply -n $NAMESPACE -f - diff --git a/tests/templates/kuttl/opa/30_nifi.yaml.j2 b/tests/templates/kuttl/opa/30_nifi.yaml.j2 new file mode 100644 index 00000000..506fe565 --- /dev/null +++ b/tests/templates/kuttl/opa/30_nifi.yaml.j2 @@ -0,0 +1,47 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: nifi-sensitive-property-key +stringData: + nifiSensitivePropsKey: mYsUp3rS3cr3tk3y +--- +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: simple-nifi-users + authorization: + opa: + configMapName: opa + package: nifi + cache: + entryTimeToLive: 5s + maxEntries: 10 + sensitiveProperties: + keySecret: nifi-sensitive-property-key +{% if lookup('env', 'VECTOR_AGGREGATOR') %} + vectorAggregatorConfigMapName: vector-aggregator-discovery +{% endif %} + zookeeperConfigMapName: nifi-znode + listenerClass: external-unstable + nodes: + config: + logging: + enableVectorAgent: {{ lookup('env', 'VECTOR_AGGREGATOR') | length > 0 }} + gracefulShutdownTimeout: 1s # let the tests run faster + roleGroups: + default: + config: {} + replicas: 2 diff --git a/tests/templates/kuttl/opa/40-create-configmap.yaml.j2 b/tests/templates/kuttl/opa/40-create-configmap.yaml.j2 new file mode 100644 index 00000000..642c5468 --- /dev/null +++ b/tests/templates/kuttl/opa/40-create-configmap.yaml.j2 @@ -0,0 +1,4 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: kubectl create cm test-nifi -n $NAMESPACE --from-file=test_nifi.py diff --git a/tests/templates/kuttl/opa/41-assert.yaml b/tests/templates/kuttl/opa/41-assert.yaml new file mode 100644 index 00000000..58987778 --- /dev/null +++ b/tests/templates/kuttl/opa/41-assert.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +metadata: + name: install-test-container +timeout: 300 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: python +status: + readyReplicas: 1 + replicas: 1 diff --git a/tests/templates/kuttl/opa/41-install-test-container.yaml.j2 b/tests/templates/kuttl/opa/41-install-test-container.yaml.j2 new file mode 100644 index 00000000..21383e9a --- /dev/null +++ b/tests/templates/kuttl/opa/41-install-test-container.yaml.j2 @@ -0,0 +1,75 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: python +--- +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: python +{% 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: python +subjects: + - kind: ServiceAccount + name: python +roleRef: + kind: Role + name: python + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +metadata: + name: install-test-container +timeout: 300 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: python + labels: + app: python +spec: + replicas: 1 + selector: + matchLabels: + app: python + template: + metadata: + labels: + app: python + spec: + serviceAccountName: python + securityContext: + fsGroup: 1000 + containers: + - name: python + image: docker.stackable.tech/stackable/testing-tools:0.2.0-stackable0.0.0-dev + stdin: true + tty: true + resources: + requests: + memory: "128Mi" + cpu: "512m" + limits: + memory: "128Mi" + cpu: "1" + volumeMounts: + - name: test-nifi + mountPath: /tmp + volumes: + - name: test-nifi + configMap: + name: test-nifi + terminationGracePeriodSeconds: 1 diff --git a/tests/templates/kuttl/opa/45-assert.yaml b/tests/templates/kuttl/opa/45-assert.yaml new file mode 100644 index 00000000..7f22a990 --- /dev/null +++ b/tests/templates/kuttl/opa/45-assert.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +metadata: + name: login +timeout: 300 +commands: + - script: kubectl exec -n $NAMESPACE python-0 -- python /tmp/test_nifi.py -u admin -p supersecretpassword -n $NAMESPACE -c 2 diff --git a/tests/templates/kuttl/opa/test_nifi.py b/tests/templates/kuttl/opa/test_nifi.py new file mode 100755 index 00000000..283b1a3b --- /dev/null +++ b/tests/templates/kuttl/opa/test_nifi.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +import requests +import json +import argparse +import urllib3 +from time import sleep + + +def get_token(nifi_host, username, password): + nifi_headers = { + 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', + } + data = {'username': username, 'password': password} + + # TODO: handle actual errors when connecting properly + nifi_url = nifi_host + '/nifi-api/access/token' + response = requests.post(nifi_url, headers=nifi_headers, data=data, verify=False) # , cert='./tmp/cacert.pem') + + if response.ok: + nifi_token = response.content.decode('utf-8') + return "Bearer " + nifi_token + else: + print(f"Failed to get token: {response.status_code}: {response.content}") + exit(-1) + + +if __name__ == '__main__': + # Construct an argument parser + all_args = argparse.ArgumentParser() + + # Add arguments to the parser + all_args.add_argument("-u", "--user", required=True, + help="Username to connect as") + all_args.add_argument("-p", "--password", required=True, + help="Password for the user") + all_args.add_argument("-n", "--namespace", required=True, + help="Namespace the test is running in") + all_args.add_argument("-c", "--count", required=True, + help="The expected number of Nodes") + args = vars(all_args.parse_args()) + + # disable warnings as we have specified non-verified https connections + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + host = f"https://test-nifi-node-default-1.test-nifi-node-default.{args['namespace']}.svc.cluster.local:8443" + token = get_token(host, args['user'], args['password']) + headers = {'Authorization': token} + node_count = int(args['count']) + + x = 0 + while x < 15: + url = host + '/nifi-api/controller/cluster' + cluster = requests.get(url, headers=headers, verify=False) # , cert='/tmp/cacert.pem') + if cluster.status_code != 200: + print("Waiting for cluster...") + else: + cluster_data = json.loads(cluster.content.decode('utf-8')) + nodes = cluster_data['cluster']['nodes'] + if len(nodes) != node_count: + print(f"Cluster should have {node_count} nodes at this stage, but has: {len(nodes)}") + else: + connected = True + for node in nodes: + if node['status'] != "CONNECTED": + print(f"Node {node['nodeId']} is in state {node['status']} but should have been CONNECTED") + connected = False + if connected: + print("Test succeeded!") + exit(0) + print("Retrying...") + x+=1 + sleep(10) + + + + print("Test failed") + exit(-1) diff --git a/tests/test-definition.yaml b/tests/test-definition.yaml index 0336c48a..407fe0d6 100644 --- a/tests/test-definition.yaml +++ b/tests/test-definition.yaml @@ -30,6 +30,9 @@ dimensions: - name: zookeeper-latest values: - 3.9.3 + - name: opa-latest + values: + - 1.0.1 - name: ldap-use-tls values: - "false" @@ -75,6 +78,12 @@ tests: - zookeeper-latest - ldap-use-tls - openshift + - name: ldap_opa_uif + dimensions: + - nifi + - zookeeper-latest + - opa-latest + - openshift - name: logging dimensions: - nifi @@ -91,6 +100,12 @@ tests: - zookeeper-latest - oidc-use-tls - openshift + - name: opa + dimensions: + - nifi + - zookeeper-latest + - opa-latest + - openshift suites: - name: nightly patch: From 142f867076cbbc6509b295c221906b4108d83ebd Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Fri, 28 Mar 2025 17:00:01 +0100 Subject: [PATCH 02/31] add docs on authorization with OPA --- .../nifi/pages/usage_guide/security.adoc | 86 ++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/docs/modules/nifi/pages/usage_guide/security.adoc b/docs/modules/nifi/pages/usage_guide/security.adoc index f45e6e56..50cd749c 100644 --- a/docs/modules/nifi/pages/usage_guide/security.adoc +++ b/docs/modules/nifi/pages/usage_guide/security.adoc @@ -1,6 +1,7 @@ = Security :description: Secure Apache NiFi on Kubernetes with TLS, authentication, and authorization using the Stackable operator. Configure LDAP, OIDC, and sensitive data encryption. :nifi-docs-authorization: https://nifi.apache.org/docs/nifi-docs/html/administration-guide.html#multi-tenant-authorization +:nifi-docs-access-policies: https://nifi.apache.org/docs/nifi-docs/html/administration-guide.html#access-policies :nifi-docs-fileusergroupprovider: https://nifi.apache.org/docs/nifi-docs/html/administration-guide.html#fileusergroupprovider :nifi-docs-fileaccesspolicyprovider: https://nifi.apache.org/docs/nifi-docs/html/administration-guide.html#fileaccesspolicyprovider :nifi-docs-sensitive-properties-key: https://nifi.apache.org/docs/nifi-docs/html/administration-guide.html#nifi_sensitive_props_key @@ -168,7 +169,7 @@ stringData: NiFi supports {nifi-docs-authorization}[multiple authorization methods], the available authorization methods depend on the chosen authentication method. -Authorization is not fully implemented by the Stackable Operator for Apache NiFi. +The Stackable Operator for Apache NiFi supports a default authorization methods for each authentication method and authorization with Open Policy Agent. [#authorization-single-user] === Single user @@ -190,6 +191,89 @@ With this authorization method, all authenticated users have administrator capab 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`. +[#authorization-opa] +=== Open Policy Agent (OPA) + +NiFi can be configured to delegate authorization decisions to an Open Policy Agent (OPA) instance. More information on the setup and configuration of OPA can be found in the xref:opa:index.adoc[OPA Operator documentation]. + +A NiFi cluster can be configured with OPA authorization by adding this section to the configuration: + +[source,yaml] +---- +spec: + clusterConfig: + authorization: + opa: + configMapName: simple-opa <1> + package: my-druid-rules <2> + cache: + entryTimeToLive: 5s <3> + maxEntries: 10 <4> +---- +<1> The name of your OPA Stacklet (`simple-opa` in this case) +<2> The RegoRule package to use for policy decisions. +The package needs to contain an `allow` rule. +This is optional and defaults to the name of the Druid Stacklet. +<3> TTL for items in the cache in NiFi. +<4> Maximum number of concurrent entries in the cache in NiFi + +=== Defining rego rules + +For a general explanation of how rules are written, please refer to the {opa-rego-docs}[OPA documentation]. For more information on NiFi's access policies please refer to the {nifi-docs-access-policies}[NiFi documentation]. + +Inside of your rego rules you have access to the content of the authorization request from NiFi as input for OPA: + +[source,json] +---- +{ + "action": { + "name": <1> + }, + "properties": { + "isAccessAttempt": , <2> + "isAnonymous": <3> + }, + "resource": { <4> + "id": , + "name": , + "safeDescription": + }, + "requestedResource": { <5> + "id": , + "name": , + "safeDescription": + }, + "user": { + "name": , <6> + "groups": String, <7> + }, + "resourceContext": , <8> + "userContext": <9> +} +---- +<1> The action taken against the resource. Possible values: `"read"` or `"write"`. +<2> Whether this is a direct access attempt of the resource or if it's being checked as part of another response. Possible values: `"true"` or `"false"` +<3> Whether the entity accessing the resource is anonymous. Possible values: `"true"` or `"false"`. +<4> The current resource being authorized on. +<5> The original resource being requested. In cases with inherited policies, this will be an ancestor resource of the current resource. For the initial request, and cases without inheritance, the requested resource will be the same as the current resource. +<6> The user accessing the resource. +<7> Comma-separated list of groups that the user accessing the resource belongs to. +<8> The event attributes to make additional access decisions for provenance events. Will be `{"": ""}` if empty. +<9> Additional context for the user to make additional access decisions. Will be `{"": ""}` if empty. + +The result of an 'allow' rego rule has to be + +[source,json] +---- +{ + "allowed": , <1> + "dumpCache": <2> +} +---- +<1> Whether the action against the resource is allowed. Possible values: `"true"` or `"false"`. +<2> Whether the complete local cache in NiFi should be invalidated. + + [#encrypting-sensitive-properties] == Encrypting sensitive properties on disk From e603d7df3214ff891762a462fa1db1c6733703d3 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Mon, 31 Mar 2025 13:54:58 +0200 Subject: [PATCH 03/31] update helm chart --- deploy/helm/nifi-operator/crds/crds.yaml | 38 +++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/deploy/helm/nifi-operator/crds/crds.yaml b/deploy/helm/nifi-operator/crds/crds.yaml index 7511f55a..f54ecd68 100644 --- a/deploy/helm/nifi-operator/crds/crds.yaml +++ b/deploy/helm/nifi-operator/crds/crds.yaml @@ -29,7 +29,7 @@ spec: description: Settings that affect all roles and role groups. The settings in the `clusterConfig` are cluster wide settings that do not need to be configurable at role or role group level. properties: authentication: - description: Authentication options for NiFi (required). Read more about authentication in the [security documentation](https://docs.stackable.tech/home/nightly/nifi/usage_guide/security). + description: Authentication options for NiFi (required). Read more about authentication in the [security documentation](https://docs.stackable.tech/home/nightly/nifi/usage_guide/security#_authentication). items: properties: authenticationClass: @@ -55,6 +55,42 @@ spec: - authenticationClass type: object type: array + authorization: + description: Authorization options. Learn more in the [NiFi authorization usage guide](https://docs.stackable.tech/home/nightly/nifi/usage-guide/security#authorization). + nullable: true + properties: + opa: + description: Configure the OPA stacklet [discovery ConfigMap](https://docs.stackable.tech/home/nightly/concepts/service_discovery) and the name of the Rego package containing your authorization rules. Consult the [OPA authorization documentation](https://docs.stackable.tech/home/nightly/concepts/opa) to learn how to deploy Rego authorization rules with OPA. + nullable: true + properties: + cache: + default: + entryTimeToLive: 30s + maxEntries: 10000 + description: Least Recently Used (LRU) cache with per-entry time-to-live (TTL) value. + properties: + entryTimeToLive: + default: 30s + description: Time to live per entry + type: string + maxEntries: + default: 10000 + description: Maximum number of entries in the cache; If this threshold is reached then the least recently used item is removed. + format: uint32 + minimum: 0.0 + type: integer + type: object + configMapName: + description: The [discovery ConfigMap](https://docs.stackable.tech/home/nightly/concepts/service_discovery) for the OPA stacklet that should be used for authorization requests. + type: string + package: + description: The name of the Rego package containing the Rego rules for the product. + nullable: true + type: string + required: + - configMapName + type: object + type: object createReportingTaskJob: default: enabled: true From 032ab37a91da0b915c2536cc5bff38cd76e8863a Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Tue, 15 Apr 2025 10:15:21 +0200 Subject: [PATCH 04/31] rename input.user to input.identity --- docs/modules/nifi/pages/usage_guide/security.adoc | 6 +++--- tests/templates/kuttl/opa/25-opa-rego.yaml | 11 +++++------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/docs/modules/nifi/pages/usage_guide/security.adoc b/docs/modules/nifi/pages/usage_guide/security.adoc index 50cd749c..13943017 100644 --- a/docs/modules/nifi/pages/usage_guide/security.adoc +++ b/docs/modules/nifi/pages/usage_guide/security.adoc @@ -243,7 +243,7 @@ Inside of your rego rules you have access to the content of the authorization re "name": , "safeDescription": }, - "user": { + "identity": { "name": , <6> "groups": String, <7> }, @@ -256,8 +256,8 @@ Inside of your rego rules you have access to the content of the authorization re <3> Whether the entity accessing the resource is anonymous. Possible values: `"true"` or `"false"`. <4> The current resource being authorized on. <5> The original resource being requested. In cases with inherited policies, this will be an ancestor resource of the current resource. For the initial request, and cases without inheritance, the requested resource will be the same as the current resource. -<6> The user accessing the resource. -<7> Comma-separated list of groups that the user accessing the resource belongs to. +<6> The identity/user accessing the resource. +<7> Comma-separated list of groups that the identity/user accessing the resource belongs to. <8> The event attributes to make additional access decisions for provenance events. Will be `{"": ""}` if empty. <9> Additional context for the user to make additional access decisions. Will be `{"": ""}` if empty. diff --git a/tests/templates/kuttl/opa/25-opa-rego.yaml b/tests/templates/kuttl/opa/25-opa-rego.yaml index 2682b1fe..6760c5ac 100644 --- a/tests/templates/kuttl/opa/25-opa-rego.yaml +++ b/tests/templates/kuttl/opa/25-opa-rego.yaml @@ -10,25 +10,24 @@ data: package nifi default allow := { - "allowed": "false", + "allowed": "false", "dumpCache": true } allow := { - "allowed": "true", + "allowed": "true", "dumpCache": true } if { - input.user.name == "CN=generated certificate for pod" + input.identity.name == "CN=generated certificate for pod" input.resource.id == "/proxy" } allow := { - "allowed": "true", + "allowed": "true", "dumpCache": true } if { - group_paths := data.stackable.opa.userinfo.v1.userInfoByUsername(input.user.name).groups + group_paths := data.stackable.opa.userinfo.v1.userInfoByUsername(input.identity.name).groups roles := [ trim(group,"/") | group := group_paths[_] ] some i roles[i] == "nifi-admin" } - From 6bd48310a56205b263e09da478378766d540fe3c Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Tue, 15 Apr 2025 10:41:51 +0200 Subject: [PATCH 05/31] add opa cm watch --- rust/operator-binary/src/main.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/rust/operator-binary/src/main.rs b/rust/operator-binary/src/main.rs index e6810d8b..b7760bdd 100644 --- a/rust/operator-binary/src/main.rs +++ b/rust/operator-binary/src/main.rs @@ -180,4 +180,11 @@ fn references_config_map( }; nifi.spec.cluster_config.zookeeper_config_map_name == config_map.name_any() + || match nifi.spec.cluster_config.authorization.to_owned() { + Some(trino_authorization) => match trino_authorization.opa { + Some(opa_config) => opa_config.opa.config_map_name == config_map.name_any(), + None => false, + }, + None => false, + } } From b87430b3bf62a9167d191064696213f63ea8f3ce Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Wed, 16 Apr 2025 10:14:54 +0200 Subject: [PATCH 06/31] remove unused test --- tests/test-definition.yaml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/test-definition.yaml b/tests/test-definition.yaml index 407fe0d6..51ba2cd6 100644 --- a/tests/test-definition.yaml +++ b/tests/test-definition.yaml @@ -78,12 +78,6 @@ tests: - zookeeper-latest - ldap-use-tls - openshift - - name: ldap_opa_uif - dimensions: - - nifi - - zookeeper-latest - - opa-latest - - openshift - name: logging dimensions: - nifi From 3c9c7c1a354ba96312f929379e66bfca990496d8 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Tue, 6 May 2025 14:03:49 +0200 Subject: [PATCH 07/31] update rego with new opa response schema --- tests/templates/kuttl/opa/25-opa-rego.yaml | 27 +++++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/tests/templates/kuttl/opa/25-opa-rego.yaml b/tests/templates/kuttl/opa/25-opa-rego.yaml index 6760c5ac..61a09bab 100644 --- a/tests/templates/kuttl/opa/25-opa-rego.yaml +++ b/tests/templates/kuttl/opa/25-opa-rego.yaml @@ -6,26 +6,41 @@ metadata: labels: opa.stackable.tech/bundle: "true" data: - roles.rego: | + nifi.rego: | package nifi + # Setting "resourceNotFound" to true results in the parent resource being evaluated for authorization, e.g. the parent of a processor is the processor-group. + # If a resource is matched by a rego rule that is not the default the parent resource will be ignored. default allow := { - "allowed": "false", + "resourceNotFound": true, "dumpCache": true } allow := { - "allowed": "true", + "allowed": true, "dumpCache": true } if { - input.identity.name == "CN=generated certificate for pod" - input.resource.id == "/proxy" + input.resource.id in ["/flow", "/controller", "/parameter-contexts", "/provenance-data", "/restricted-components", "/policies", "/tenants", "/site-to-site", "/system", "counters"] + nifi_admin } allow := { - "allowed": "true", + "allowed": true, "dumpCache": true } if { + input.resource.name in ["NiFi Flow"] + nifi_admin + } + + allow := { + "allowed": true, + "dumpCache": true + } if { + input.identity.name == "CN=generated certificate for pod" + input.resource.id == "/proxy" + } + + nifi_admin := true if { group_paths := data.stackable.opa.userinfo.v1.userInfoByUsername(input.identity.name).groups roles := [ trim(group,"/") | group := group_paths[_] ] some i From 7dd6e3817a2350d12b6d225561207c97a6b842fe Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Thu, 8 May 2025 16:10:11 +0200 Subject: [PATCH 08/31] update docs --- .../nifi/pages/usage_guide/security.adoc | 67 +++++++++++++++++-- tests/templates/kuttl/opa/25-opa-rego.yaml | 3 +- 2 files changed, 63 insertions(+), 7 deletions(-) diff --git a/docs/modules/nifi/pages/usage_guide/security.adoc b/docs/modules/nifi/pages/usage_guide/security.adoc index 13943017..194bb0ea 100644 --- a/docs/modules/nifi/pages/usage_guide/security.adoc +++ b/docs/modules/nifi/pages/usage_guide/security.adoc @@ -2,9 +2,13 @@ :description: Secure Apache NiFi on Kubernetes with TLS, authentication, and authorization using the Stackable operator. Configure LDAP, OIDC, and sensitive data encryption. :nifi-docs-authorization: https://nifi.apache.org/docs/nifi-docs/html/administration-guide.html#multi-tenant-authorization :nifi-docs-access-policies: https://nifi.apache.org/docs/nifi-docs/html/administration-guide.html#access-policies +nifi-docs-component-level-access-policies: https://nifi.apache.org/docs/nifi-docs/html/administration-guide.html#component-level-access-policies +:nifi-docs-access-policy-inheritance: https://nifi.apache.org/docs/nifi-docs/html/administration-guide.html#access-policy-inheritance :nifi-docs-fileusergroupprovider: https://nifi.apache.org/docs/nifi-docs/html/administration-guide.html#fileusergroupprovider :nifi-docs-fileaccesspolicyprovider: https://nifi.apache.org/docs/nifi-docs/html/administration-guide.html#fileaccesspolicyprovider :nifi-docs-sensitive-properties-key: https://nifi.apache.org/docs/nifi-docs/html/administration-guide.html#nifi_sensitive_props_key +:nifi-opa-plugin: https://github.com/DavidGitter/nifi-opa-plugin/ +:opa-rego-docs: https://www.openpolicyagent.org/docs/latest/#rego == TLS @@ -219,8 +223,9 @@ This is optional and defaults to the name of the Druid Stacklet. === Defining rego rules -For a general explanation of how rules are written, please refer to the {opa-rego-docs}[OPA documentation]. For more information on NiFi's access policies please refer to the {nifi-docs-access-policies}[NiFi documentation]. +For a general explanation of how rules are written, please refer to the {opa-rego-docs}[OPA documentation]. Authorization with OPA is done using {nifi-opa-plugin}[NiFi OPA Plugin]. +==== OPA Inputs Inside of your rego rules you have access to the content of the authorization request from NiFi as input for OPA: [source,json] @@ -261,18 +266,68 @@ Inside of your rego rules you have access to the content of the authorization re <8> The event attributes to make additional access decisions for provenance events. Will be `{"": ""}` if empty. <9> Additional context for the user to make additional access decisions. Will be `{"": ""}` if empty. -The result of an 'allow' rego rule has to be +==== OPA Result +The OPA authorizer plugin expects relo rules to be named `allow` and to return a result following this schema: [source,json] ---- { - "allowed": , <1> - "dumpCache": <2> + "allowed": , <1> + "resourceNotFound": , <2> + "dumpCache": , <3> + "message": , <4> } ---- -<1> Whether the action against the resource is allowed. Possible values: `"true"` or `"false"`. -<2> Whether the complete local cache in NiFi should be invalidated. +<1> Whether the action against the resource is allowed. Optional, defaults to false. +<2> Whether no rule was found for the authorization request. This should only be set to true in the default rule. If set to true the value of the "allowed" field will be ignored. Optional, defaults to false. +<3> Whether the whole local cache in the OPA authorizer plugin in NiFi should be invalidated. Optional, defaults to false. +<4> An optional error message that is shown to the user when access is denied. +==== Access Policies +NiFi uses {nifi-docs-access-policies}[access policies] to manage access to system-wide resources like the user interface. + +==== Component Level Access Policies and Access Policy Inheritance + +{nifi-docs-component-level-access-policies}[Component Level Access Policies] allow managing granular access to components like process-groups and processors. Components can {nifi-docs-access-policy-inheritance}[inherite access policies] defined for parent components, e.g. a process group is the parent component for a processor component. When an authorizer returns "resourceNotFound" as result instead of an authorization decision, NiFi will send an authorization request for the parent component. Access policy inheritance can be recursive up to the root component. If "resourceNotFound" is returned for an authorization request and the component doesn't have a parent component, NiFi will deny access to the component. + +To manage access for all process groups in the NiFi instance a rule has to be defined for the root process group which is identified by the resource name "NiFi Flow" and a resource id generated at random ("/process-groups/"). + +[source,rego] +---- +default allow := { + "resourceNotFound": true +} <1> + +allow := { + "allowed": true +} if { + input.resource.name == "NiFi Flow" + startswith(input.resource.id, "/process-groups") +} <2> + +allow := { + "allowed": false +} if { + input.resource.id == "/process-groups/a10c311e-0196-1000-2856-dc0606d3c5d7" + input.identity.name == "alice" +} <3> +---- +<1> Default rule returns should `"resourceNotFound": true`. If this is not set NiFi's access policy inheritance won't work. Any values for the `allowed` field in the response will be ignored. +<2> A rule that grants all users access to the root process group and thus to all components in the NiFi instance. +<3> A rule that denies access to a specific process group for the user "alice". For this process group the default rego rule will not be applied and NiFi's component inhertiance will not be used. All child components of this process group will also be authorized based on this rule unless a more granular rule overrides it. + +==== Caching + +The OPA authorizer has a mechanism to cache requests to OPA which can be configured in the NifCluster spec (see above). To delete the whole cache add `"dumpCache": true` to the result. +[source,rego] +---- +allow := { + "allowed": false + "dumpCache": true +} if { + ... +} +---- [#encrypting-sensitive-properties] == Encrypting sensitive properties on disk diff --git a/tests/templates/kuttl/opa/25-opa-rego.yaml b/tests/templates/kuttl/opa/25-opa-rego.yaml index 61a09bab..eff75bc9 100644 --- a/tests/templates/kuttl/opa/25-opa-rego.yaml +++ b/tests/templates/kuttl/opa/25-opa-rego.yaml @@ -28,7 +28,8 @@ data: "allowed": true, "dumpCache": true } if { - input.resource.name in ["NiFi Flow"] + input.resource.name == "NiFi Flow" + startswith(input.resource.id, "/process-groups") nifi_admin } From 47f1ab572be80f9c0912df2d527cdcd5175b04e9 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Fri, 9 May 2025 11:17:07 +0200 Subject: [PATCH 09/31] improve docs --- docs/modules/nifi/pages/usage_guide/security.adoc | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/modules/nifi/pages/usage_guide/security.adoc b/docs/modules/nifi/pages/usage_guide/security.adoc index 194bb0ea..76dcea3a 100644 --- a/docs/modules/nifi/pages/usage_guide/security.adoc +++ b/docs/modules/nifi/pages/usage_guide/security.adoc @@ -221,10 +221,12 @@ This is optional and defaults to the name of the Druid Stacklet. <3> TTL for items in the cache in NiFi. <4> Maximum number of concurrent entries in the cache in NiFi +[#defining-rego-rules] === Defining rego rules For a general explanation of how rules are written, please refer to the {opa-rego-docs}[OPA documentation]. Authorization with OPA is done using {nifi-opa-plugin}[NiFi OPA Plugin]. +[#opa-inputs] ==== OPA Inputs Inside of your rego rules you have access to the content of the authorization request from NiFi as input for OPA: @@ -266,6 +268,7 @@ Inside of your rego rules you have access to the content of the authorization re <8> The event attributes to make additional access decisions for provenance events. Will be `{"": ""}` if empty. <9> Additional context for the user to make additional access decisions. Will be `{"": ""}` if empty. +[#opa-result] ==== OPA Result The OPA authorizer plugin expects relo rules to be named `allow` and to return a result following this schema: @@ -283,9 +286,11 @@ The OPA authorizer plugin expects relo rules to be named `allow` and to return a <3> Whether the whole local cache in the OPA authorizer plugin in NiFi should be invalidated. Optional, defaults to false. <4> An optional error message that is shown to the user when access is denied. +[#access-policies] ==== Access Policies NiFi uses {nifi-docs-access-policies}[access policies] to manage access to system-wide resources like the user interface. +[#-component-level-access-policies] ==== Component Level Access Policies and Access Policy Inheritance {nifi-docs-component-level-access-policies}[Component Level Access Policies] allow managing granular access to components like process-groups and processors. Components can {nifi-docs-access-policy-inheritance}[inherite access policies] defined for parent components, e.g. a process group is the parent component for a processor component. When an authorizer returns "resourceNotFound" as result instead of an authorization decision, NiFi will send an authorization request for the parent component. Access policy inheritance can be recursive up to the root component. If "resourceNotFound" is returned for an authorization request and the component doesn't have a parent component, NiFi will deny access to the component. @@ -316,9 +321,10 @@ allow := { <2> A rule that grants all users access to the root process group and thus to all components in the NiFi instance. <3> A rule that denies access to a specific process group for the user "alice". For this process group the default rego rule will not be applied and NiFi's component inhertiance will not be used. All child components of this process group will also be authorized based on this rule unless a more granular rule overrides it. +[#caching] ==== Caching -The OPA authorizer has a mechanism to cache requests to OPA which can be configured in the NifCluster spec (see above). To delete the whole cache add `"dumpCache": true` to the result. +The OPA authorizer has a mechanism to cache results from OPA which can be configured in the NifCluster spec (see above). To delete the whole cache add `"dumpCache": true` to the result. [source,rego] ---- allow := { From 150dfdd66e6b65df1b9dd47168586e12cadfbb22 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Fri, 9 May 2025 14:44:37 +0200 Subject: [PATCH 10/31] update docs --- deploy/helm/nifi-operator/crds/crds.yaml | 2 +- .../nifi/pages/usage_guide/security.adoc | 115 +++++++++++------- rust/operator-binary/src/crd/mod.rs | 2 +- 3 files changed, 72 insertions(+), 47 deletions(-) diff --git a/deploy/helm/nifi-operator/crds/crds.yaml b/deploy/helm/nifi-operator/crds/crds.yaml index f54ecd68..fec50157 100644 --- a/deploy/helm/nifi-operator/crds/crds.yaml +++ b/deploy/helm/nifi-operator/crds/crds.yaml @@ -29,7 +29,7 @@ spec: description: Settings that affect all roles and role groups. The settings in the `clusterConfig` are cluster wide settings that do not need to be configurable at role or role group level. properties: authentication: - description: Authentication options for NiFi (required). Read more about authentication in the [security documentation](https://docs.stackable.tech/home/nightly/nifi/usage_guide/security#_authentication). + description: Authentication options for NiFi (required). Read more about authentication in the [security documentation](https://docs.stackable.tech/home/nightly/nifi/usage_guide/security#authentication). items: properties: authenticationClass: diff --git a/docs/modules/nifi/pages/usage_guide/security.adoc b/docs/modules/nifi/pages/usage_guide/security.adoc index 76dcea3a..bef1e0ff 100644 --- a/docs/modules/nifi/pages/usage_guide/security.adoc +++ b/docs/modules/nifi/pages/usage_guide/security.adoc @@ -2,7 +2,7 @@ :description: Secure Apache NiFi on Kubernetes with TLS, authentication, and authorization using the Stackable operator. Configure LDAP, OIDC, and sensitive data encryption. :nifi-docs-authorization: https://nifi.apache.org/docs/nifi-docs/html/administration-guide.html#multi-tenant-authorization :nifi-docs-access-policies: https://nifi.apache.org/docs/nifi-docs/html/administration-guide.html#access-policies -nifi-docs-component-level-access-policies: https://nifi.apache.org/docs/nifi-docs/html/administration-guide.html#component-level-access-policies +:nifi-docs-component-level-access-policies: https://nifi.apache.org/docs/nifi-docs/html/administration-guide.html#component-level-access-policies :nifi-docs-access-policy-inheritance: https://nifi.apache.org/docs/nifi-docs/html/administration-guide.html#access-policy-inheritance :nifi-docs-fileusergroupprovider: https://nifi.apache.org/docs/nifi-docs/html/administration-guide.html#fileusergroupprovider :nifi-docs-fileaccesspolicyprovider: https://nifi.apache.org/docs/nifi-docs/html/administration-guide.html#fileaccesspolicyprovider @@ -209,13 +209,13 @@ spec: authorization: opa: configMapName: simple-opa <1> - package: my-druid-rules <2> + package: my-nifi-rules <2> cache: entryTimeToLive: 5s <3> maxEntries: 10 <4> ---- <1> The name of your OPA Stacklet (`simple-opa` in this case) -<2> The RegoRule package to use for policy decisions. +<2> The rego rule package to use for policy decisions. The package needs to contain an `allow` rule. This is optional and defaults to the name of the Druid Stacklet. <3> TTL for items in the cache in NiFi. @@ -224,49 +224,55 @@ This is optional and defaults to the name of the Druid Stacklet. [#defining-rego-rules] === Defining rego rules -For a general explanation of how rules are written, please refer to the {opa-rego-docs}[OPA documentation]. Authorization with OPA is done using {nifi-opa-plugin}[NiFi OPA Plugin]. +For a general explanation of how rules are written, please refer to the {opa-rego-docs}[OPA documentation]. Authorization with OPA is done using a {nifi-opa-plugin}[custom authorizer provided by a plugin for NiFi]. [#opa-inputs] ==== OPA Inputs -Inside of your rego rules you have access to the content of the authorization request from NiFi as input for OPA: - -[source,json] ----- -{ - "action": { - "name": <1> - }, - "properties": { - "isAccessAttempt": , <2> - "isAnonymous": <3> - }, - "resource": { <4> - "id": , - "name": , - "safeDescription": - }, - "requestedResource": { <5> - "id": , - "name": , - "safeDescription": - }, - "identity": { - "name": , <6> - "groups": String, <7> - }, - "resourceContext": , <8> - "userContext": <9> -} ----- -<1> The action taken against the resource. Possible values: `"read"` or `"write"`. -<2> Whether this is a direct access attempt of the resource or if it's being checked as part of another response. Possible values: `"true"` or `"false"` -<3> Whether the entity accessing the resource is anonymous. Possible values: `"true"` or `"false"`. -<4> The current resource being authorized on. -<5> The original resource being requested. In cases with inherited policies, this will be an ancestor resource of the current resource. For the initial request, and cases without inheritance, the requested resource will be the same as the current resource. -<6> The identity/user accessing the resource. -<7> Comma-separated list of groups that the identity/user accessing the resource belongs to. -<8> The event attributes to make additional access decisions for provenance events. Will be `{"": ""}` if empty. -<9> Additional context for the user to make additional access decisions. Will be `{"": ""}` if empty. +The payload sent by NiFi with each request to OPA, that is accessible within the rego rules, has the following structure: + +[cols="1,2,1"] +|=== +| Payload Field| Description| Possible Values +| action.name +| The action taken against the resource. +|`read`, `write` +| resource.id +| The unique identifier of the resource that is being authorized. +| +| resource.name +| The name of the resource that is being authorized. +| +| resource.safeDescription +| The description of the resource that is being authorized. +| +| requestedResource.id +| The unique identifier of the original resource that was requested (see <>). +| +| requestedResource.name +| The name of the original resource that is being authorized on (see <>). +| +| requestedResource.safeDescription +| The description of the resource that is being authorized on (see <>). +| +| identity.name +| The name of the identity/user accessing the resource. +| +| identity.groups +| Comma-separated list of groups that the identity/user accessing the resource belongs to. +| +| properties.isAccessAttempt +| Whether this is a direct access attempt of the resource or if it's being checked as part of another response. +| `true`, `false` (String) +| isAnonymous +| Whether the entity accessing the resource is anonymous. +| `true`, `false` (String) +| resourceContext +| Object containing the event attributes to make additional access decisions for provenance events. +| ```{"": ""}``` if empty +| userContext +| Additional context for the user to make additional access decisions. +| ```{"": ""}``` if empty +|=== [#opa-result] ==== OPA Result @@ -290,10 +296,14 @@ The OPA authorizer plugin expects relo rules to be named `allow` and to return a ==== Access Policies NiFi uses {nifi-docs-access-policies}[access policies] to manage access to system-wide resources like the user interface. -[#-component-level-access-policies] +[#component-level-access-policies] ==== Component Level Access Policies and Access Policy Inheritance -{nifi-docs-component-level-access-policies}[Component Level Access Policies] allow managing granular access to components like process-groups and processors. Components can {nifi-docs-access-policy-inheritance}[inherite access policies] defined for parent components, e.g. a process group is the parent component for a processor component. When an authorizer returns "resourceNotFound" as result instead of an authorization decision, NiFi will send an authorization request for the parent component. Access policy inheritance can be recursive up to the root component. If "resourceNotFound" is returned for an authorization request and the component doesn't have a parent component, NiFi will deny access to the component. +{nifi-docs-component-level-access-policies}[Component Level Access Policies] allow managing granular access to components like process-groups and processors. Components can {nifi-docs-access-policy-inheritance}[inherite access policies] defined for parent components, e.g. a process group is the parent component for a processor component. + +The payload field `requestedResource` contains the id, name and description of the original resource that was requested. In cases with inherited policies, this will be an ancestor resource of the current resource. For the initial request, and cases without inheritance, the requested resource will be the same as the current resource. + +When an authorizer returns "resourceNotFound" as result instead of an authorization decision, NiFi will send an authorization request for the parent component. Access policy inheritance can be recursive up to the root component. If "resourceNotFound" is returned for an authorization request and the component doesn't have a parent component, NiFi will deny access to the component. To manage access for all process groups in the NiFi instance a rule has to be defined for the root process group which is identified by the resource name "NiFi Flow" and a resource id generated at random ("/process-groups/"). @@ -321,6 +331,21 @@ allow := { <2> A rule that grants all users access to the root process group and thus to all components in the NiFi instance. <3> A rule that denies access to a specific process group for the user "alice". For this process group the default rego rule will not be applied and NiFi's component inhertiance will not be used. All child components of this process group will also be authorized based on this rule unless a more granular rule overrides it. +[#communication-between-nifi-nodes] +==== Communication between NiFi nodes +To allow communication between NiFi nodes an additional rego rule is required: +[source,rego] +---- +allow := { + "allowed": true +} if { + input.identity.name == "CN=generated certificate for pod" <1> + input.resource.id == "/proxy" <2> +} +---- +<1> The identity of NiFi nodes authenticated with TLS certificates provided by the secrets operator. +<2> Only access to the `/proxy` API is required. + [#caching] ==== Caching diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 53f690c3..56ec1ced 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -117,7 +117,7 @@ pub mod versioned { #[serde(rename_all = "camelCase")] pub struct NifiClusterConfig { /// Authentication options for NiFi (required). - /// Read more about authentication in the [security documentation](DOCS_BASE_URL_PLACEHOLDER/nifi/usage_guide/security#_authentication). + /// Read more about authentication in the [security documentation](DOCS_BASE_URL_PLACEHOLDER/nifi/usage_guide/security#authentication). // We don't add `#[serde(default)]` here, as we require authentication pub authentication: Vec, From 031820b949e079944e6ca3c7ba0c463273f2fc42 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Fri, 9 May 2025 16:23:56 +0200 Subject: [PATCH 11/31] fix docs --- 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 bef1e0ff..3bee67cb 100644 --- a/docs/modules/nifi/pages/usage_guide/security.adoc +++ b/docs/modules/nifi/pages/usage_guide/security.adoc @@ -217,7 +217,7 @@ spec: <1> The name of your OPA Stacklet (`simple-opa` in this case) <2> The rego rule package to use for policy decisions. The package needs to contain an `allow` rule. -This is optional and defaults to the name of the Druid Stacklet. +This is optional and defaults to the name of the NiFi Stacklet. <3> TTL for items in the cache in NiFi. <4> Maximum number of concurrent entries in the cache in NiFi From 13e60bc53138f83d6c6f120e5ef4c86050ea5349 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Fri, 9 May 2025 16:27:29 +0200 Subject: [PATCH 12/31] refactor references_config_map function --- rust/operator-binary/src/main.rs | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/rust/operator-binary/src/main.rs b/rust/operator-binary/src/main.rs index b7760bdd..938ff62c 100644 --- a/rust/operator-binary/src/main.rs +++ b/rust/operator-binary/src/main.rs @@ -179,12 +179,22 @@ fn references_config_map( return false; }; - nifi.spec.cluster_config.zookeeper_config_map_name == config_map.name_any() - || match nifi.spec.cluster_config.authorization.to_owned() { - Some(trino_authorization) => match trino_authorization.opa { - Some(opa_config) => opa_config.opa.config_map_name == config_map.name_any(), - None => false, - }, - None => false, - } + let references_zookeeper_config_map = + nifi.spec.cluster_config.zookeeper_config_map_name == config_map.name_any(); + let references_authorization_config_map = references_authorization_config_map(nifi, config_map); + + references_zookeeper_config_map || references_authorization_config_map +} + +fn references_authorization_config_map( + nifi: &v1alpha1::NifiCluster, + config_map: &DeserializeGuard, +) -> bool { + nifi.spec + .cluster_config + .authorization + .as_ref() + .and_then(|authz| authz.opa.as_ref()) + .map(|opa_config| opa_config.opa.config_map_name == config_map.name_any()) + .unwrap_or(false) } From cfa67833882dc110655b08699e9e0bdc7a3207ec Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Fri, 9 May 2025 16:33:01 +0200 Subject: [PATCH 13/31] fix docs --- .../nifi/pages/usage_guide/security.adoc | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/modules/nifi/pages/usage_guide/security.adoc b/docs/modules/nifi/pages/usage_guide/security.adoc index 3bee67cb..0c4e6c9e 100644 --- a/docs/modules/nifi/pages/usage_guide/security.adoc +++ b/docs/modules/nifi/pages/usage_guide/security.adoc @@ -208,11 +208,11 @@ spec: clusterConfig: authorization: opa: - configMapName: simple-opa <1> - package: my-nifi-rules <2> + configMapName: simple-opa # <1> + package: my-nifi-rules # <2> cache: - entryTimeToLive: 5s <3> - maxEntries: 10 <4> + entryTimeToLive: 5s # <3> + maxEntries: 10 # <4> ---- <1> The name of your OPA Stacklet (`simple-opa` in this case) <2> The rego rule package to use for policy decisions. @@ -281,10 +281,10 @@ The OPA authorizer plugin expects relo rules to be named `allow` and to return a [source,json] ---- { - "allowed": , <1> - "resourceNotFound": , <2> - "dumpCache": , <3> - "message": , <4> + "allowed": , # <1> + "resourceNotFound": , # <2> + "dumpCache": , # <3> + "message": , # <4> } ---- <1> Whether the action against the resource is allowed. Optional, defaults to false. @@ -339,8 +339,8 @@ To allow communication between NiFi nodes an additional rego rule is required: allow := { "allowed": true } if { - input.identity.name == "CN=generated certificate for pod" <1> - input.resource.id == "/proxy" <2> + input.identity.name == "CN=generated certificate for pod" # <1> + input.resource.id == "/proxy" # <2> } ---- <1> The identity of NiFi nodes authenticated with TLS certificates provided by the secrets operator. From f9bcd442d53c210ab9b54afc65699b4013449e77 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Wed, 14 May 2025 18:30:22 +0200 Subject: [PATCH 14/31] wip: add integration test --- .../kuttl/{opa => oidc-opa}/00-assert.yaml.j2 | 0 ...tor-aggregator-discovery-configmap.yaml.j2 | 0 .../{opa => oidc-opa}/00-patch-ns.yaml.j2 | 0 .../kuttl/{opa => oidc-opa}/10-assert.yaml | 0 .../{opa => oidc-opa}/10-install-zk.yaml.j2 | 0 .../15-install-authentication-class.yaml | 5 + .../oidc-opa/15_authentication-class.yaml.j2 | 26 ++ .../kuttl/{opa => oidc-opa}/19-assert.yaml | 0 .../kuttl/oidc-opa/19-install-keycloak.yaml | 5 + .../oidc-opa/19-keycloak-realm-cm.yaml.j2 | 192 +++++++++ .../kuttl/oidc-opa/19_keycloak.yaml.j2 | 146 +++++++ .../kuttl/{opa => oidc-opa}/20-assert.yaml | 0 .../{opa => oidc-opa}/20-install-opa.yaml.j2 | 4 + .../templates/kuttl/oidc-opa/25-opa-rego.yaml | 89 ++++ .../kuttl/{opa => oidc-opa}/30-assert.yaml | 4 +- .../{opa => oidc-opa}/30-install-nifi.yaml | 0 .../kuttl/{opa => oidc-opa}/30_nifi.yaml.j2 | 6 +- tests/templates/kuttl/oidc-opa/31-assert.yaml | 12 + .../kuttl/oidc-opa/31-create-flow.yaml.j2 | 10 + tests/templates/kuttl/oidc-opa/31_flow.json | 382 ++++++++++++++++++ .../oidc-opa/40-create-configmap.yaml.j2 | 4 + .../kuttl/{opa => oidc-opa}/41-assert.yaml | 0 .../41-install-test-container.yaml.j2 | 22 +- .../kuttl/{opa => oidc-opa}/45-assert.yaml | 4 +- tests/templates/kuttl/oidc-opa/test.py | 193 +++++++++ tests/templates/kuttl/opa/15-auth_class.yaml | 17 - .../kuttl/opa/19-install-keycloak.yaml.j2 | 165 -------- .../kuttl/opa/19-keycloak-realm-cm.yaml | 78 ---- tests/templates/kuttl/opa/25-opa-rego.yaml | 49 --- .../kuttl/opa/40-create-configmap.yaml.j2 | 4 - tests/templates/kuttl/opa/test_nifi.py | 77 ---- tests/test-definition.yaml | 99 ++--- 32 files changed, 1142 insertions(+), 451 deletions(-) rename tests/templates/kuttl/{opa => oidc-opa}/00-assert.yaml.j2 (100%) rename tests/templates/kuttl/{opa => oidc-opa}/00-install-vector-aggregator-discovery-configmap.yaml.j2 (100%) rename tests/templates/kuttl/{opa => oidc-opa}/00-patch-ns.yaml.j2 (100%) rename tests/templates/kuttl/{opa => oidc-opa}/10-assert.yaml (100%) rename tests/templates/kuttl/{opa => oidc-opa}/10-install-zk.yaml.j2 (100%) create mode 100644 tests/templates/kuttl/oidc-opa/15-install-authentication-class.yaml create mode 100644 tests/templates/kuttl/oidc-opa/15_authentication-class.yaml.j2 rename tests/templates/kuttl/{opa => oidc-opa}/19-assert.yaml (100%) create mode 100644 tests/templates/kuttl/oidc-opa/19-install-keycloak.yaml create mode 100644 tests/templates/kuttl/oidc-opa/19-keycloak-realm-cm.yaml.j2 create mode 100644 tests/templates/kuttl/oidc-opa/19_keycloak.yaml.j2 rename tests/templates/kuttl/{opa => oidc-opa}/20-assert.yaml (100%) rename tests/templates/kuttl/{opa => oidc-opa}/20-install-opa.yaml.j2 (92%) create mode 100644 tests/templates/kuttl/oidc-opa/25-opa-rego.yaml rename tests/templates/kuttl/{opa => oidc-opa}/30-assert.yaml (82%) rename tests/templates/kuttl/{opa => oidc-opa}/30-install-nifi.yaml (100%) rename tests/templates/kuttl/{opa => oidc-opa}/30_nifi.yaml.j2 (88%) create mode 100644 tests/templates/kuttl/oidc-opa/31-assert.yaml create mode 100644 tests/templates/kuttl/oidc-opa/31-create-flow.yaml.j2 create mode 100644 tests/templates/kuttl/oidc-opa/31_flow.json create mode 100644 tests/templates/kuttl/oidc-opa/40-create-configmap.yaml.j2 rename tests/templates/kuttl/{opa => oidc-opa}/41-assert.yaml (100%) rename tests/templates/kuttl/{opa => oidc-opa}/41-install-test-container.yaml.j2 (69%) rename tests/templates/kuttl/{opa => oidc-opa}/45-assert.yaml (65%) create mode 100644 tests/templates/kuttl/oidc-opa/test.py delete mode 100644 tests/templates/kuttl/opa/15-auth_class.yaml delete mode 100644 tests/templates/kuttl/opa/19-install-keycloak.yaml.j2 delete mode 100644 tests/templates/kuttl/opa/19-keycloak-realm-cm.yaml delete mode 100644 tests/templates/kuttl/opa/25-opa-rego.yaml delete mode 100644 tests/templates/kuttl/opa/40-create-configmap.yaml.j2 delete mode 100755 tests/templates/kuttl/opa/test_nifi.py diff --git a/tests/templates/kuttl/opa/00-assert.yaml.j2 b/tests/templates/kuttl/oidc-opa/00-assert.yaml.j2 similarity index 100% rename from tests/templates/kuttl/opa/00-assert.yaml.j2 rename to tests/templates/kuttl/oidc-opa/00-assert.yaml.j2 diff --git a/tests/templates/kuttl/opa/00-install-vector-aggregator-discovery-configmap.yaml.j2 b/tests/templates/kuttl/oidc-opa/00-install-vector-aggregator-discovery-configmap.yaml.j2 similarity index 100% rename from tests/templates/kuttl/opa/00-install-vector-aggregator-discovery-configmap.yaml.j2 rename to tests/templates/kuttl/oidc-opa/00-install-vector-aggregator-discovery-configmap.yaml.j2 diff --git a/tests/templates/kuttl/opa/00-patch-ns.yaml.j2 b/tests/templates/kuttl/oidc-opa/00-patch-ns.yaml.j2 similarity index 100% rename from tests/templates/kuttl/opa/00-patch-ns.yaml.j2 rename to tests/templates/kuttl/oidc-opa/00-patch-ns.yaml.j2 diff --git a/tests/templates/kuttl/opa/10-assert.yaml b/tests/templates/kuttl/oidc-opa/10-assert.yaml similarity index 100% rename from tests/templates/kuttl/opa/10-assert.yaml rename to tests/templates/kuttl/oidc-opa/10-assert.yaml diff --git a/tests/templates/kuttl/opa/10-install-zk.yaml.j2 b/tests/templates/kuttl/oidc-opa/10-install-zk.yaml.j2 similarity index 100% rename from tests/templates/kuttl/opa/10-install-zk.yaml.j2 rename to tests/templates/kuttl/oidc-opa/10-install-zk.yaml.j2 diff --git a/tests/templates/kuttl/oidc-opa/15-install-authentication-class.yaml b/tests/templates/kuttl/oidc-opa/15-install-authentication-class.yaml new file mode 100644 index 00000000..73181d11 --- /dev/null +++ b/tests/templates/kuttl/oidc-opa/15-install-authentication-class.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: envsubst < 15_authentication-class.yaml | kubectl apply -n $NAMESPACE -f - diff --git a/tests/templates/kuttl/oidc-opa/15_authentication-class.yaml.j2 b/tests/templates/kuttl/oidc-opa/15_authentication-class.yaml.j2 new file mode 100644 index 00000000..8b0afe51 --- /dev/null +++ b/tests/templates/kuttl/oidc-opa/15_authentication-class.yaml.j2 @@ -0,0 +1,26 @@ +--- +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 +{% if test_scenario['values']['oidc-use-tls'] == 'true' %} + port: 8443 + tls: + verification: + server: + caCert: + secretClass: keycloak-tls-$NAMESPACE +{% else %} + port: 8080 + tls: null +{% endif %} diff --git a/tests/templates/kuttl/opa/19-assert.yaml b/tests/templates/kuttl/oidc-opa/19-assert.yaml similarity index 100% rename from tests/templates/kuttl/opa/19-assert.yaml rename to tests/templates/kuttl/oidc-opa/19-assert.yaml diff --git a/tests/templates/kuttl/oidc-opa/19-install-keycloak.yaml b/tests/templates/kuttl/oidc-opa/19-install-keycloak.yaml new file mode 100644 index 00000000..6a4b4b33 --- /dev/null +++ b/tests/templates/kuttl/oidc-opa/19-install-keycloak.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: envsubst < 19_keycloak.yaml | kubectl apply -n $NAMESPACE -f - diff --git a/tests/templates/kuttl/oidc-opa/19-keycloak-realm-cm.yaml.j2 b/tests/templates/kuttl/oidc-opa/19-keycloak-realm-cm.yaml.j2 new file mode 100644 index 00000000..914ce580 --- /dev/null +++ b/tests/templates/kuttl/oidc-opa/19-keycloak-realm-cm.yaml.j2 @@ -0,0 +1,192 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: user-info-fetcher-client-credentials +stringData: + clientId: user-info-fetcher + clientSecret: user-info-fetcher-client-secret +--- +apiVersion: v1 +kind: Secret +metadata: + name: nifi-client-credentials +stringData: + clientId: nifi + clientSecret: nifi-client-secret +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: keycloak-test-realm +data: + realm.json: | + { + "realm": "test", + "enabled": true, + "attributes": { + "frontendUrl": "keycloak.$NAMESPACE.svc.cluster.local" + }, + "groups": [ + { + "name": "nifi-admin", + "path": "/nifi-admin" + }, + { + "name": "nifi-user", + "path": "/nifi-user" + }, + { + "name": "nifi-process-group-a", + "path": "/nifi-process-group-a" + }, + { + "name": "nifi-process-group-b", + "path": "/nifi-process-group-b" + } + ], + "users": [ + { + "username": "nifi-admin", + "enabled": true, + "emailVerified": true, + "firstName": "nifi-admin", + "lastName": "nifi-admin", + "email": "nifi-admin@example.com", + "credentials": [ + { + "type": "password", + "value": "nifi-admin" + } + ], + "realmRoles": [ + "default-roles-my-dataspace" + ], + "groups": [ + "/nifi-admin" + ] + }, + { + "username": "alice", + "enabled": true, + "emailVerified": true, + "firstName": "alice", + "lastName": "alice", + "email": "alice@example.com", + "credentials": [ + { + "type": "password", + "value": "alice" + } + ], + "realmRoles": [ + "default-roles-my-dataspace" + ], + "groups": [ + "/nifi-user", + "/nifi-process-group-a" + ] + }, + { + "username": "bob", + "enabled": true, + "emailVerified": true, + "firstName": "bob", + "lastName": "bob", + "email": "bob@example.com", + "credentials": [ + { + "type": "password", + "value": "bob" + } + ], + "realmRoles": [], + "groups": [ + "/nifi-user", + "/nifi-process-group-b" + ] + }, + { + "username": "charlie", + "enabled": true, + "emailVerified": true, + "firstName": "charlie", + "lastName": "charlie", + "email": "charlie@example.com", + "credentials": [ + { + "type": "password", + "value": "charlie" + } + ], + "realmRoles": [ + "default-roles-my-dataspace" + ], + "groups": [ + "/nifi-user" + ] + }, + { + "username": "service-account-user-info-fetcher", + "enabled": true, + "totp": false, + "emailVerified": false, + "serviceAccountClientId": "user-info-fetcher", + "credentials": [], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": [ + "default-roles-my-dataspace" + ], + "clientRoles": { + "realm-management": [ + "view-users" + ] + }, + "notBefore": 0, + "groups": [] + } + ], + "clients": [ + { + "clientId": "user-info-fetcher", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "user-info-fetcher-client-secret", + "redirectUris": [ + "/*" + ], + "webOrigins": [ + "/*" + ], + "notBefore": 0, + "bearerOnly": false, + "serviceAccountsEnabled": true, + "publicClient": false, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "oidc.ciba.grant.enabled": "true", + "oauth2.device.authorization.grant.enabled": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true + }, + { + "clientId": "nifi", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "nifi-client-secret", + "redirectUris": [ + "*" + ], + "webOrigins": [ + "*" + ], + "standardFlowEnabled": true, + "protocol": "openid-connect" + } + ] + } diff --git a/tests/templates/kuttl/oidc-opa/19_keycloak.yaml.j2 b/tests/templates/kuttl/oidc-opa/19_keycloak.yaml.j2 new file mode 100644 index 00000000..34923758 --- /dev/null +++ b/tests/templates/kuttl/oidc-opa/19_keycloak.yaml.j2 @@ -0,0 +1,146 @@ +--- +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-$NAMESPACE +spec: + backend: + autoTls: + ca: + autoGenerate: true + secret: + name: keycloak-tls-ca-$NAMESPACE + namespace: $NAMESPACE +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: keycloak + labels: + app: keycloak +spec: + replicas: 1 + selector: + matchLabels: + app: keycloak + template: + metadata: + labels: + app: keycloak + spec: + serviceAccountName: keycloak + containers: + - name: keycloak + image: quay.io/keycloak/keycloak:25.0.0 + args: + - start + - --hostname-strict=false + - --import-realm +{% if test_scenario['values']['oidc-use-tls'] == 'true' %} + - --https-key-store-file=/tls/keystore.p12 + - --https-key-store-password=changeit +{% endif %} + env: + - name: KEYCLOAK_ADMIN + value: admin + - name: KEYCLOAK_ADMIN_PASSWORD + value: admin + - name: JAVA_OPTS_KC_HEAP + value: -Xmx500M +{% if test_scenario['values']['oidc-use-tls'] == 'true' %} + ports: + - name: https + containerPort: 8443 +{% else %} + - name: http + containerPort: 8080 +{% endif %} + readinessProbe: + httpGet: + scheme: HTTPS + path: /realms/master + port: https + resources: + limits: + cpu: 1 + memory: 1024Mi + requests: + cpu: 500m + memory: 1024Mi + volumeMounts: + - name: data + mountPath: /opt/keycloak/data/ + - name: tls + mountPath: /tls/ + - name: realm-volume + mountPath: /opt/keycloak/data/import + securityContext: + fsGroup: 1000 + runAsGroup: 1000 + runAsUser: 1000 + volumes: + - name: data + emptyDir: {} + - name: tls + ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/class: keycloak-tls-$NAMESPACE + secrets.stackable.tech/format: tls-pkcs12 + secrets.stackable.tech/format.compatibility.tls-pkcs12.password: changeit + secrets.stackable.tech/scope: service=keycloak,node + spec: + storageClassName: secrets.stackable.tech + accessModes: + - ReadWriteOnce + resources: + requests: + storage: "1" + - name: realm-volume + configMap: + name: keycloak-test-realm +--- +apiVersion: v1 +kind: Service +metadata: + name: keycloak + labels: + app: keycloak +spec: + ports: + - name: https + port: 8443 + targetPort: 8443 + selector: + app: keycloak diff --git a/tests/templates/kuttl/opa/20-assert.yaml b/tests/templates/kuttl/oidc-opa/20-assert.yaml similarity index 100% rename from tests/templates/kuttl/opa/20-assert.yaml rename to tests/templates/kuttl/oidc-opa/20-assert.yaml diff --git a/tests/templates/kuttl/opa/20-install-opa.yaml.j2 b/tests/templates/kuttl/oidc-opa/20-install-opa.yaml.j2 similarity index 92% rename from tests/templates/kuttl/opa/20-install-opa.yaml.j2 rename to tests/templates/kuttl/oidc-opa/20-install-opa.yaml.j2 index 3df2666d..b5cfcb59 100644 --- a/tests/templates/kuttl/opa/20-install-opa.yaml.j2 +++ b/tests/templates/kuttl/oidc-opa/20-install-opa.yaml.j2 @@ -18,12 +18,16 @@ commands: backend: keycloak: hostname: keycloak.$NAMESPACE.svc.cluster.local +{% if test_scenario['values']['oidc-use-tls'] == 'true' %} port: 8443 tls: verification: server: caCert: secretClass: keycloak-tls-$NAMESPACE +{% else %} + port: 8080 +{% endif %} clientCredentialsSecret: user-info-fetcher-client-credentials adminRealm: test userRealm: test diff --git a/tests/templates/kuttl/oidc-opa/25-opa-rego.yaml b/tests/templates/kuttl/oidc-opa/25-opa-rego.yaml new file mode 100644 index 00000000..2b44141d --- /dev/null +++ b/tests/templates/kuttl/oidc-opa/25-opa-rego.yaml @@ -0,0 +1,89 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: opa-nifi-uif-rego + labels: + opa.stackable.tech/bundle: "true" +data: + nifi.rego: | + package nifi + + # Setting "resourceNotFound" to true results in the parent resource beingevaluated for authorization, + # e.g. the parent of a processor is the processor-group. + # If a resource is matched by a rego rule that is not the default the parent resource will be ignored. + default allow := { + "resourceNotFound": true + } + + # Allow access nifi-admin for all resource types that don't have other policies in place + # This convoluted way of writing the rule is necessary because a general allow rule for nifi-admin would result in + # an the rego error "complete rules must not produce multiple outputs" + allow := { + "allowed": true, + } if { + resource_types_with_other_policies = [ + "/flow", + "/process-groups", + "/processors" + ] + every resource_type in resource_types_with_other_policies { not startswith(input.resource.id, resource_type) } + groups_with_access := ["nifi-admin"] + some group in user_groups + group in groups_with_access + } + + # Access to the UI for every user + allow := { + "allowed": true, + "dumpCache": true + } if { + input.resource.id in ["/flow"] + groups_with_access := ["nifi-admin", "nifi-user"] + some group in user_groups + group in groups_with_access + } + + # Access to the root process group for every user + allow := { + "allowed": true, + "dumpCache": true + } if { + input.resource.name == "NiFi Flow" + startswith(input.resource.id, "/process-groups") + groups_with_access := ["nifi-admin", "nifi-user"] + some group in user_groups + group in groups_with_access + } + + # Allow access to process groups A & B only to users in the corresponding group & admin + # Rules have to explicitly deny access because every user in group "nifi-user" + # has inherited access to each process group for which they are not explicitly denied. + allow := { + "allowed": false, + } if { + input.resource.id == "/process-groups/c9186a05-0196-1000-ffff-ffffd8474359" + groups_with_access := ["nifi-admin", "nifi-process-group-a"] + every group in user_groups { not group in groups_with_access } + } + + allow := { + "allowed": false + } if { + input.resource.id == "/process-groups/7e08561b-447d-3acb-b510-744d886c3ca4" + groups_with_access := ["nifi-admin", "nifi-process-group-b"] + every group in user_groups { not group in groups_with_access } + } + + allow := { + "allowed": true, + "dumpCache": true + } if { + input.identity.name == "CN=generated certificate for pod" + input.resource.id == "/proxy" + } + + user_groups := user_groups if { + user_group_paths := data.stackable.opa.userinfo.v1.userInfoByUsername(input.identity.name).groups + user_groups := [ trim(user_group,"/") | user_group := user_group_paths[_] ] + } diff --git a/tests/templates/kuttl/opa/30-assert.yaml b/tests/templates/kuttl/oidc-opa/30-assert.yaml similarity index 82% rename from tests/templates/kuttl/opa/30-assert.yaml rename to tests/templates/kuttl/oidc-opa/30-assert.yaml index 03958264..2f03b6b1 100644 --- a/tests/templates/kuttl/opa/30-assert.yaml +++ b/tests/templates/kuttl/oidc-opa/30-assert.yaml @@ -8,5 +8,5 @@ kind: StatefulSet metadata: name: test-nifi-node-default status: - readyReplicas: 2 - replicas: 2 + readyReplicas: 1 + replicas: 1 diff --git a/tests/templates/kuttl/opa/30-install-nifi.yaml b/tests/templates/kuttl/oidc-opa/30-install-nifi.yaml similarity index 100% rename from tests/templates/kuttl/opa/30-install-nifi.yaml rename to tests/templates/kuttl/oidc-opa/30-install-nifi.yaml diff --git a/tests/templates/kuttl/opa/30_nifi.yaml.j2 b/tests/templates/kuttl/oidc-opa/30_nifi.yaml.j2 similarity index 88% rename from tests/templates/kuttl/opa/30_nifi.yaml.j2 rename to tests/templates/kuttl/oidc-opa/30_nifi.yaml.j2 index 506fe565..27fb4ff1 100644 --- a/tests/templates/kuttl/opa/30_nifi.yaml.j2 +++ b/tests/templates/kuttl/oidc-opa/30_nifi.yaml.j2 @@ -21,7 +21,9 @@ spec: pullPolicy: IfNotPresent clusterConfig: authentication: - - authenticationClass: simple-nifi-users + - authenticationClass: nifi-oidc-auth-class-$NAMESPACE + oidc: + clientCredentialsSecret: nifi-client-credentials authorization: opa: configMapName: opa @@ -44,4 +46,4 @@ spec: roleGroups: default: config: {} - replicas: 2 + replicas: 1 diff --git a/tests/templates/kuttl/oidc-opa/31-assert.yaml b/tests/templates/kuttl/oidc-opa/31-assert.yaml new file mode 100644 index 00000000..2f03b6b1 --- /dev/null +++ b/tests/templates/kuttl/oidc-opa/31-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: 1 + replicas: 1 diff --git a/tests/templates/kuttl/oidc-opa/31-create-flow.yaml.j2 b/tests/templates/kuttl/oidc-opa/31-create-flow.yaml.j2 new file mode 100644 index 00000000..0430f267 --- /dev/null +++ b/tests/templates/kuttl/oidc-opa/31-create-flow.yaml.j2 @@ -0,0 +1,10 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: | + gzip --keep 31_flow.json + kubectl cp -n $NAMESPACE -c nifi ./31_flow.json.gz test-nifi-node-default-0:/stackable/data/database/flow.json.gz + rm 31_flow.json.gz + # Delete Pod to trigger NiFi to load the replaced flow.json.gz + kubectl -n $NAMESPACE delete pod test-nifi-node-default-0 diff --git a/tests/templates/kuttl/oidc-opa/31_flow.json b/tests/templates/kuttl/oidc-opa/31_flow.json new file mode 100644 index 00000000..4a2691c5 --- /dev/null +++ b/tests/templates/kuttl/oidc-opa/31_flow.json @@ -0,0 +1,382 @@ +{ + "encodingVersion": { + "majorVersion": 2, + "minorVersion": 0 + }, + "maxTimerDrivenThreadCount": 10, + "registries": [], + "parameterContexts": [], + "parameterProviders": [], + "controllerServices": [], + "reportingTasks": [], + "flowAnalysisRules": [], + "rootGroup": { + "identifier": "96d9abc7-e736-324d-9da8-1c0a4e671508", + "instanceIdentifier": "c377901a-0196-1000-d1e8-b549997e4d94", + "name": "NiFi Flow", + "comments": "", + "position": { + "x": 0, + "y": 0 + }, + "processGroups": [ + { + "identifier": "0ad13e3a-a773-380d-0000-000031bd1ecb", + "instanceIdentifier": "7e08561b-447d-3acb-b510-744d886c3ca4", + "name": "Process Group B", + "comments": "", + "position": { + "x": 248, + "y": -112 + }, + "processGroups": [], + "remoteProcessGroups": [], + "processors": [ + { + "identifier": "347682fa-61d4-31ae-0000-000031bd1ecb", + "instanceIdentifier": "351336f0-903f-362c-b479-c06040537e68", + "name": "GenerateFlowFile C", + "comments": "", + "position": { + "x": -1032, + "y": -520 + }, + "type": "org.apache.nifi.processors.standard.GenerateFlowFile", + "bundle": { + "group": "org.apache.nifi", + "artifact": "nifi-standard-nar", + "version": "2.2.0" + }, + "properties": { + "character-set": "UTF-8", + "File Size": "0B", + "Batch Size": "1", + "Unique FlowFiles": "false", + "Data Format": "Text" + }, + "propertyDescriptors": {}, + "style": {}, + "schedulingPeriod": "1 min", + "schedulingStrategy": "TIMER_DRIVEN", + "executionNode": "ALL", + "penaltyDuration": "30 sec", + "yieldDuration": "1 sec", + "bulletinLevel": "WARN", + "runDurationMillis": 0, + "concurrentlySchedulableTaskCount": 1, + "autoTerminatedRelationships": [], + "scheduledState": "ENABLED", + "retryCount": 10, + "retriedRelationships": [], + "backoffMechanism": "PENALIZE_FLOWFILE", + "maxBackoffPeriod": "10 mins", + "componentType": "PROCESSOR", + "groupIdentifier": "0ad13e3a-a773-380d-0000-000031bd1ecb" + }, + { + "identifier": "f7b6da7b-daa3-3b68-0000-000031bd1ecb", + "instanceIdentifier": "1ed101fd-f976-38ad-8137-45dc0c7b6e6e", + "name": "GenerateFlowFile D", + "comments": "", + "position": { + "x": -648, + "y": -520 + }, + "type": "org.apache.nifi.processors.standard.GenerateFlowFile", + "bundle": { + "group": "org.apache.nifi", + "artifact": "nifi-standard-nar", + "version": "2.2.0" + }, + "properties": { + "character-set": "UTF-8", + "File Size": "0B", + "Batch Size": "1", + "Unique FlowFiles": "false", + "Data Format": "Text" + }, + "propertyDescriptors": {}, + "style": {}, + "schedulingPeriod": "1 min", + "schedulingStrategy": "TIMER_DRIVEN", + "executionNode": "ALL", + "penaltyDuration": "30 sec", + "yieldDuration": "1 sec", + "bulletinLevel": "WARN", + "runDurationMillis": 0, + "concurrentlySchedulableTaskCount": 1, + "autoTerminatedRelationships": [], + "scheduledState": "ENABLED", + "retryCount": 10, + "retriedRelationships": [], + "backoffMechanism": "PENALIZE_FLOWFILE", + "maxBackoffPeriod": "10 mins", + "componentType": "PROCESSOR", + "groupIdentifier": "0ad13e3a-a773-380d-0000-000031bd1ecb" + } + ], + "inputPorts": [], + "outputPorts": [], + "connections": [], + "labels": [], + "funnels": [], + "controllerServices": [], + "defaultFlowFileExpiration": "0 sec", + "defaultBackPressureObjectThreshold": 10000, + "defaultBackPressureDataSizeThreshold": "1 GB", + "scheduledState": "ENABLED", + "executionEngine": "INHERITED", + "maxConcurrentTasks": 1, + "statelessFlowTimeout": "1 min", + "flowFileConcurrency": "UNBOUNDED", + "flowFileOutboundPolicy": "STREAM_WHEN_AVAILABLE", + "componentType": "PROCESS_GROUP", + "groupIdentifier": "96d9abc7-e736-324d-9da8-1c0a4e671508" + }, + { + "identifier": "8f62d3e7-2a0b-3ba8-936c-8a0cb425284a", + "instanceIdentifier": "c9186a05-0196-1000-ffff-ffffd8474359", + "name": "Process Group A", + "comments": "", + "position": { + "x": -190, + "y": -112.91667938232422 + }, + "processGroups": [], + "remoteProcessGroups": [], + "processors": [ + { + "identifier": "e14260ce-23cd-361d-0000-00001b31a7fc", + "instanceIdentifier": "63ad5386-61ce-327a-bce3-5b597829aa54", + "name": "GenerateFlowFile B", + "comments": "", + "position": { + "x": -648, + "y": -520 + }, + "type": "org.apache.nifi.processors.standard.GenerateFlowFile", + "bundle": { + "group": "org.apache.nifi", + "artifact": "nifi-standard-nar", + "version": "2.2.0" + }, + "properties": { + "character-set": "UTF-8", + "File Size": "0B", + "Batch Size": "1", + "Unique FlowFiles": "false", + "Data Format": "Text" + }, + "propertyDescriptors": {}, + "style": {}, + "schedulingPeriod": "1 min", + "schedulingStrategy": "TIMER_DRIVEN", + "executionNode": "ALL", + "penaltyDuration": "30 sec", + "yieldDuration": "1 sec", + "bulletinLevel": "WARN", + "runDurationMillis": 0, + "concurrentlySchedulableTaskCount": 1, + "autoTerminatedRelationships": [], + "scheduledState": "ENABLED", + "retryCount": 10, + "retriedRelationships": [], + "backoffMechanism": "PENALIZE_FLOWFILE", + "maxBackoffPeriod": "10 mins", + "componentType": "PROCESSOR", + "groupIdentifier": "8f62d3e7-2a0b-3ba8-936c-8a0cb425284a" + }, + { + "identifier": "4623a22e-5c14-310b-afb8-c82965f74a7d", + "instanceIdentifier": "c9196a9e-0196-1000-0000-0000418515a4", + "name": "GenerateFlowFile A", + "comments": "", + "position": { + "x": -1032, + "y": -520 + }, + "type": "org.apache.nifi.processors.standard.GenerateFlowFile", + "bundle": { + "group": "org.apache.nifi", + "artifact": "nifi-standard-nar", + "version": "2.2.0" + }, + "properties": { + "character-set": "UTF-8", + "File Size": "0B", + "Batch Size": "1", + "Unique FlowFiles": "false", + "Data Format": "Text" + }, + "propertyDescriptors": {}, + "style": {}, + "schedulingPeriod": "1 min", + "schedulingStrategy": "TIMER_DRIVEN", + "executionNode": "ALL", + "penaltyDuration": "30 sec", + "yieldDuration": "1 sec", + "bulletinLevel": "WARN", + "runDurationMillis": 0, + "concurrentlySchedulableTaskCount": 1, + "autoTerminatedRelationships": [], + "scheduledState": "ENABLED", + "retryCount": 10, + "retriedRelationships": [], + "backoffMechanism": "PENALIZE_FLOWFILE", + "maxBackoffPeriod": "10 mins", + "componentType": "PROCESSOR", + "groupIdentifier": "8f62d3e7-2a0b-3ba8-936c-8a0cb425284a" + } + ], + "inputPorts": [], + "outputPorts": [], + "connections": [], + "labels": [], + "funnels": [], + "controllerServices": [], + "defaultFlowFileExpiration": "0 sec", + "defaultBackPressureObjectThreshold": 10000, + "defaultBackPressureDataSizeThreshold": "1 GB", + "scheduledState": "ENABLED", + "executionEngine": "INHERITED", + "maxConcurrentTasks": 1, + "statelessFlowTimeout": "1 min", + "flowFileConcurrency": "UNBOUNDED", + "flowFileOutboundPolicy": "STREAM_WHEN_AVAILABLE", + "componentType": "PROCESS_GROUP", + "groupIdentifier": "96d9abc7-e736-324d-9da8-1c0a4e671508" + }, + { + "identifier": "82533e93-7494-352d-ffff-ffffaad75c48", + "instanceIdentifier": "f3b78341-b2ff-3ae3-b7fc-4b518617c802", + "name": "Process Group C", + "comments": "", + "position": { + "x": 696, + "y": -112 + }, + "processGroups": [], + "remoteProcessGroups": [], + "processors": [ + { + "identifier": "fcd0595d-bf18-34be-ffff-ffffaad75c48", + "instanceIdentifier": "9d95cac3-2759-3fce-9c07-71215b0fb554", + "name": "GenerateFlowFile E", + "comments": "", + "position": { + "x": -1032, + "y": -520 + }, + "type": "org.apache.nifi.processors.standard.GenerateFlowFile", + "bundle": { + "group": "org.apache.nifi", + "artifact": "nifi-standard-nar", + "version": "2.2.0" + }, + "properties": { + "character-set": "UTF-8", + "File Size": "0B", + "Batch Size": "1", + "Unique FlowFiles": "false", + "Data Format": "Text" + }, + "propertyDescriptors": {}, + "style": {}, + "schedulingPeriod": "1 min", + "schedulingStrategy": "TIMER_DRIVEN", + "executionNode": "ALL", + "penaltyDuration": "30 sec", + "yieldDuration": "1 sec", + "bulletinLevel": "WARN", + "runDurationMillis": 0, + "concurrentlySchedulableTaskCount": 1, + "autoTerminatedRelationships": [], + "scheduledState": "ENABLED", + "retryCount": 10, + "retriedRelationships": [], + "backoffMechanism": "PENALIZE_FLOWFILE", + "maxBackoffPeriod": "10 mins", + "componentType": "PROCESSOR", + "groupIdentifier": "82533e93-7494-352d-ffff-ffffaad75c48" + }, + { + "identifier": "077fd08e-4c7c-39cc-ffff-ffffaad75c48", + "instanceIdentifier": "63d8f319-893c-38d4-9cd4-35cea3f43ea2", + "name": "GenerateFlowFile F", + "comments": "", + "position": { + "x": -648, + "y": -520 + }, + "type": "org.apache.nifi.processors.standard.GenerateFlowFile", + "bundle": { + "group": "org.apache.nifi", + "artifact": "nifi-standard-nar", + "version": "2.2.0" + }, + "properties": { + "character-set": "UTF-8", + "File Size": "0B", + "Batch Size": "1", + "Unique FlowFiles": "false", + "Data Format": "Text" + }, + "propertyDescriptors": {}, + "style": {}, + "schedulingPeriod": "1 min", + "schedulingStrategy": "TIMER_DRIVEN", + "executionNode": "ALL", + "penaltyDuration": "30 sec", + "yieldDuration": "1 sec", + "bulletinLevel": "WARN", + "runDurationMillis": 0, + "concurrentlySchedulableTaskCount": 1, + "autoTerminatedRelationships": [], + "scheduledState": "ENABLED", + "retryCount": 10, + "retriedRelationships": [], + "backoffMechanism": "PENALIZE_FLOWFILE", + "maxBackoffPeriod": "10 mins", + "componentType": "PROCESSOR", + "groupIdentifier": "82533e93-7494-352d-ffff-ffffaad75c48" + } + ], + "inputPorts": [], + "outputPorts": [], + "connections": [], + "labels": [], + "funnels": [], + "controllerServices": [], + "defaultFlowFileExpiration": "0 sec", + "defaultBackPressureObjectThreshold": 10000, + "defaultBackPressureDataSizeThreshold": "1 GB", + "scheduledState": "ENABLED", + "executionEngine": "INHERITED", + "maxConcurrentTasks": 1, + "statelessFlowTimeout": "1 min", + "flowFileConcurrency": "UNBOUNDED", + "flowFileOutboundPolicy": "STREAM_WHEN_AVAILABLE", + "componentType": "PROCESS_GROUP", + "groupIdentifier": "96d9abc7-e736-324d-9da8-1c0a4e671508" + } + ], + "remoteProcessGroups": [], + "processors": [], + "inputPorts": [], + "outputPorts": [], + "connections": [], + "labels": [], + "funnels": [], + "controllerServices": [], + "defaultFlowFileExpiration": "0 sec", + "defaultBackPressureObjectThreshold": 10000, + "defaultBackPressureDataSizeThreshold": "1 GB", + "scheduledState": "ENABLED", + "executionEngine": "INHERITED", + "maxConcurrentTasks": 1, + "statelessFlowTimeout": "1 min", + "flowFileConcurrency": "UNBOUNDED", + "flowFileOutboundPolicy": "STREAM_WHEN_AVAILABLE", + "componentType": "PROCESS_GROUP" + } +} diff --git a/tests/templates/kuttl/oidc-opa/40-create-configmap.yaml.j2 b/tests/templates/kuttl/oidc-opa/40-create-configmap.yaml.j2 new file mode 100644 index 00000000..51b0321c --- /dev/null +++ b/tests/templates/kuttl/oidc-opa/40-create-configmap.yaml.j2 @@ -0,0 +1,4 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: kubectl create cm test-script -n $NAMESPACE --from-file=test.py diff --git a/tests/templates/kuttl/opa/41-assert.yaml b/tests/templates/kuttl/oidc-opa/41-assert.yaml similarity index 100% rename from tests/templates/kuttl/opa/41-assert.yaml rename to tests/templates/kuttl/oidc-opa/41-assert.yaml diff --git a/tests/templates/kuttl/opa/41-install-test-container.yaml.j2 b/tests/templates/kuttl/oidc-opa/41-install-test-container.yaml.j2 similarity index 69% rename from tests/templates/kuttl/opa/41-install-test-container.yaml.j2 rename to tests/templates/kuttl/oidc-opa/41-install-test-container.yaml.j2 index 21383e9a..89ec070f 100644 --- a/tests/templates/kuttl/opa/41-install-test-container.yaml.j2 +++ b/tests/templates/kuttl/oidc-opa/41-install-test-container.yaml.j2 @@ -54,8 +54,8 @@ spec: securityContext: fsGroup: 1000 containers: - - name: python - image: docker.stackable.tech/stackable/testing-tools:0.2.0-stackable0.0.0-dev + - name: oidc-login-test + image: oci.stackable.tech/sdp/testing-tools:0.2.0-stackable0.0.0-dev stdin: true tty: true resources: @@ -65,11 +65,21 @@ spec: limits: memory: "128Mi" cpu: "1" + env: + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: NIFI_VERSION + value: "{{ test_scenario['values']['nifi'] }}" + - name: OIDC_USE_TLS + value: "true" volumeMounts: - - name: test-nifi - mountPath: /tmp + - name: test-script + mountPath: /tmp/test-script + terminationGracePeriodSeconds: 1 volumes: - - name: test-nifi + - name: test-script configMap: - name: test-nifi + name: test-script terminationGracePeriodSeconds: 1 diff --git a/tests/templates/kuttl/opa/45-assert.yaml b/tests/templates/kuttl/oidc-opa/45-assert.yaml similarity index 65% rename from tests/templates/kuttl/opa/45-assert.yaml rename to tests/templates/kuttl/oidc-opa/45-assert.yaml index 7f22a990..bd633b9b 100644 --- a/tests/templates/kuttl/opa/45-assert.yaml +++ b/tests/templates/kuttl/oidc-opa/45-assert.yaml @@ -2,7 +2,7 @@ apiVersion: kuttl.dev/v1beta1 kind: TestAssert metadata: - name: login + name: test timeout: 300 commands: - - script: kubectl exec -n $NAMESPACE python-0 -- python /tmp/test_nifi.py -u admin -p supersecretpassword -n $NAMESPACE -c 2 + - script: kubectl exec -n $NAMESPACE python-0 -- python /tmp/test-script/test.py diff --git a/tests/templates/kuttl/oidc-opa/test.py b/tests/templates/kuttl/oidc-opa/test.py new file mode 100644 index 00000000..3f6ea777 --- /dev/null +++ b/tests/templates/kuttl/oidc-opa/test.py @@ -0,0 +1,193 @@ +import logging +import os +import requests +import sys +import json +from bs4 import BeautifulSoup + +logging.basicConfig( + level="DEBUG", format="%(asctime)s %(levelname)s: %(message)s", stream=sys.stdout +) + +namespace = os.environ["NAMESPACE"] +tls = os.environ["OIDC_USE_TLS"] +nifi_version = os.environ["NIFI_VERSION"] +nifi = f"test-nifi-node-default-0.test-nifi-node-default.{namespace}.svc.cluster.local" +keycloak_service = f"keycloak.{namespace}.svc.cluster.local" + +keycloak_base_url = ( + f"https://{keycloak_service}:8443" + if tls == "true" + else f"http://{keycloak_service}:8080" +) + + +def login(session: requests.Session, username: str, password: str): + # startswith instead of an exact check to + # a) hit all 2.x versions and + # b) to allow for custom images because `nifi_version` will contain the whole custom image string + # e.g. 2.0.0,localhost:5000/stackable/nifi:2.0.0-stackable0.0.0-dev + if not nifi_version.startswith("1."): + 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=" + ), "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": username, "password": password}, + verify=False, + ) + assert welcome_page.ok, "Login failed" + assert welcome_page.url == f"https://{nifi}:8443/nifi/", ( + "Redirection to the NiFi web UI expected" + ) + + +# alice +session = requests.Session() +login(session, "alice", "alice") +process_group_a = session.get( + f"https://{nifi}:8443/nifi-api/flow/process-groups/c9186a05-0196-1000-ffff-ffffd8474359?uiOnly=true", + verify=False, +) +assert process_group_a.json()["permissions"]["canRead"], ( + "Alice should be able to access process group A" +) +process_group_b = session.get( + f"https://{nifi}:8443/nifi-api/flow/process-groups/7e08561b-447d-3acb-b510-744d886c3ca4?uiOnly=true", + verify=False, +) +assert not process_group_b.json()["permissions"]["canRead"], ( + "Alice should not be able to access process group B" +) +processor_e = session.get( + f"https://{nifi}:8443/nifi-api/processors/9d95cac3-2759-3fce-9c07-71215b0fb554?uiOnly=true", + verify=False, +) +assert processor_e.ok, "Alice should be able to access a processor E in process group C" +counters = session.get( + f"https://{nifi}:8443/nifi-api/counters", + verify=False, +) +assert not counters.ok, ( + "Alice should not be able to access the global resource 'counters'" +) + +# bob +session = requests.Session() +login(session, "bob", "bob") +process_group_a = session.get( + f"https://{nifi}:8443/nifi-api/flow/process-groups/c9186a05-0196-1000-ffff-ffffd8474359?uiOnly=true", + verify=False, +) +assert not process_group_a.json()["permissions"]["canRead"], ( + "Bob should not be able to access process group A" +) +process_group_b = session.get( + f"https://{nifi}:8443/nifi-api/flow/process-groups/7e08561b-447d-3acb-b510-744d886c3ca4?uiOnly=true", + verify=False, +) +assert process_group_b.json()["permissions"]["canRead"], ( + "Bob should be able to access process group B" +) +processor_e = session.get( + f"https://{nifi}:8443/nifi-api/processors/9d95cac3-2759-3fce-9c07-71215b0fb554?uiOnly=true", + verify=False, +) +assert processor_e.ok, "Bob should be able to access a processor E in process group C" +counters = session.get( + f"https://{nifi}:8443/nifi-api/counters", + verify=False, +) +assert not counters.ok, ( + "Bob should not be able to access the global resource 'counters'" +) + +# charlie +session = requests.Session() +login(session, "charlie", "charlie") +process_group_a = session.get( + f"https://{nifi}:8443/nifi-api/flow/process-groups/c9186a05-0196-1000-ffff-ffffd8474359?uiOnly=true", + verify=False, +) +assert not process_group_a.json()["permissions"]["canRead"], ( + "Charlie should not be able to access process group A" +) +process_group_b = session.get( + f"https://{nifi}:8443/nifi-api/flow/process-groups/7e08561b-447d-3acb-b510-744d886c3ca4?uiOnly=true", + verify=False, +) +assert not process_group_b.json()["permissions"]["canRead"], ( + "Charlie should not be able to access process group B" +) +processor_e = session.get( + f"https://{nifi}:8443/nifi-api/processors/9d95cac3-2759-3fce-9c07-71215b0fb554?uiOnly=true", + verify=False, +) +assert processor_e.ok, ( + "Charlie should be able to access a processor E in process group C" +) +counters = session.get( + f"https://{nifi}:8443/nifi-api/counters", + verify=False, +) +assert not counters.ok, ( + "Charlie should not be able to access the global resource 'counters'" +) + +# nifi-admin +session = requests.Session() +login(session, "nifi-admin", "nifi-admin") +process_group_a = session.get( + f"https://{nifi}:8443/nifi-api/flow/process-groups/c9186a05-0196-1000-ffff-ffffd8474359?uiOnly=true", + verify=False, +) +assert process_group_a.json()["permissions"]["canRead"], ( + "Nifi-admin should be able to access process group A" +) +process_group_b = session.get( + f"https://{nifi}:8443/nifi-api/flow/process-groups/7e08561b-447d-3acb-b510-744d886c3ca4?uiOnly=true", + verify=False, +) +assert process_group_b.json()["permissions"]["canRead"], ( + "Nifi-admin should be able to access process group B" +) +processor_e = session.get( + f"https://{nifi}:8443/nifi-api/processors/9d95cac3-2759-3fce-9c07-71215b0fb554?uiOnly=true", + verify=False, +) +assert processor_e.ok, ( + "Nifi-admin should be able to access a processor E in process group C" +) +counters = session.get( + f"https://{nifi}:8443/nifi-api/counters", + verify=False, +) +assert counters.ok, "Nifi-admin should be able to access the global resource 'counters'" diff --git a/tests/templates/kuttl/opa/15-auth_class.yaml b/tests/templates/kuttl/opa/15-auth_class.yaml deleted file mode 100644 index d55ce903..00000000 --- a/tests/templates/kuttl/opa/15-auth_class.yaml +++ /dev/null @@ -1,17 +0,0 @@ ---- -apiVersion: authentication.stackable.tech/v1alpha1 -kind: AuthenticationClass -metadata: - name: simple-nifi-users -spec: - provider: - static: - userCredentialsSecret: - name: simple-nifi-admin-credentials ---- -apiVersion: v1 -kind: Secret -metadata: - name: simple-nifi-admin-credentials -stringData: - admin: supersecretpassword diff --git a/tests/templates/kuttl/opa/19-install-keycloak.yaml.j2 b/tests/templates/kuttl/opa/19-install-keycloak.yaml.j2 deleted file mode 100644 index e567efe1..00000000 --- a/tests/templates/kuttl/opa/19-install-keycloak.yaml.j2 +++ /dev/null @@ -1,165 +0,0 @@ ---- -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: kuttl.dev/v1beta1 -kind: TestStep -commands: - - script: | - kubectl apply -n $NAMESPACE -f - << EOF - --- - apiVersion: secrets.stackable.tech/v1alpha1 - kind: SecretClass - metadata: - name: keycloak-tls-$NAMESPACE - spec: - backend: - autoTls: - ca: - autoGenerate: true - secret: - name: keycloak-tls-ca-$NAMESPACE - namespace: $NAMESPACE - --- - apiVersion: apps/v1 - kind: Deployment - metadata: - name: keycloak - labels: - app: keycloak - spec: - replicas: 1 - selector: - matchLabels: - app: keycloak - template: - metadata: - labels: - app: keycloak - spec: - serviceAccountName: keycloak - containers: - - name: keycloak - image: quay.io/keycloak/keycloak:25.0.0 - args: - - start - - --hostname-strict=false - - --https-key-store-file=/tls/keystore.p12 - - --https-key-store-password=changeit - - --import-realm - env: - - name: KEYCLOAK_ADMIN - value: admin - - name: KEYCLOAK_ADMIN_PASSWORD - valueFrom: - secretKeyRef: - name: keycloak-admin-credentials - key: admin - - name: USER_INFO_FETCHER_CLIENT_ID - valueFrom: - secretKeyRef: - name: user-info-fetcher-client-credentials - key: clientId - - name: USER_INFO_FETCHER_CLIENT_SECRET - valueFrom: - secretKeyRef: - name: user-info-fetcher-client-credentials - key: clientSecret - - name: JAVA_OPTS_KC_HEAP - value: -Xmx500M - ports: - - name: https - containerPort: 8443 - readinessProbe: - httpGet: - scheme: HTTPS - path: /realms/master - port: https - resources: - limits: - cpu: 1 - memory: 1024Mi - requests: - cpu: 500m - memory: 1024Mi - volumeMounts: - - name: data - mountPath: /opt/keycloak/data/ - - name: tls - mountPath: /tls/ - - name: realm-volume - mountPath: /opt/keycloak/data/import - securityContext: - fsGroup: 1000 - runAsGroup: 1000 - runAsUser: 1000 - volumes: - - name: data - emptyDir: {} - - name: tls - ephemeral: - volumeClaimTemplate: - metadata: - annotations: - secrets.stackable.tech/class: keycloak-tls-$NAMESPACE - secrets.stackable.tech/format: tls-pkcs12 - secrets.stackable.tech/format.compatibility.tls-pkcs12.password: changeit - secrets.stackable.tech/scope: service=keycloak,node - spec: - storageClassName: secrets.stackable.tech - accessModes: - - ReadWriteOnce - resources: - requests: - storage: "1" - - name: realm-volume - configMap: - name: keycloak-test-realm - --- - apiVersion: v1 - kind: Secret - metadata: - name: keycloak-admin-credentials - stringData: - admin: "adminadmin" - --- - apiVersion: v1 - kind: Service - metadata: - name: keycloak - labels: - app: keycloak - spec: - ports: - - name: https - port: 8443 - targetPort: 8443 - selector: - app: keycloak - EOF diff --git a/tests/templates/kuttl/opa/19-keycloak-realm-cm.yaml b/tests/templates/kuttl/opa/19-keycloak-realm-cm.yaml deleted file mode 100644 index dcf5cb69..00000000 --- a/tests/templates/kuttl/opa/19-keycloak-realm-cm.yaml +++ /dev/null @@ -1,78 +0,0 @@ ---- -apiVersion: v1 -kind: Secret -metadata: - name: user-info-fetcher-client-credentials -stringData: - clientId: user-info-fetcher - clientSecret: user-info-fetcher-client-secret ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: keycloak-test-realm -data: - realm.json: | - { - "realm" : "test", - "enabled" : true, - "groups" : [ { - "name" : "nifi-admin", - "path" : "/nifi-admin" - } ], - "users" : [ { - "username" : "admin", - "enabled" : true, - "emailVerified" : true, - "firstName" : "admin", - "lastName" : "admin", - "email" : "admin@example.com", - "credentials" : [ { - "type" : "password", - "userLabel" : "My password", - "secretData" : "{\"value\":\"JxIyEshkBUrhZX1BEN9JO8EM3ue5/SnGHDfuyTqOH6A=\",\"salt\":\"f6iCn2rWqZQaRnCCsKAoQQ==\",\"additionalParameters\":{}}", - "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" - } ], - "realmRoles" : [ "default-roles-my-dataspace" ], - "groups" : [ "/nifi-admin" ] - }, { - "username" : "service-account-user-info-fetcher", - "enabled" : true, - "totp" : false, - "emailVerified" : false, - "serviceAccountClientId" : "user-info-fetcher", - "credentials" : [ ], - "disableableCredentialTypes" : [ ], - "requiredActions" : [ ], - "realmRoles" : [ "default-roles-my-dataspace" ], - "clientRoles" : { - "realm-management" : [ - "view-users" - ] - }, - "notBefore" : 0, - "groups" : [ ] - } ], - "clients" : [ { - "clientId" : "${USER_INFO_FETCHER_CLIENT_ID}", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "secret" : "${USER_INFO_FETCHER_CLIENT_SECRET}", - "redirectUris" : [ "/*" ], - "webOrigins" : [ "/*" ], - "notBefore" : 0, - "bearerOnly" : false, - "serviceAccountsEnabled" : true, - "publicClient" : false, - "frontchannelLogout" : true, - "protocol" : "openid-connect", - "attributes" : { - "oidc.ciba.grant.enabled" : "true", - "oauth2.device.authorization.grant.enabled" : "false" - }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : true - } ] - } diff --git a/tests/templates/kuttl/opa/25-opa-rego.yaml b/tests/templates/kuttl/opa/25-opa-rego.yaml deleted file mode 100644 index eff75bc9..00000000 --- a/tests/templates/kuttl/opa/25-opa-rego.yaml +++ /dev/null @@ -1,49 +0,0 @@ ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: opa-nifi-uif-rego - labels: - opa.stackable.tech/bundle: "true" -data: - nifi.rego: | - package nifi - - # Setting "resourceNotFound" to true results in the parent resource being evaluated for authorization, e.g. the parent of a processor is the processor-group. - # If a resource is matched by a rego rule that is not the default the parent resource will be ignored. - default allow := { - "resourceNotFound": true, - "dumpCache": true - } - - allow := { - "allowed": true, - "dumpCache": true - } if { - input.resource.id in ["/flow", "/controller", "/parameter-contexts", "/provenance-data", "/restricted-components", "/policies", "/tenants", "/site-to-site", "/system", "counters"] - nifi_admin - } - - allow := { - "allowed": true, - "dumpCache": true - } if { - input.resource.name == "NiFi Flow" - startswith(input.resource.id, "/process-groups") - nifi_admin - } - - allow := { - "allowed": true, - "dumpCache": true - } if { - input.identity.name == "CN=generated certificate for pod" - input.resource.id == "/proxy" - } - - nifi_admin := true if { - group_paths := data.stackable.opa.userinfo.v1.userInfoByUsername(input.identity.name).groups - roles := [ trim(group,"/") | group := group_paths[_] ] - some i - roles[i] == "nifi-admin" - } diff --git a/tests/templates/kuttl/opa/40-create-configmap.yaml.j2 b/tests/templates/kuttl/opa/40-create-configmap.yaml.j2 deleted file mode 100644 index 642c5468..00000000 --- a/tests/templates/kuttl/opa/40-create-configmap.yaml.j2 +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: kuttl.dev/v1beta1 -kind: TestStep -commands: - - script: kubectl create cm test-nifi -n $NAMESPACE --from-file=test_nifi.py diff --git a/tests/templates/kuttl/opa/test_nifi.py b/tests/templates/kuttl/opa/test_nifi.py deleted file mode 100755 index 283b1a3b..00000000 --- a/tests/templates/kuttl/opa/test_nifi.py +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env python -import requests -import json -import argparse -import urllib3 -from time import sleep - - -def get_token(nifi_host, username, password): - nifi_headers = { - 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', - } - data = {'username': username, 'password': password} - - # TODO: handle actual errors when connecting properly - nifi_url = nifi_host + '/nifi-api/access/token' - response = requests.post(nifi_url, headers=nifi_headers, data=data, verify=False) # , cert='./tmp/cacert.pem') - - if response.ok: - nifi_token = response.content.decode('utf-8') - return "Bearer " + nifi_token - else: - print(f"Failed to get token: {response.status_code}: {response.content}") - exit(-1) - - -if __name__ == '__main__': - # Construct an argument parser - all_args = argparse.ArgumentParser() - - # Add arguments to the parser - all_args.add_argument("-u", "--user", required=True, - help="Username to connect as") - all_args.add_argument("-p", "--password", required=True, - help="Password for the user") - all_args.add_argument("-n", "--namespace", required=True, - help="Namespace the test is running in") - all_args.add_argument("-c", "--count", required=True, - help="The expected number of Nodes") - args = vars(all_args.parse_args()) - - # disable warnings as we have specified non-verified https connections - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - - host = f"https://test-nifi-node-default-1.test-nifi-node-default.{args['namespace']}.svc.cluster.local:8443" - token = get_token(host, args['user'], args['password']) - headers = {'Authorization': token} - node_count = int(args['count']) - - x = 0 - while x < 15: - url = host + '/nifi-api/controller/cluster' - cluster = requests.get(url, headers=headers, verify=False) # , cert='/tmp/cacert.pem') - if cluster.status_code != 200: - print("Waiting for cluster...") - else: - cluster_data = json.loads(cluster.content.decode('utf-8')) - nodes = cluster_data['cluster']['nodes'] - if len(nodes) != node_count: - print(f"Cluster should have {node_count} nodes at this stage, but has: {len(nodes)}") - else: - connected = True - for node in nodes: - if node['status'] != "CONNECTED": - print(f"Node {node['nodeId']} is in state {node['status']} but should have been CONNECTED") - connected = False - if connected: - print("Test succeeded!") - exit(0) - print("Retrying...") - x+=1 - sleep(10) - - - - print("Test failed") - exit(-1) diff --git a/tests/test-definition.yaml b/tests/test-definition.yaml index f29d5d35..c8207831 100644 --- a/tests/test-definition.yaml +++ b/tests/test-definition.yaml @@ -2,12 +2,12 @@ dimensions: - name: nifi values: - - 1.27.0 - - 1.28.1 - - 2.2.0 + # - 1.27.0 + # - 1.28.1 + # - 2.2.0 # 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. - # - 2.2.0,oci.stackable.tech/sandbox/nifi:2.2.0-stackable0.0.0-dev + - 2.2.0,oci.stackable.tech/sdp/nifi:2.2.0-stackable0.0.0-dev-opa - name: nifi_old values: - 1.27.0 @@ -38,7 +38,7 @@ dimensions: - "true" - name: oidc-use-tls values: - - "false" + # - "false" - "true" - name: openshift values: @@ -49,54 +49,55 @@ 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: oidc + # - 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 + # - zookeeper-latest + # - oidc-use-tls + # - openshift + - name: oidc-opa dimensions: - nifi - zookeeper-latest - oidc-use-tls - - openshift - - name: opa - dimensions: - - nifi - - zookeeper-latest - opa-latest - openshift suites: From 46fc5117291de31f4692709540d75af8fc2061b0 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Thu, 15 May 2025 10:04:37 +0200 Subject: [PATCH 15/31] add retries to nifi calls during test --- tests/templates/kuttl/oidc-opa/test.py | 139 +++++++++++++------------ 1 file changed, 75 insertions(+), 64 deletions(-) diff --git a/tests/templates/kuttl/oidc-opa/test.py b/tests/templates/kuttl/oidc-opa/test.py index 3f6ea777..bf8d93fe 100644 --- a/tests/templates/kuttl/oidc-opa/test.py +++ b/tests/templates/kuttl/oidc-opa/test.py @@ -2,6 +2,7 @@ import os import requests import sys +import time import json from bs4 import BeautifulSoup @@ -68,96 +69,114 @@ def login(session: requests.Session, username: str, password: str): assert welcome_page.url == f"https://{nifi}:8443/nifi/", ( "Redirection to the NiFi web UI expected" ) + print(f"logged in as {username}") + + +def get_process_group_a(session: requests.Session) -> requests.Response: + return get_resource_with_retries( + session, "/flow/process-groups/c9186a05-0196-1000-ffff-ffffd8474359" + ) + + +def get_process_group_b(session: requests.Session) -> requests.Response: + return get_resource_with_retries( + session, "/flow/process-groups/7e08561b-447d-3acb-b510-744d886c3ca4" + ) + + +def get_processor_e(session: requests.Session) -> requests.Response: + return get_resource_with_retries( + session, "/processors/9d95cac3-2759-3fce-9c07-71215b0fb554" + ) + + +def get_counters(session: requests.Session) -> requests.Response: + return get_resource_with_retries(session, "/counters") + + +def get_resource_with_retries( + session: requests.Session, resource: str +) -> requests.Response: + retries = 0 + max_retries = 5 + while True: + time.sleep(retries ^ 2) + response = session.get( + f"https://{nifi}:8443/nifi-api{resource}?uiOnly=true", + verify=False, + ) + # Occasionally NiFi will respond with an 409 http error + if response.status_code == 409 and retries <= max_retries: + print("NiFi returned HTTP 409") + retries += 1 + else: + return response # alice session = requests.Session() login(session, "alice", "alice") -process_group_a = session.get( - f"https://{nifi}:8443/nifi-api/flow/process-groups/c9186a05-0196-1000-ffff-ffffd8474359?uiOnly=true", - verify=False, -) + +process_group_a = get_process_group_a(session) assert process_group_a.json()["permissions"]["canRead"], ( "Alice should be able to access process group A" ) -process_group_b = session.get( - f"https://{nifi}:8443/nifi-api/flow/process-groups/7e08561b-447d-3acb-b510-744d886c3ca4?uiOnly=true", - verify=False, -) +process_group_b = get_process_group_b(session) assert not process_group_b.json()["permissions"]["canRead"], ( "Alice should not be able to access process group B" ) -processor_e = session.get( - f"https://{nifi}:8443/nifi-api/processors/9d95cac3-2759-3fce-9c07-71215b0fb554?uiOnly=true", - verify=False, -) +processor_e = get_processor_e(session) assert processor_e.ok, "Alice should be able to access a processor E in process group C" -counters = session.get( - f"https://{nifi}:8443/nifi-api/counters", - verify=False, -) + +counters = get_counters(session) assert not counters.ok, ( "Alice should not be able to access the global resource 'counters'" ) + # bob session = requests.Session() login(session, "bob", "bob") -process_group_a = session.get( - f"https://{nifi}:8443/nifi-api/flow/process-groups/c9186a05-0196-1000-ffff-ffffd8474359?uiOnly=true", - verify=False, -) + +process_group_a = get_process_group_a(session) assert not process_group_a.json()["permissions"]["canRead"], ( "Bob should not be able to access process group A" ) -process_group_b = session.get( - f"https://{nifi}:8443/nifi-api/flow/process-groups/7e08561b-447d-3acb-b510-744d886c3ca4?uiOnly=true", - verify=False, -) + +process_group_b = get_process_group_b(session) assert process_group_b.json()["permissions"]["canRead"], ( "Bob should be able to access process group B" ) -processor_e = session.get( - f"https://{nifi}:8443/nifi-api/processors/9d95cac3-2759-3fce-9c07-71215b0fb554?uiOnly=true", - verify=False, -) + +processor_e = get_processor_e(session) assert processor_e.ok, "Bob should be able to access a processor E in process group C" -counters = session.get( - f"https://{nifi}:8443/nifi-api/counters", - verify=False, -) + +counters = get_counters(session) assert not counters.ok, ( "Bob should not be able to access the global resource 'counters'" ) + # charlie session = requests.Session() login(session, "charlie", "charlie") -process_group_a = session.get( - f"https://{nifi}:8443/nifi-api/flow/process-groups/c9186a05-0196-1000-ffff-ffffd8474359?uiOnly=true", - verify=False, -) + +process_group_a = get_process_group_a(session) assert not process_group_a.json()["permissions"]["canRead"], ( "Charlie should not be able to access process group A" ) -process_group_b = session.get( - f"https://{nifi}:8443/nifi-api/flow/process-groups/7e08561b-447d-3acb-b510-744d886c3ca4?uiOnly=true", - verify=False, -) + +process_group_b = get_process_group_b(session) assert not process_group_b.json()["permissions"]["canRead"], ( "Charlie should not be able to access process group B" ) -processor_e = session.get( - f"https://{nifi}:8443/nifi-api/processors/9d95cac3-2759-3fce-9c07-71215b0fb554?uiOnly=true", - verify=False, -) + +processor_e = get_processor_e(session) assert processor_e.ok, ( "Charlie should be able to access a processor E in process group C" ) -counters = session.get( - f"https://{nifi}:8443/nifi-api/counters", - verify=False, -) + +counters = get_counters(session) assert not counters.ok, ( "Charlie should not be able to access the global resource 'counters'" ) @@ -165,29 +184,21 @@ def login(session: requests.Session, username: str, password: str): # nifi-admin session = requests.Session() login(session, "nifi-admin", "nifi-admin") -process_group_a = session.get( - f"https://{nifi}:8443/nifi-api/flow/process-groups/c9186a05-0196-1000-ffff-ffffd8474359?uiOnly=true", - verify=False, -) + +process_group_a = get_process_group_a(session) assert process_group_a.json()["permissions"]["canRead"], ( "Nifi-admin should be able to access process group A" ) -process_group_b = session.get( - f"https://{nifi}:8443/nifi-api/flow/process-groups/7e08561b-447d-3acb-b510-744d886c3ca4?uiOnly=true", - verify=False, -) + +process_group_b = get_process_group_b(session) assert process_group_b.json()["permissions"]["canRead"], ( "Nifi-admin should be able to access process group B" ) -processor_e = session.get( - f"https://{nifi}:8443/nifi-api/processors/9d95cac3-2759-3fce-9c07-71215b0fb554?uiOnly=true", - verify=False, -) + +processor_e = get_processor_e(session) assert processor_e.ok, ( "Nifi-admin should be able to access a processor E in process group C" ) -counters = session.get( - f"https://{nifi}:8443/nifi-api/counters", - verify=False, -) + +counters = get_counters(session) assert counters.ok, "Nifi-admin should be able to access the global resource 'counters'" From 8f0a02f8265bcfda55a6a6d62edde0b1b8d03beb Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Thu, 15 May 2025 11:16:38 +0200 Subject: [PATCH 16/31] simplify rego rules --- .../templates/kuttl/oidc-opa/25-opa-rego.yaml | 48 +++++++------------ tests/templates/kuttl/oidc-opa/test.py | 28 ++++++++--- 2 files changed, 39 insertions(+), 37 deletions(-) diff --git a/tests/templates/kuttl/oidc-opa/25-opa-rego.yaml b/tests/templates/kuttl/oidc-opa/25-opa-rego.yaml index 2b44141d..43f58fd6 100644 --- a/tests/templates/kuttl/oidc-opa/25-opa-rego.yaml +++ b/tests/templates/kuttl/oidc-opa/25-opa-rego.yaml @@ -16,63 +16,49 @@ data: "resourceNotFound": true } - # Allow access nifi-admin for all resource types that don't have other policies in place - # This convoluted way of writing the rule is necessary because a general allow rule for nifi-admin would result in - # an the rego error "complete rules must not produce multiple outputs" + # The nifi-admin has access to everything allow := { "allowed": true, } if { - resource_types_with_other_policies = [ - "/flow", - "/process-groups", - "/processors" - ] - every resource_type in resource_types_with_other_policies { not startswith(input.resource.id, resource_type) } - groups_with_access := ["nifi-admin"] some group in user_groups - group in groups_with_access + group == "nifi-admin" } - # Access to the UI for every user + # Every user in the group "nifi-user" has access to the UI allow := { "allowed": true, "dumpCache": true } if { input.resource.id in ["/flow"] - groups_with_access := ["nifi-admin", "nifi-user"] some group in user_groups - group in groups_with_access + group == "nifi-user" } - # Access to the root process group for every user + # Allow access to process groups A & B only to users in the corresponding group allow := { "allowed": true, - "dumpCache": true } if { - input.resource.name == "NiFi Flow" - startswith(input.resource.id, "/process-groups") - groups_with_access := ["nifi-admin", "nifi-user"] + input.resource.id == "/process-groups/c9186a05-0196-1000-ffff-ffffd8474359" some group in user_groups - group in groups_with_access + group in ["nifi-process-group-a"] } - # Allow access to process groups A & B only to users in the corresponding group & admin - # Rules have to explicitly deny access because every user in group "nifi-user" - # has inherited access to each process group for which they are not explicitly denied. allow := { - "allowed": false, + "allowed": true } if { - input.resource.id == "/process-groups/c9186a05-0196-1000-ffff-ffffd8474359" - groups_with_access := ["nifi-admin", "nifi-process-group-a"] - every group in user_groups { not group in groups_with_access } + input.resource.id == "/process-groups/7e08561b-447d-3acb-b510-744d886c3ca4" + some group in user_groups + group in ["nifi-process-group-b"] } + # Allow read-only access to group "nifi-user" to proccess group C allow := { - "allowed": false + "allowed": true } if { - input.resource.id == "/process-groups/7e08561b-447d-3acb-b510-744d886c3ca4" - groups_with_access := ["nifi-admin", "nifi-process-group-b"] - every group in user_groups { not group in groups_with_access } + input.resource.id == "/process-groups/f3b78341-b2ff-3ae3-b7fc-4b518617c802" + input.action.name == "read" + some group in user_groups + group == "nifi-user" } allow := { diff --git a/tests/templates/kuttl/oidc-opa/test.py b/tests/templates/kuttl/oidc-opa/test.py index bf8d93fe..6d28ad2f 100644 --- a/tests/templates/kuttl/oidc-opa/test.py +++ b/tests/templates/kuttl/oidc-opa/test.py @@ -126,7 +126,12 @@ def get_resource_with_retries( "Alice should not be able to access process group B" ) processor_e = get_processor_e(session) -assert processor_e.ok, "Alice should be able to access a processor E in process group C" +assert processor_e.json()["permissions"]["canRead"], ( + "Alice should be able to read processor E in process group C" +) +assert not processor_e.json()["permissions"]["canWrite"], ( + "Alice should not be able to write to processor E in process group C" +) counters = get_counters(session) assert not counters.ok, ( @@ -149,7 +154,12 @@ def get_resource_with_retries( ) processor_e = get_processor_e(session) -assert processor_e.ok, "Bob should be able to access a processor E in process group C" +assert processor_e.json()["permissions"]["canRead"], ( + "Bob should be able to read processor E in process group C" +) +assert not processor_e.json()["permissions"]["canWrite"], ( + "Bob should not be able to write to processor E in process group C" +) counters = get_counters(session) assert not counters.ok, ( @@ -172,8 +182,11 @@ def get_resource_with_retries( ) processor_e = get_processor_e(session) -assert processor_e.ok, ( - "Charlie should be able to access a processor E in process group C" +assert processor_e.json()["permissions"]["canRead"], ( + "Charlie should be able to read processor E in process group C" +) +assert not processor_e.json()["permissions"]["canWrite"], ( + "Charlie should not be able to write to processor E in process group C" ) counters = get_counters(session) @@ -196,8 +209,11 @@ def get_resource_with_retries( ) processor_e = get_processor_e(session) -assert processor_e.ok, ( - "Nifi-admin should be able to access a processor E in process group C" +assert processor_e.json()["permissions"]["canRead"], ( + "Nifi-admin should be able to read processor E in process group C" +) +assert processor_e.json()["permissions"]["canWrite"], ( + "Nifi-admin should be able to write to processor E in process group C" ) counters = get_counters(session) From ca61f9249cc70e0db3085bc1cf7660a00c91388a Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Thu, 15 May 2025 13:41:21 +0200 Subject: [PATCH 17/31] move nifi flow copying into init container --- .../kuttl/oidc-opa/30-nifi-flow-cm.yaml | 388 ++++++++++++++++++ .../templates/kuttl/oidc-opa/30_nifi.yaml.j2 | 21 + tests/templates/kuttl/oidc-opa/31-assert.yaml | 12 - .../kuttl/oidc-opa/31-create-flow.yaml.j2 | 10 - tests/templates/kuttl/oidc-opa/31_flow.json | 382 ----------------- 5 files changed, 409 insertions(+), 404 deletions(-) create mode 100644 tests/templates/kuttl/oidc-opa/30-nifi-flow-cm.yaml delete mode 100644 tests/templates/kuttl/oidc-opa/31-assert.yaml delete mode 100644 tests/templates/kuttl/oidc-opa/31-create-flow.yaml.j2 delete mode 100644 tests/templates/kuttl/oidc-opa/31_flow.json diff --git a/tests/templates/kuttl/oidc-opa/30-nifi-flow-cm.yaml b/tests/templates/kuttl/oidc-opa/30-nifi-flow-cm.yaml new file mode 100644 index 00000000..87824f63 --- /dev/null +++ b/tests/templates/kuttl/oidc-opa/30-nifi-flow-cm.yaml @@ -0,0 +1,388 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: nifi-flow-json +data: + flow.json: | + { + "encodingVersion": { + "majorVersion": 2, + "minorVersion": 0 + }, + "maxTimerDrivenThreadCount": 10, + "registries": [], + "parameterContexts": [], + "parameterProviders": [], + "controllerServices": [], + "reportingTasks": [], + "flowAnalysisRules": [], + "rootGroup": { + "identifier": "96d9abc7-e736-324d-9da8-1c0a4e671508", + "instanceIdentifier": "c377901a-0196-1000-d1e8-b549997e4d94", + "name": "NiFi Flow", + "comments": "", + "position": { + "x": 0, + "y": 0 + }, + "processGroups": [ + { + "identifier": "0ad13e3a-a773-380d-0000-000031bd1ecb", + "instanceIdentifier": "7e08561b-447d-3acb-b510-744d886c3ca4", + "name": "Process Group B", + "comments": "", + "position": { + "x": 248, + "y": -112 + }, + "processGroups": [], + "remoteProcessGroups": [], + "processors": [ + { + "identifier": "347682fa-61d4-31ae-0000-000031bd1ecb", + "instanceIdentifier": "351336f0-903f-362c-b479-c06040537e68", + "name": "GenerateFlowFile C", + "comments": "", + "position": { + "x": -1032, + "y": -520 + }, + "type": "org.apache.nifi.processors.standard.GenerateFlowFile", + "bundle": { + "group": "org.apache.nifi", + "artifact": "nifi-standard-nar", + "version": "2.2.0" + }, + "properties": { + "character-set": "UTF-8", + "File Size": "0B", + "Batch Size": "1", + "Unique FlowFiles": "false", + "Data Format": "Text" + }, + "propertyDescriptors": {}, + "style": {}, + "schedulingPeriod": "1 min", + "schedulingStrategy": "TIMER_DRIVEN", + "executionNode": "ALL", + "penaltyDuration": "30 sec", + "yieldDuration": "1 sec", + "bulletinLevel": "WARN", + "runDurationMillis": 0, + "concurrentlySchedulableTaskCount": 1, + "autoTerminatedRelationships": [], + "scheduledState": "ENABLED", + "retryCount": 10, + "retriedRelationships": [], + "backoffMechanism": "PENALIZE_FLOWFILE", + "maxBackoffPeriod": "10 mins", + "componentType": "PROCESSOR", + "groupIdentifier": "0ad13e3a-a773-380d-0000-000031bd1ecb" + }, + { + "identifier": "f7b6da7b-daa3-3b68-0000-000031bd1ecb", + "instanceIdentifier": "1ed101fd-f976-38ad-8137-45dc0c7b6e6e", + "name": "GenerateFlowFile D", + "comments": "", + "position": { + "x": -648, + "y": -520 + }, + "type": "org.apache.nifi.processors.standard.GenerateFlowFile", + "bundle": { + "group": "org.apache.nifi", + "artifact": "nifi-standard-nar", + "version": "2.2.0" + }, + "properties": { + "character-set": "UTF-8", + "File Size": "0B", + "Batch Size": "1", + "Unique FlowFiles": "false", + "Data Format": "Text" + }, + "propertyDescriptors": {}, + "style": {}, + "schedulingPeriod": "1 min", + "schedulingStrategy": "TIMER_DRIVEN", + "executionNode": "ALL", + "penaltyDuration": "30 sec", + "yieldDuration": "1 sec", + "bulletinLevel": "WARN", + "runDurationMillis": 0, + "concurrentlySchedulableTaskCount": 1, + "autoTerminatedRelationships": [], + "scheduledState": "ENABLED", + "retryCount": 10, + "retriedRelationships": [], + "backoffMechanism": "PENALIZE_FLOWFILE", + "maxBackoffPeriod": "10 mins", + "componentType": "PROCESSOR", + "groupIdentifier": "0ad13e3a-a773-380d-0000-000031bd1ecb" + } + ], + "inputPorts": [], + "outputPorts": [], + "connections": [], + "labels": [], + "funnels": [], + "controllerServices": [], + "defaultFlowFileExpiration": "0 sec", + "defaultBackPressureObjectThreshold": 10000, + "defaultBackPressureDataSizeThreshold": "1 GB", + "scheduledState": "ENABLED", + "executionEngine": "INHERITED", + "maxConcurrentTasks": 1, + "statelessFlowTimeout": "1 min", + "flowFileConcurrency": "UNBOUNDED", + "flowFileOutboundPolicy": "STREAM_WHEN_AVAILABLE", + "componentType": "PROCESS_GROUP", + "groupIdentifier": "96d9abc7-e736-324d-9da8-1c0a4e671508" + }, + { + "identifier": "8f62d3e7-2a0b-3ba8-936c-8a0cb425284a", + "instanceIdentifier": "c9186a05-0196-1000-ffff-ffffd8474359", + "name": "Process Group A", + "comments": "", + "position": { + "x": -190, + "y": -112.91667938232422 + }, + "processGroups": [], + "remoteProcessGroups": [], + "processors": [ + { + "identifier": "e14260ce-23cd-361d-0000-00001b31a7fc", + "instanceIdentifier": "63ad5386-61ce-327a-bce3-5b597829aa54", + "name": "GenerateFlowFile B", + "comments": "", + "position": { + "x": -648, + "y": -520 + }, + "type": "org.apache.nifi.processors.standard.GenerateFlowFile", + "bundle": { + "group": "org.apache.nifi", + "artifact": "nifi-standard-nar", + "version": "2.2.0" + }, + "properties": { + "character-set": "UTF-8", + "File Size": "0B", + "Batch Size": "1", + "Unique FlowFiles": "false", + "Data Format": "Text" + }, + "propertyDescriptors": {}, + "style": {}, + "schedulingPeriod": "1 min", + "schedulingStrategy": "TIMER_DRIVEN", + "executionNode": "ALL", + "penaltyDuration": "30 sec", + "yieldDuration": "1 sec", + "bulletinLevel": "WARN", + "runDurationMillis": 0, + "concurrentlySchedulableTaskCount": 1, + "autoTerminatedRelationships": [], + "scheduledState": "ENABLED", + "retryCount": 10, + "retriedRelationships": [], + "backoffMechanism": "PENALIZE_FLOWFILE", + "maxBackoffPeriod": "10 mins", + "componentType": "PROCESSOR", + "groupIdentifier": "8f62d3e7-2a0b-3ba8-936c-8a0cb425284a" + }, + { + "identifier": "4623a22e-5c14-310b-afb8-c82965f74a7d", + "instanceIdentifier": "c9196a9e-0196-1000-0000-0000418515a4", + "name": "GenerateFlowFile A", + "comments": "", + "position": { + "x": -1032, + "y": -520 + }, + "type": "org.apache.nifi.processors.standard.GenerateFlowFile", + "bundle": { + "group": "org.apache.nifi", + "artifact": "nifi-standard-nar", + "version": "2.2.0" + }, + "properties": { + "character-set": "UTF-8", + "File Size": "0B", + "Batch Size": "1", + "Unique FlowFiles": "false", + "Data Format": "Text" + }, + "propertyDescriptors": {}, + "style": {}, + "schedulingPeriod": "1 min", + "schedulingStrategy": "TIMER_DRIVEN", + "executionNode": "ALL", + "penaltyDuration": "30 sec", + "yieldDuration": "1 sec", + "bulletinLevel": "WARN", + "runDurationMillis": 0, + "concurrentlySchedulableTaskCount": 1, + "autoTerminatedRelationships": [], + "scheduledState": "ENABLED", + "retryCount": 10, + "retriedRelationships": [], + "backoffMechanism": "PENALIZE_FLOWFILE", + "maxBackoffPeriod": "10 mins", + "componentType": "PROCESSOR", + "groupIdentifier": "8f62d3e7-2a0b-3ba8-936c-8a0cb425284a" + } + ], + "inputPorts": [], + "outputPorts": [], + "connections": [], + "labels": [], + "funnels": [], + "controllerServices": [], + "defaultFlowFileExpiration": "0 sec", + "defaultBackPressureObjectThreshold": 10000, + "defaultBackPressureDataSizeThreshold": "1 GB", + "scheduledState": "ENABLED", + "executionEngine": "INHERITED", + "maxConcurrentTasks": 1, + "statelessFlowTimeout": "1 min", + "flowFileConcurrency": "UNBOUNDED", + "flowFileOutboundPolicy": "STREAM_WHEN_AVAILABLE", + "componentType": "PROCESS_GROUP", + "groupIdentifier": "96d9abc7-e736-324d-9da8-1c0a4e671508" + }, + { + "identifier": "82533e93-7494-352d-ffff-ffffaad75c48", + "instanceIdentifier": "f3b78341-b2ff-3ae3-b7fc-4b518617c802", + "name": "Process Group C", + "comments": "", + "position": { + "x": 696, + "y": -112 + }, + "processGroups": [], + "remoteProcessGroups": [], + "processors": [ + { + "identifier": "fcd0595d-bf18-34be-ffff-ffffaad75c48", + "instanceIdentifier": "9d95cac3-2759-3fce-9c07-71215b0fb554", + "name": "GenerateFlowFile E", + "comments": "", + "position": { + "x": -1032, + "y": -520 + }, + "type": "org.apache.nifi.processors.standard.GenerateFlowFile", + "bundle": { + "group": "org.apache.nifi", + "artifact": "nifi-standard-nar", + "version": "2.2.0" + }, + "properties": { + "character-set": "UTF-8", + "File Size": "0B", + "Batch Size": "1", + "Unique FlowFiles": "false", + "Data Format": "Text" + }, + "propertyDescriptors": {}, + "style": {}, + "schedulingPeriod": "1 min", + "schedulingStrategy": "TIMER_DRIVEN", + "executionNode": "ALL", + "penaltyDuration": "30 sec", + "yieldDuration": "1 sec", + "bulletinLevel": "WARN", + "runDurationMillis": 0, + "concurrentlySchedulableTaskCount": 1, + "autoTerminatedRelationships": [], + "scheduledState": "ENABLED", + "retryCount": 10, + "retriedRelationships": [], + "backoffMechanism": "PENALIZE_FLOWFILE", + "maxBackoffPeriod": "10 mins", + "componentType": "PROCESSOR", + "groupIdentifier": "82533e93-7494-352d-ffff-ffffaad75c48" + }, + { + "identifier": "077fd08e-4c7c-39cc-ffff-ffffaad75c48", + "instanceIdentifier": "63d8f319-893c-38d4-9cd4-35cea3f43ea2", + "name": "GenerateFlowFile F", + "comments": "", + "position": { + "x": -648, + "y": -520 + }, + "type": "org.apache.nifi.processors.standard.GenerateFlowFile", + "bundle": { + "group": "org.apache.nifi", + "artifact": "nifi-standard-nar", + "version": "2.2.0" + }, + "properties": { + "character-set": "UTF-8", + "File Size": "0B", + "Batch Size": "1", + "Unique FlowFiles": "false", + "Data Format": "Text" + }, + "propertyDescriptors": {}, + "style": {}, + "schedulingPeriod": "1 min", + "schedulingStrategy": "TIMER_DRIVEN", + "executionNode": "ALL", + "penaltyDuration": "30 sec", + "yieldDuration": "1 sec", + "bulletinLevel": "WARN", + "runDurationMillis": 0, + "concurrentlySchedulableTaskCount": 1, + "autoTerminatedRelationships": [], + "scheduledState": "ENABLED", + "retryCount": 10, + "retriedRelationships": [], + "backoffMechanism": "PENALIZE_FLOWFILE", + "maxBackoffPeriod": "10 mins", + "componentType": "PROCESSOR", + "groupIdentifier": "82533e93-7494-352d-ffff-ffffaad75c48" + } + ], + "inputPorts": [], + "outputPorts": [], + "connections": [], + "labels": [], + "funnels": [], + "controllerServices": [], + "defaultFlowFileExpiration": "0 sec", + "defaultBackPressureObjectThreshold": 10000, + "defaultBackPressureDataSizeThreshold": "1 GB", + "scheduledState": "ENABLED", + "executionEngine": "INHERITED", + "maxConcurrentTasks": 1, + "statelessFlowTimeout": "1 min", + "flowFileConcurrency": "UNBOUNDED", + "flowFileOutboundPolicy": "STREAM_WHEN_AVAILABLE", + "componentType": "PROCESS_GROUP", + "groupIdentifier": "96d9abc7-e736-324d-9da8-1c0a4e671508" + } + ], + "remoteProcessGroups": [], + "processors": [], + "inputPorts": [], + "outputPorts": [], + "connections": [], + "labels": [], + "funnels": [], + "controllerServices": [], + "defaultFlowFileExpiration": "0 sec", + "defaultBackPressureObjectThreshold": 10000, + "defaultBackPressureDataSizeThreshold": "1 GB", + "scheduledState": "ENABLED", + "executionEngine": "INHERITED", + "maxConcurrentTasks": 1, + "statelessFlowTimeout": "1 min", + "flowFileConcurrency": "UNBOUNDED", + "flowFileOutboundPolicy": "STREAM_WHEN_AVAILABLE", + "componentType": "PROCESS_GROUP" + } + } diff --git a/tests/templates/kuttl/oidc-opa/30_nifi.yaml.j2 b/tests/templates/kuttl/oidc-opa/30_nifi.yaml.j2 index 27fb4ff1..87f7ffd7 100644 --- a/tests/templates/kuttl/oidc-opa/30_nifi.yaml.j2 +++ b/tests/templates/kuttl/oidc-opa/30_nifi.yaml.j2 @@ -43,6 +43,27 @@ spec: logging: enableVectorAgent: {{ lookup('env', 'VECTOR_AGGREGATOR') | length > 0 }} gracefulShutdownTimeout: 1s # let the tests run faster + podOverrides: + spec: + initContainers: + - name: copy-nifi-flow + image: oci.stackable.tech/sdp/testing-tools:0.2.0-stackable0.0.0-dev + command: + - /bin/bash + - -c + - -euo + - pipefail + args: + - cp /tmp/flow.json /stackable/data/database/flow.json && gzip /stackable/data/database/flow.json + volumeMounts: + - mountPath: /stackable/data/database + name: database-repository + - mountPath: /tmp + name: nifi-flow-json + volumes: + - configMap: + name: nifi-flow-json + name: nifi-flow-json roleGroups: default: config: {} diff --git a/tests/templates/kuttl/oidc-opa/31-assert.yaml b/tests/templates/kuttl/oidc-opa/31-assert.yaml deleted file mode 100644 index 2f03b6b1..00000000 --- a/tests/templates/kuttl/oidc-opa/31-assert.yaml +++ /dev/null @@ -1,12 +0,0 @@ ---- -apiVersion: kuttl.dev/v1beta1 -kind: TestAssert -timeout: 1200 ---- -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: test-nifi-node-default -status: - readyReplicas: 1 - replicas: 1 diff --git a/tests/templates/kuttl/oidc-opa/31-create-flow.yaml.j2 b/tests/templates/kuttl/oidc-opa/31-create-flow.yaml.j2 deleted file mode 100644 index 0430f267..00000000 --- a/tests/templates/kuttl/oidc-opa/31-create-flow.yaml.j2 +++ /dev/null @@ -1,10 +0,0 @@ ---- -apiVersion: kuttl.dev/v1beta1 -kind: TestStep -commands: - - script: | - gzip --keep 31_flow.json - kubectl cp -n $NAMESPACE -c nifi ./31_flow.json.gz test-nifi-node-default-0:/stackable/data/database/flow.json.gz - rm 31_flow.json.gz - # Delete Pod to trigger NiFi to load the replaced flow.json.gz - kubectl -n $NAMESPACE delete pod test-nifi-node-default-0 diff --git a/tests/templates/kuttl/oidc-opa/31_flow.json b/tests/templates/kuttl/oidc-opa/31_flow.json deleted file mode 100644 index 4a2691c5..00000000 --- a/tests/templates/kuttl/oidc-opa/31_flow.json +++ /dev/null @@ -1,382 +0,0 @@ -{ - "encodingVersion": { - "majorVersion": 2, - "minorVersion": 0 - }, - "maxTimerDrivenThreadCount": 10, - "registries": [], - "parameterContexts": [], - "parameterProviders": [], - "controllerServices": [], - "reportingTasks": [], - "flowAnalysisRules": [], - "rootGroup": { - "identifier": "96d9abc7-e736-324d-9da8-1c0a4e671508", - "instanceIdentifier": "c377901a-0196-1000-d1e8-b549997e4d94", - "name": "NiFi Flow", - "comments": "", - "position": { - "x": 0, - "y": 0 - }, - "processGroups": [ - { - "identifier": "0ad13e3a-a773-380d-0000-000031bd1ecb", - "instanceIdentifier": "7e08561b-447d-3acb-b510-744d886c3ca4", - "name": "Process Group B", - "comments": "", - "position": { - "x": 248, - "y": -112 - }, - "processGroups": [], - "remoteProcessGroups": [], - "processors": [ - { - "identifier": "347682fa-61d4-31ae-0000-000031bd1ecb", - "instanceIdentifier": "351336f0-903f-362c-b479-c06040537e68", - "name": "GenerateFlowFile C", - "comments": "", - "position": { - "x": -1032, - "y": -520 - }, - "type": "org.apache.nifi.processors.standard.GenerateFlowFile", - "bundle": { - "group": "org.apache.nifi", - "artifact": "nifi-standard-nar", - "version": "2.2.0" - }, - "properties": { - "character-set": "UTF-8", - "File Size": "0B", - "Batch Size": "1", - "Unique FlowFiles": "false", - "Data Format": "Text" - }, - "propertyDescriptors": {}, - "style": {}, - "schedulingPeriod": "1 min", - "schedulingStrategy": "TIMER_DRIVEN", - "executionNode": "ALL", - "penaltyDuration": "30 sec", - "yieldDuration": "1 sec", - "bulletinLevel": "WARN", - "runDurationMillis": 0, - "concurrentlySchedulableTaskCount": 1, - "autoTerminatedRelationships": [], - "scheduledState": "ENABLED", - "retryCount": 10, - "retriedRelationships": [], - "backoffMechanism": "PENALIZE_FLOWFILE", - "maxBackoffPeriod": "10 mins", - "componentType": "PROCESSOR", - "groupIdentifier": "0ad13e3a-a773-380d-0000-000031bd1ecb" - }, - { - "identifier": "f7b6da7b-daa3-3b68-0000-000031bd1ecb", - "instanceIdentifier": "1ed101fd-f976-38ad-8137-45dc0c7b6e6e", - "name": "GenerateFlowFile D", - "comments": "", - "position": { - "x": -648, - "y": -520 - }, - "type": "org.apache.nifi.processors.standard.GenerateFlowFile", - "bundle": { - "group": "org.apache.nifi", - "artifact": "nifi-standard-nar", - "version": "2.2.0" - }, - "properties": { - "character-set": "UTF-8", - "File Size": "0B", - "Batch Size": "1", - "Unique FlowFiles": "false", - "Data Format": "Text" - }, - "propertyDescriptors": {}, - "style": {}, - "schedulingPeriod": "1 min", - "schedulingStrategy": "TIMER_DRIVEN", - "executionNode": "ALL", - "penaltyDuration": "30 sec", - "yieldDuration": "1 sec", - "bulletinLevel": "WARN", - "runDurationMillis": 0, - "concurrentlySchedulableTaskCount": 1, - "autoTerminatedRelationships": [], - "scheduledState": "ENABLED", - "retryCount": 10, - "retriedRelationships": [], - "backoffMechanism": "PENALIZE_FLOWFILE", - "maxBackoffPeriod": "10 mins", - "componentType": "PROCESSOR", - "groupIdentifier": "0ad13e3a-a773-380d-0000-000031bd1ecb" - } - ], - "inputPorts": [], - "outputPorts": [], - "connections": [], - "labels": [], - "funnels": [], - "controllerServices": [], - "defaultFlowFileExpiration": "0 sec", - "defaultBackPressureObjectThreshold": 10000, - "defaultBackPressureDataSizeThreshold": "1 GB", - "scheduledState": "ENABLED", - "executionEngine": "INHERITED", - "maxConcurrentTasks": 1, - "statelessFlowTimeout": "1 min", - "flowFileConcurrency": "UNBOUNDED", - "flowFileOutboundPolicy": "STREAM_WHEN_AVAILABLE", - "componentType": "PROCESS_GROUP", - "groupIdentifier": "96d9abc7-e736-324d-9da8-1c0a4e671508" - }, - { - "identifier": "8f62d3e7-2a0b-3ba8-936c-8a0cb425284a", - "instanceIdentifier": "c9186a05-0196-1000-ffff-ffffd8474359", - "name": "Process Group A", - "comments": "", - "position": { - "x": -190, - "y": -112.91667938232422 - }, - "processGroups": [], - "remoteProcessGroups": [], - "processors": [ - { - "identifier": "e14260ce-23cd-361d-0000-00001b31a7fc", - "instanceIdentifier": "63ad5386-61ce-327a-bce3-5b597829aa54", - "name": "GenerateFlowFile B", - "comments": "", - "position": { - "x": -648, - "y": -520 - }, - "type": "org.apache.nifi.processors.standard.GenerateFlowFile", - "bundle": { - "group": "org.apache.nifi", - "artifact": "nifi-standard-nar", - "version": "2.2.0" - }, - "properties": { - "character-set": "UTF-8", - "File Size": "0B", - "Batch Size": "1", - "Unique FlowFiles": "false", - "Data Format": "Text" - }, - "propertyDescriptors": {}, - "style": {}, - "schedulingPeriod": "1 min", - "schedulingStrategy": "TIMER_DRIVEN", - "executionNode": "ALL", - "penaltyDuration": "30 sec", - "yieldDuration": "1 sec", - "bulletinLevel": "WARN", - "runDurationMillis": 0, - "concurrentlySchedulableTaskCount": 1, - "autoTerminatedRelationships": [], - "scheduledState": "ENABLED", - "retryCount": 10, - "retriedRelationships": [], - "backoffMechanism": "PENALIZE_FLOWFILE", - "maxBackoffPeriod": "10 mins", - "componentType": "PROCESSOR", - "groupIdentifier": "8f62d3e7-2a0b-3ba8-936c-8a0cb425284a" - }, - { - "identifier": "4623a22e-5c14-310b-afb8-c82965f74a7d", - "instanceIdentifier": "c9196a9e-0196-1000-0000-0000418515a4", - "name": "GenerateFlowFile A", - "comments": "", - "position": { - "x": -1032, - "y": -520 - }, - "type": "org.apache.nifi.processors.standard.GenerateFlowFile", - "bundle": { - "group": "org.apache.nifi", - "artifact": "nifi-standard-nar", - "version": "2.2.0" - }, - "properties": { - "character-set": "UTF-8", - "File Size": "0B", - "Batch Size": "1", - "Unique FlowFiles": "false", - "Data Format": "Text" - }, - "propertyDescriptors": {}, - "style": {}, - "schedulingPeriod": "1 min", - "schedulingStrategy": "TIMER_DRIVEN", - "executionNode": "ALL", - "penaltyDuration": "30 sec", - "yieldDuration": "1 sec", - "bulletinLevel": "WARN", - "runDurationMillis": 0, - "concurrentlySchedulableTaskCount": 1, - "autoTerminatedRelationships": [], - "scheduledState": "ENABLED", - "retryCount": 10, - "retriedRelationships": [], - "backoffMechanism": "PENALIZE_FLOWFILE", - "maxBackoffPeriod": "10 mins", - "componentType": "PROCESSOR", - "groupIdentifier": "8f62d3e7-2a0b-3ba8-936c-8a0cb425284a" - } - ], - "inputPorts": [], - "outputPorts": [], - "connections": [], - "labels": [], - "funnels": [], - "controllerServices": [], - "defaultFlowFileExpiration": "0 sec", - "defaultBackPressureObjectThreshold": 10000, - "defaultBackPressureDataSizeThreshold": "1 GB", - "scheduledState": "ENABLED", - "executionEngine": "INHERITED", - "maxConcurrentTasks": 1, - "statelessFlowTimeout": "1 min", - "flowFileConcurrency": "UNBOUNDED", - "flowFileOutboundPolicy": "STREAM_WHEN_AVAILABLE", - "componentType": "PROCESS_GROUP", - "groupIdentifier": "96d9abc7-e736-324d-9da8-1c0a4e671508" - }, - { - "identifier": "82533e93-7494-352d-ffff-ffffaad75c48", - "instanceIdentifier": "f3b78341-b2ff-3ae3-b7fc-4b518617c802", - "name": "Process Group C", - "comments": "", - "position": { - "x": 696, - "y": -112 - }, - "processGroups": [], - "remoteProcessGroups": [], - "processors": [ - { - "identifier": "fcd0595d-bf18-34be-ffff-ffffaad75c48", - "instanceIdentifier": "9d95cac3-2759-3fce-9c07-71215b0fb554", - "name": "GenerateFlowFile E", - "comments": "", - "position": { - "x": -1032, - "y": -520 - }, - "type": "org.apache.nifi.processors.standard.GenerateFlowFile", - "bundle": { - "group": "org.apache.nifi", - "artifact": "nifi-standard-nar", - "version": "2.2.0" - }, - "properties": { - "character-set": "UTF-8", - "File Size": "0B", - "Batch Size": "1", - "Unique FlowFiles": "false", - "Data Format": "Text" - }, - "propertyDescriptors": {}, - "style": {}, - "schedulingPeriod": "1 min", - "schedulingStrategy": "TIMER_DRIVEN", - "executionNode": "ALL", - "penaltyDuration": "30 sec", - "yieldDuration": "1 sec", - "bulletinLevel": "WARN", - "runDurationMillis": 0, - "concurrentlySchedulableTaskCount": 1, - "autoTerminatedRelationships": [], - "scheduledState": "ENABLED", - "retryCount": 10, - "retriedRelationships": [], - "backoffMechanism": "PENALIZE_FLOWFILE", - "maxBackoffPeriod": "10 mins", - "componentType": "PROCESSOR", - "groupIdentifier": "82533e93-7494-352d-ffff-ffffaad75c48" - }, - { - "identifier": "077fd08e-4c7c-39cc-ffff-ffffaad75c48", - "instanceIdentifier": "63d8f319-893c-38d4-9cd4-35cea3f43ea2", - "name": "GenerateFlowFile F", - "comments": "", - "position": { - "x": -648, - "y": -520 - }, - "type": "org.apache.nifi.processors.standard.GenerateFlowFile", - "bundle": { - "group": "org.apache.nifi", - "artifact": "nifi-standard-nar", - "version": "2.2.0" - }, - "properties": { - "character-set": "UTF-8", - "File Size": "0B", - "Batch Size": "1", - "Unique FlowFiles": "false", - "Data Format": "Text" - }, - "propertyDescriptors": {}, - "style": {}, - "schedulingPeriod": "1 min", - "schedulingStrategy": "TIMER_DRIVEN", - "executionNode": "ALL", - "penaltyDuration": "30 sec", - "yieldDuration": "1 sec", - "bulletinLevel": "WARN", - "runDurationMillis": 0, - "concurrentlySchedulableTaskCount": 1, - "autoTerminatedRelationships": [], - "scheduledState": "ENABLED", - "retryCount": 10, - "retriedRelationships": [], - "backoffMechanism": "PENALIZE_FLOWFILE", - "maxBackoffPeriod": "10 mins", - "componentType": "PROCESSOR", - "groupIdentifier": "82533e93-7494-352d-ffff-ffffaad75c48" - } - ], - "inputPorts": [], - "outputPorts": [], - "connections": [], - "labels": [], - "funnels": [], - "controllerServices": [], - "defaultFlowFileExpiration": "0 sec", - "defaultBackPressureObjectThreshold": 10000, - "defaultBackPressureDataSizeThreshold": "1 GB", - "scheduledState": "ENABLED", - "executionEngine": "INHERITED", - "maxConcurrentTasks": 1, - "statelessFlowTimeout": "1 min", - "flowFileConcurrency": "UNBOUNDED", - "flowFileOutboundPolicy": "STREAM_WHEN_AVAILABLE", - "componentType": "PROCESS_GROUP", - "groupIdentifier": "96d9abc7-e736-324d-9da8-1c0a4e671508" - } - ], - "remoteProcessGroups": [], - "processors": [], - "inputPorts": [], - "outputPorts": [], - "connections": [], - "labels": [], - "funnels": [], - "controllerServices": [], - "defaultFlowFileExpiration": "0 sec", - "defaultBackPressureObjectThreshold": 10000, - "defaultBackPressureDataSizeThreshold": "1 GB", - "scheduledState": "ENABLED", - "executionEngine": "INHERITED", - "maxConcurrentTasks": 1, - "statelessFlowTimeout": "1 min", - "flowFileConcurrency": "UNBOUNDED", - "flowFileOutboundPolicy": "STREAM_WHEN_AVAILABLE", - "componentType": "PROCESS_GROUP" - } -} From 6e666274eca72210cbe78a5db552345891f7ffdf Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Fri, 16 May 2025 16:38:16 +0200 Subject: [PATCH 18/31] remove oidc test and fix oidc-opa test with tls disabled --- .../kuttl/oidc-opa/19_keycloak.yaml.j2 | 29 ++-- .../41-install-test-container.yaml.j2 | 2 +- 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 | 159 ------------------ tests/templates/kuttl/oidc/10-assert.yaml | 12 -- .../kuttl/oidc/10-install-zk.yaml.j2 | 29 ---- .../11-create-authentication-classes.yaml.j2 | 6 - .../oidc/11_authentication-classes.yaml.j2 | 26 --- tests/templates/kuttl/oidc/12-assert.yaml | 12 -- .../templates/kuttl/oidc/12-install-nifi.yaml | 5 - tests/templates/kuttl/oidc/12_nifi.yaml.j2 | 51 ------ tests/templates/kuttl/oidc/20-assert.yaml | 11 -- .../kuttl/oidc/20-login-test.yaml.j2 | 37 ---- tests/templates/kuttl/oidc/21-assert.yaml | 11 -- .../kuttl/oidc/21-login-test-logs.yaml | 7 - tests/templates/kuttl/oidc/login.py | 71 -------- tests/test-definition.yaml | 84 +++++---- 21 files changed, 59 insertions(+), 550 deletions(-) delete mode 100644 tests/templates/kuttl/oidc/00-assert.yaml.j2 delete mode 100644 tests/templates/kuttl/oidc/00-install-vector-aggregator-discovery-configmap.yaml.j2 delete mode 100644 tests/templates/kuttl/oidc/00-patch-ns.yaml.j2 delete mode 100644 tests/templates/kuttl/oidc/01-assert.yaml delete mode 100644 tests/templates/kuttl/oidc/01-install-keycloak.yaml delete mode 100644 tests/templates/kuttl/oidc/01_keycloak.yaml.j2 delete mode 100644 tests/templates/kuttl/oidc/10-assert.yaml delete mode 100644 tests/templates/kuttl/oidc/10-install-zk.yaml.j2 delete mode 100644 tests/templates/kuttl/oidc/11-create-authentication-classes.yaml.j2 delete mode 100644 tests/templates/kuttl/oidc/11_authentication-classes.yaml.j2 delete mode 100644 tests/templates/kuttl/oidc/12-assert.yaml delete mode 100644 tests/templates/kuttl/oidc/12-install-nifi.yaml delete mode 100644 tests/templates/kuttl/oidc/12_nifi.yaml.j2 delete mode 100644 tests/templates/kuttl/oidc/20-assert.yaml delete mode 100644 tests/templates/kuttl/oidc/20-login-test.yaml.j2 delete mode 100644 tests/templates/kuttl/oidc/21-assert.yaml delete mode 100644 tests/templates/kuttl/oidc/21-login-test-logs.yaml delete mode 100644 tests/templates/kuttl/oidc/login.py diff --git a/tests/templates/kuttl/oidc-opa/19_keycloak.yaml.j2 b/tests/templates/kuttl/oidc-opa/19_keycloak.yaml.j2 index 34923758..9bb019ab 100644 --- a/tests/templates/kuttl/oidc-opa/19_keycloak.yaml.j2 +++ b/tests/templates/kuttl/oidc-opa/19_keycloak.yaml.j2 @@ -63,12 +63,11 @@ spec: - name: keycloak image: quay.io/keycloak/keycloak:25.0.0 args: - - start - - --hostname-strict=false + - start-dev - --import-realm {% if test_scenario['values']['oidc-use-tls'] == 'true' %} - - --https-key-store-file=/tls/keystore.p12 - - --https-key-store-password=changeit + - --https-certificate-file=/tls/tls.crt + - --https-certificate-key-file=/tls/tls.key {% endif %} env: - name: KEYCLOAK_ADMIN @@ -81,15 +80,21 @@ spec: ports: - name: https containerPort: 8443 + readinessProbe: + httpGet: + scheme: HTTPS + path: /realms/master + port: https {% else %} + ports: - name: http containerPort: 8080 -{% endif %} readinessProbe: httpGet: - scheme: HTTPS + scheme: HTTP path: /realms/master - port: https + port: http +{% endif %} resources: limits: cpu: 1 @@ -117,9 +122,7 @@ spec: metadata: annotations: secrets.stackable.tech/class: keycloak-tls-$NAMESPACE - secrets.stackable.tech/format: tls-pkcs12 - secrets.stackable.tech/format.compatibility.tls-pkcs12.password: changeit - secrets.stackable.tech/scope: service=keycloak,node + secrets.stackable.tech/scope: service=keycloak spec: storageClassName: secrets.stackable.tech accessModes: @@ -139,8 +142,14 @@ metadata: app: keycloak spec: ports: +{% if test_scenario['values']['oidc-use-tls'] == 'true' %} - name: https port: 8443 targetPort: 8443 +{% else %} + - name: http + port: 8080 + targetPort: 8080 +{% endif %} selector: app: keycloak diff --git a/tests/templates/kuttl/oidc-opa/41-install-test-container.yaml.j2 b/tests/templates/kuttl/oidc-opa/41-install-test-container.yaml.j2 index 89ec070f..08d76dcd 100644 --- a/tests/templates/kuttl/oidc-opa/41-install-test-container.yaml.j2 +++ b/tests/templates/kuttl/oidc-opa/41-install-test-container.yaml.j2 @@ -73,7 +73,7 @@ spec: - name: NIFI_VERSION value: "{{ test_scenario['values']['nifi'] }}" - name: OIDC_USE_TLS - value: "true" + value: "{{ test_scenario['values']['oidc-use-tls'] }}" volumeMounts: - name: test-script mountPath: /tmp/test-script diff --git a/tests/templates/kuttl/oidc/00-assert.yaml.j2 b/tests/templates/kuttl/oidc/00-assert.yaml.j2 deleted file mode 100644 index 50b1d4c3..00000000 --- a/tests/templates/kuttl/oidc/00-assert.yaml.j2 +++ /dev/null @@ -1,10 +0,0 @@ ---- -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 deleted file mode 100644 index 2d6a0df5..00000000 --- a/tests/templates/kuttl/oidc/00-install-vector-aggregator-discovery-configmap.yaml.j2 +++ /dev/null @@ -1,9 +0,0 @@ -{% 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 deleted file mode 100644 index 67185acf..00000000 --- a/tests/templates/kuttl/oidc/00-patch-ns.yaml.j2 +++ /dev/null @@ -1,9 +0,0 @@ -{% 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 deleted file mode 100644 index 5f3fae52..00000000 --- a/tests/templates/kuttl/oidc/01-assert.yaml +++ /dev/null @@ -1,14 +0,0 @@ ---- -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 deleted file mode 100644 index 4e07a328..00000000 --- a/tests/templates/kuttl/oidc/01-install-keycloak.yaml +++ /dev/null @@ -1,15 +0,0 @@ ---- -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 deleted file mode 100644 index 1331f2ac..00000000 --- a/tests/templates/kuttl/oidc/01_keycloak.yaml.j2 +++ /dev/null @@ -1,159 +0,0 @@ -# The environment variables must be replaced. ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: $INSTANCE_NAME-realms -data: - test-realm.json: | - { - "realm": "$REALM", - "enabled": true, - "attributes": { - "frontendUrl": "keycloak.$NAMESPACE.svc.cluster.local" - }, - "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: - 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: keycloak-tls-$NAMESPACE - 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: secrets.stackable.tech/v1alpha1 -kind: SecretClass -metadata: - name: keycloak-tls-$NAMESPACE -spec: - backend: - autoTls: - ca: - autoGenerate: true - secret: - name: keycloak-tls-ca - namespace: $NAMESPACE diff --git a/tests/templates/kuttl/oidc/10-assert.yaml b/tests/templates/kuttl/oidc/10-assert.yaml deleted file mode 100644 index e0766c49..00000000 --- a/tests/templates/kuttl/oidc/10-assert.yaml +++ /dev/null @@ -1,12 +0,0 @@ ---- -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 deleted file mode 100644 index 275907d4..00000000 --- a/tests/templates/kuttl/oidc/10-install-zk.yaml.j2 +++ /dev/null @@ -1,29 +0,0 @@ ---- -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: - gracefulShutdownTimeout: 1m - logging: - enableVectorAgent: {{ lookup('env', 'VECTOR_AGGREGATOR') | length > 0 }} - roleGroups: - default: - replicas: 1 ---- -apiVersion: zookeeper.stackable.tech/v1alpha1 -kind: ZookeeperZnode -metadata: - 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 deleted file mode 100644 index 6efd6383..00000000 --- a/tests/templates/kuttl/oidc/11-create-authentication-classes.yaml.j2 +++ /dev/null @@ -1,6 +0,0 @@ ---- -apiVersion: kuttl.dev/v1beta1 -kind: TestStep -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 deleted file mode 100644 index 8b0afe51..00000000 --- a/tests/templates/kuttl/oidc/11_authentication-classes.yaml.j2 +++ /dev/null @@ -1,26 +0,0 @@ ---- -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 -{% if test_scenario['values']['oidc-use-tls'] == 'true' %} - port: 8443 - tls: - verification: - server: - caCert: - secretClass: keycloak-tls-$NAMESPACE -{% else %} - port: 8080 - tls: null -{% endif %} diff --git a/tests/templates/kuttl/oidc/12-assert.yaml b/tests/templates/kuttl/oidc/12-assert.yaml deleted file mode 100644 index 2f03b6b1..00000000 --- a/tests/templates/kuttl/oidc/12-assert.yaml +++ /dev/null @@ -1,12 +0,0 @@ ---- -apiVersion: kuttl.dev/v1beta1 -kind: TestAssert -timeout: 1200 ---- -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: test-nifi-node-default -status: - readyReplicas: 1 - replicas: 1 diff --git a/tests/templates/kuttl/oidc/12-install-nifi.yaml b/tests/templates/kuttl/oidc/12-install-nifi.yaml deleted file mode 100644 index edef731d..00000000 --- a/tests/templates/kuttl/oidc/12-install-nifi.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -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 deleted file mode 100644 index 6edd2e42..00000000 --- a/tests/templates/kuttl/oidc/12_nifi.yaml.j2 +++ /dev/null @@ -1,51 +0,0 @@ ---- -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 %} - custom: null - 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-oidc-znode - listenerClass: external-unstable - nodes: - config: - logging: - enableVectorAgent: {{ lookup('env', 'VECTOR_AGGREGATOR') | length > 0 }} - gracefulShutdownTimeout: 1s # let the tests run faster - roleGroups: - default: - config: {} - replicas: 1 diff --git a/tests/templates/kuttl/oidc/20-assert.yaml b/tests/templates/kuttl/oidc/20-assert.yaml deleted file mode 100644 index 47c18ad2..00000000 --- a/tests/templates/kuttl/oidc/20-assert.yaml +++ /dev/null @@ -1,11 +0,0 @@ ---- -apiVersion: kuttl.dev/v1beta1 -kind: TestAssert -timeout: 600 ---- -apiVersion: batch/v1 -kind: Job -metadata: - name: oidc-login-test -status: - succeeded: 1 # wait for the test job to start before streaming its logs in the next test step diff --git a/tests/templates/kuttl/oidc/20-login-test.yaml.j2 b/tests/templates/kuttl/oidc/20-login-test.yaml.j2 deleted file mode 100644 index 97e3443c..00000000 --- a/tests/templates/kuttl/oidc/20-login-test.yaml.j2 +++ /dev/null @@ -1,37 +0,0 @@ ---- -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: oci.stackable.tech/sdp/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: NIFI_VERSION - value: "{{ test_scenario['values']['nifi'] }}" - - 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/21-assert.yaml b/tests/templates/kuttl/oidc/21-assert.yaml deleted file mode 100644 index f55ee23d..00000000 --- a/tests/templates/kuttl/oidc/21-assert.yaml +++ /dev/null @@ -1,11 +0,0 @@ ---- -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 deleted file mode 100644 index 092debe4..00000000 --- a/tests/templates/kuttl/oidc/21-login-test-logs.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -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 deleted file mode 100644 index 4961c5fe..00000000 --- a/tests/templates/kuttl/oidc/login.py +++ /dev/null @@ -1,71 +0,0 @@ -import logging -import os -import requests -import sys -import json -from bs4 import BeautifulSoup - -logging.basicConfig( - level="DEBUG", format="%(asctime)s %(levelname)s: %(message)s", stream=sys.stdout -) - -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" - -keycloak_base_url = ( - f"https://{keycloak_service}:8443" - if tls == "true" - else f"http://{keycloak_service}:8080" -) - -# startswith instead of an exact check to -# a) hit all 2.x versions and -# b) to allow for custom images because `nifi_version` will contain the whole custom image string -# e.g. 2.0.0,localhost:5000/stackable/nifi:2.0.0-stackable0.0.0-dev -if not nifi_version.startswith("1."): - 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=" -), "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": "jane.doe", "password": "T8mn72D9"}, - verify=False, -) -assert welcome_page.ok, "Login failed" -assert ( - welcome_page.url == f"https://{nifi}:8443/nifi/" -), "Redirection to the NiFi web UI expected" diff --git a/tests/test-definition.yaml b/tests/test-definition.yaml index c8207831..9d32335b 100644 --- a/tests/test-definition.yaml +++ b/tests/test-definition.yaml @@ -38,7 +38,7 @@ dimensions: - "true" - name: oidc-use-tls values: - # - "false" + - "false" - "true" - name: openshift values: @@ -49,50 +49,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: oidc - # dimensions: - # - nifi - # - zookeeper-latest - # - oidc-use-tls - # - 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-opa dimensions: - nifi From 96d66db45b1e182bd34e3010a9aaec13776a284e Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Mon, 19 May 2025 13:58:40 +0200 Subject: [PATCH 19/31] Apply suggestions from code review Co-authored-by: Malte Sander --- docs/modules/nifi/pages/usage_guide/security.adoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/modules/nifi/pages/usage_guide/security.adoc b/docs/modules/nifi/pages/usage_guide/security.adoc index 0c4e6c9e..3d8625d5 100644 --- a/docs/modules/nifi/pages/usage_guide/security.adoc +++ b/docs/modules/nifi/pages/usage_guide/security.adoc @@ -277,7 +277,7 @@ The payload sent by NiFi with each request to OPA, that is accessible within the [#opa-result] ==== OPA Result -The OPA authorizer plugin expects relo rules to be named `allow` and to return a result following this schema: +The OPA authorizer plugin expects rego rules to be named `allow` and to return a result following this schema: [source,json] ---- { @@ -299,7 +299,7 @@ NiFi uses {nifi-docs-access-policies}[access policies] to manage access to syste [#component-level-access-policies] ==== Component Level Access Policies and Access Policy Inheritance -{nifi-docs-component-level-access-policies}[Component Level Access Policies] allow managing granular access to components like process-groups and processors. Components can {nifi-docs-access-policy-inheritance}[inherite access policies] defined for parent components, e.g. a process group is the parent component for a processor component. +{nifi-docs-component-level-access-policies}[Component Level Access Policies] allow managing granular access to components like process-groups and processors. Components can {nifi-docs-access-policy-inheritance}[inherite access policies] defined for parent components, e.g. a process group is the parent component for a contained processor component. The payload field `requestedResource` contains the id, name and description of the original resource that was requested. In cases with inherited policies, this will be an ancestor resource of the current resource. For the initial request, and cases without inheritance, the requested resource will be the same as the current resource. @@ -327,7 +327,7 @@ allow := { input.identity.name == "alice" } <3> ---- -<1> Default rule returns should `"resourceNotFound": true`. If this is not set NiFi's access policy inheritance won't work. Any values for the `allowed` field in the response will be ignored. +<1> The default rule should return `"resourceNotFound": true`. If this is not set, NiFi's access policy inheritance will not work. Any values for the `allowed` field in the response will be ignored. <2> A rule that grants all users access to the root process group and thus to all components in the NiFi instance. <3> A rule that denies access to a specific process group for the user "alice". For this process group the default rego rule will not be applied and NiFi's component inhertiance will not be used. All child components of this process group will also be authorized based on this rule unless a more granular rule overrides it. From 310770407fa9d4701607192d57f9993e4cb29a8d Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Mon, 19 May 2025 14:00:22 +0200 Subject: [PATCH 20/31] improve docs --- docs/modules/nifi/pages/usage_guide/security.adoc | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/modules/nifi/pages/usage_guide/security.adoc b/docs/modules/nifi/pages/usage_guide/security.adoc index 3d8625d5..4e2f14f9 100644 --- a/docs/modules/nifi/pages/usage_guide/security.adoc +++ b/docs/modules/nifi/pages/usage_guide/security.adoc @@ -171,9 +171,7 @@ stringData: [#authorization] == Authorization -NiFi supports {nifi-docs-authorization}[multiple authorization methods], the available authorization methods depend on the chosen authentication method. - -The Stackable Operator for Apache NiFi supports a default authorization methods for each authentication method and authorization with Open Policy Agent. +The Stackable Operator for Apache NiFi supports {nifi-docs-authorization}[multiple authorization methods], the available authorization methods depend on the chosen authentication method. Using Open Policy Agent for authorization is independent of the authentication method. [#authorization-single-user] === Single user From 2a9a99229e75468aa4b0f3f5d658057c5e303986 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Mon, 19 May 2025 16:04:45 +0200 Subject: [PATCH 21/31] require opa operator during integration tests --- tests/release.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/release.yaml b/tests/release.yaml index b6ecf959..2f086353 100644 --- a/tests/release.yaml +++ b/tests/release.yaml @@ -14,5 +14,7 @@ releases: operatorVersion: 0.0.0-dev zookeeper: operatorVersion: 0.0.0-dev + opa: + operatorVersion: 0.0.0-dev nifi: operatorVersion: 0.0.0-dev From ca80daae7e40bee53abd7213d4cb9165896e4e9b Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Tue, 20 May 2025 12:29:55 +0200 Subject: [PATCH 22/31] reduce flow election time --- tests/templates/kuttl/oidc-opa/30_nifi.yaml.j2 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/templates/kuttl/oidc-opa/30_nifi.yaml.j2 b/tests/templates/kuttl/oidc-opa/30_nifi.yaml.j2 index 87f7ffd7..bb446c0b 100644 --- a/tests/templates/kuttl/oidc-opa/30_nifi.yaml.j2 +++ b/tests/templates/kuttl/oidc-opa/30_nifi.yaml.j2 @@ -43,6 +43,10 @@ spec: logging: enableVectorAgent: {{ lookup('env', 'VECTOR_AGGREGATOR') | length > 0 }} gracefulShutdownTimeout: 1s # let the tests run faster + configOverrides: + nifi.properties: + # speed up tests + nifi.cluster.flow.election.max.wait.time: 10 secs podOverrides: spec: initContainers: From 71519afa6f23cfb0ac2a8df15d607ee1a7332f18 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Tue, 20 May 2025 12:31:36 +0200 Subject: [PATCH 23/31] reduce verbosity, improve response handling --- tests/templates/kuttl/oidc-opa/test.py | 45 ++++++++++++++------------ 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/tests/templates/kuttl/oidc-opa/test.py b/tests/templates/kuttl/oidc-opa/test.py index 6d28ad2f..496572d2 100644 --- a/tests/templates/kuttl/oidc-opa/test.py +++ b/tests/templates/kuttl/oidc-opa/test.py @@ -4,10 +4,15 @@ import sys import time import json +import urllib3 from bs4 import BeautifulSoup +from requests.exceptions import JSONDecodeError + +# disable tls insecure warnings +urllib3.disable_warnings() logging.basicConfig( - level="DEBUG", format="%(asctime)s %(levelname)s: %(message)s", stream=sys.stdout + level="INFO", format="%(asctime)s %(levelname)s: %(message)s", stream=sys.stdout ) namespace = os.environ["NAMESPACE"] @@ -73,44 +78,44 @@ def login(session: requests.Session, username: str, password: str): def get_process_group_a(session: requests.Session) -> requests.Response: - return get_resource_with_retries( + return get_resource( session, "/flow/process-groups/c9186a05-0196-1000-ffff-ffffd8474359" ) def get_process_group_b(session: requests.Session) -> requests.Response: - return get_resource_with_retries( + return get_resource( session, "/flow/process-groups/7e08561b-447d-3acb-b510-744d886c3ca4" ) def get_processor_e(session: requests.Session) -> requests.Response: - return get_resource_with_retries( + return get_resource( session, "/processors/9d95cac3-2759-3fce-9c07-71215b0fb554" ) def get_counters(session: requests.Session) -> requests.Response: - return get_resource_with_retries(session, "/counters") + return get_resource(session, "/counters") -def get_resource_with_retries( +def get_resource( session: requests.Session, resource: str ) -> requests.Response: - retries = 0 - max_retries = 5 - while True: - time.sleep(retries ^ 2) - response = session.get( - f"https://{nifi}:8443/nifi-api{resource}?uiOnly=true", - verify=False, - ) - # Occasionally NiFi will respond with an 409 http error - if response.status_code == 409 and retries <= max_retries: - print("NiFi returned HTTP 409") - retries += 1 - else: - return response + response = session.get( + f"https://{nifi}:8443/nifi-api{resource}?uiOnly=true", + verify=False, + ) + + # let success or unauthorized pass + if response.status_code == 200 or response.status_code == 403: + return response + else: + print(f"Could not retrieve resource [{resource}] ...") + print("Status Code:", response.status_code) + print("Response Text:", response.text) + time.sleep(5) + exit(1) # alice From c26759f0af849c399fef30ef35660eff41845faa Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Tue, 20 May 2025 12:40:36 +0200 Subject: [PATCH 24/31] linter attempt 1 --- tests/templates/kuttl/oidc-opa/test.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/tests/templates/kuttl/oidc-opa/test.py b/tests/templates/kuttl/oidc-opa/test.py index 496572d2..a7e96d55 100644 --- a/tests/templates/kuttl/oidc-opa/test.py +++ b/tests/templates/kuttl/oidc-opa/test.py @@ -6,7 +6,6 @@ import json import urllib3 from bs4 import BeautifulSoup -from requests.exceptions import JSONDecodeError # disable tls insecure warnings urllib3.disable_warnings() @@ -90,20 +89,16 @@ def get_process_group_b(session: requests.Session) -> requests.Response: def get_processor_e(session: requests.Session) -> requests.Response: - return get_resource( - session, "/processors/9d95cac3-2759-3fce-9c07-71215b0fb554" - ) + return get_resource(session, "/processors/9d95cac3-2759-3fce-9c07-71215b0fb554") def get_counters(session: requests.Session) -> requests.Response: return get_resource(session, "/counters") -def get_resource( - session: requests.Session, resource: str -) -> requests.Response: +def get_resource(session: requests.Session, resource: str) -> requests.Response: response = session.get( - f"https://{nifi}:8443/nifi-api{resource}?uiOnly=true", + f"https://{nifi}:8443/nifi-api{resource}?uiOnly=true", verify=False, ) From 860e34640284e454925e1c6defaa4d6bfe384682 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Tue, 20 May 2025 14:54:32 +0200 Subject: [PATCH 25/31] remove securityContext from keycloak --- tests/templates/kuttl/oidc-opa/19_keycloak.yaml.j2 | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/templates/kuttl/oidc-opa/19_keycloak.yaml.j2 b/tests/templates/kuttl/oidc-opa/19_keycloak.yaml.j2 index 9bb019ab..9f43dcbb 100644 --- a/tests/templates/kuttl/oidc-opa/19_keycloak.yaml.j2 +++ b/tests/templates/kuttl/oidc-opa/19_keycloak.yaml.j2 @@ -109,10 +109,6 @@ spec: mountPath: /tls/ - name: realm-volume mountPath: /opt/keycloak/data/import - securityContext: - fsGroup: 1000 - runAsGroup: 1000 - runAsUser: 1000 volumes: - name: data emptyDir: {} From 6d7799209fdb4ea72ad668b639b6db7740cbc292 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Tue, 20 May 2025 15:36:07 +0200 Subject: [PATCH 26/31] remove securityContext from test container --- .../templates/kuttl/oidc-opa/41-install-test-container.yaml.j2 | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/templates/kuttl/oidc-opa/41-install-test-container.yaml.j2 b/tests/templates/kuttl/oidc-opa/41-install-test-container.yaml.j2 index 08d76dcd..7a860e26 100644 --- a/tests/templates/kuttl/oidc-opa/41-install-test-container.yaml.j2 +++ b/tests/templates/kuttl/oidc-opa/41-install-test-container.yaml.j2 @@ -51,8 +51,6 @@ spec: app: python spec: serviceAccountName: python - securityContext: - fsGroup: 1000 containers: - name: oidc-login-test image: oci.stackable.tech/sdp/testing-tools:0.2.0-stackable0.0.0-dev From 993d4d92449327a9d2bcdf8e881256a89f10dceb Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Tue, 20 May 2025 16:00:50 +0200 Subject: [PATCH 27/31] Apply suggestions from code review Co-authored-by: Malte Sander --- docs/modules/nifi/pages/usage_guide/security.adoc | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/modules/nifi/pages/usage_guide/security.adoc b/docs/modules/nifi/pages/usage_guide/security.adoc index 4e2f14f9..ae05098f 100644 --- a/docs/modules/nifi/pages/usage_guide/security.adoc +++ b/docs/modules/nifi/pages/usage_guide/security.adoc @@ -235,10 +235,10 @@ The payload sent by NiFi with each request to OPA, that is accessible within the | The action taken against the resource. |`read`, `write` | resource.id -| The unique identifier of the resource that is being authorized. +| The unique identifier of the resource that is being authorized. This might be a parent component in the case of `resourceNotFound` is set to `true`. | | resource.name -| The name of the resource that is being authorized. +| The name of the resource that is being authorized. This might be a parent component in the case of `resourceNotFound` is set to `true`. | | resource.safeDescription | The description of the resource that is being authorized. @@ -250,7 +250,7 @@ The payload sent by NiFi with each request to OPA, that is accessible within the | The name of the original resource that is being authorized on (see <>). | | requestedResource.safeDescription -| The description of the resource that is being authorized on (see <>). +| The description of the original resource that is being authorized on (see <>). | | identity.name | The name of the identity/user accessing the resource. @@ -286,7 +286,7 @@ The OPA authorizer plugin expects rego rules to be named `allow` and to return a } ---- <1> Whether the action against the resource is allowed. Optional, defaults to false. -<2> Whether no rule was found for the authorization request. This should only be set to true in the default rule. If set to true the value of the "allowed" field will be ignored. Optional, defaults to false. +<2> Whether no rule was found for the authorization request. This should only be set to true in the default rule to e.g. forward policy decisions to parent components. If set to true the value of the "allowed" field will be ignored. Optional, defaults to false. <3> Whether the whole local cache in the OPA authorizer plugin in NiFi should be invalidated. Optional, defaults to false. <4> An optional error message that is shown to the user when access is denied. @@ -309,21 +309,21 @@ To manage access for all process groups in the NiFi instance a rule has to be de ---- default allow := { "resourceNotFound": true -} <1> +} # <1> allow := { "allowed": true } if { input.resource.name == "NiFi Flow" startswith(input.resource.id, "/process-groups") -} <2> +} # <2> allow := { "allowed": false } if { input.resource.id == "/process-groups/a10c311e-0196-1000-2856-dc0606d3c5d7" input.identity.name == "alice" -} <3> +} # <3> ---- <1> The default rule should return `"resourceNotFound": true`. If this is not set, NiFi's access policy inheritance will not work. Any values for the `allowed` field in the response will be ignored. <2> A rule that grants all users access to the root process group and thus to all components in the NiFi instance. From fff5c79bd128d210fa8d73bb8b7316dbda9ccf4b Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Tue, 20 May 2025 16:20:07 +0200 Subject: [PATCH 28/31] remove 'json' annotation from code snippet --- 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 ae05098f..39b1291d 100644 --- a/docs/modules/nifi/pages/usage_guide/security.adoc +++ b/docs/modules/nifi/pages/usage_guide/security.adoc @@ -276,7 +276,7 @@ The payload sent by NiFi with each request to OPA, that is accessible within the ==== OPA Result The OPA authorizer plugin expects rego rules to be named `allow` and to return a result following this schema: -[source,json] +[source] ---- { "allowed": , # <1> From 74a824350f95fcf7ee5d259f33ead781cf66ee21 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Tue, 20 May 2025 19:40:56 +0200 Subject: [PATCH 29/31] fix docs formatting --- 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 39b1291d..7b24c871 100644 --- a/docs/modules/nifi/pages/usage_guide/security.adoc +++ b/docs/modules/nifi/pages/usage_guide/security.adoc @@ -235,7 +235,7 @@ The payload sent by NiFi with each request to OPA, that is accessible within the | The action taken against the resource. |`read`, `write` | resource.id -| The unique identifier of the resource that is being authorized. This might be a parent component in the case of `resourceNotFound` is set to `true`. +| The unique identifier of the resource that is being authorized. This might be a parent component in the case of `resourceNotFound` is set to `true`. | | resource.name | The name of the resource that is being authorized. This might be a parent component in the case of `resourceNotFound` is set to `true`. From 0e7f10bf67886dd84d305da48963653728c0ea13 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Wed, 21 May 2025 14:05:37 +0200 Subject: [PATCH 30/31] Update docs/modules/nifi/pages/usage_guide/security.adoc Co-authored-by: Malte Sander --- 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 7b24c871..2027bdab 100644 --- a/docs/modules/nifi/pages/usage_guide/security.adoc +++ b/docs/modules/nifi/pages/usage_guide/security.adoc @@ -347,7 +347,7 @@ allow := { [#caching] ==== Caching -The OPA authorizer has a mechanism to cache results from OPA which can be configured in the NifCluster spec (see above). To delete the whole cache add `"dumpCache": true` to the result. +The OPA authorizer has a mechanism to cache results from OPA which can be configured in the NifiCluster spec (see above). To delete the whole cache add `"dumpCache": true` to the result. [source,rego] ---- allow := { From 19225a9c6cf81d662048049a9d5014c84ef915fb Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Wed, 21 May 2025 14:28:07 +0200 Subject: [PATCH 31/31] add caching default values to docs --- docs/modules/nifi/pages/usage_guide/security.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/modules/nifi/pages/usage_guide/security.adoc b/docs/modules/nifi/pages/usage_guide/security.adoc index 2027bdab..883ccebe 100644 --- a/docs/modules/nifi/pages/usage_guide/security.adoc +++ b/docs/modules/nifi/pages/usage_guide/security.adoc @@ -216,8 +216,8 @@ spec: <2> The rego rule package to use for policy decisions. The package needs to contain an `allow` rule. This is optional and defaults to the name of the NiFi Stacklet. -<3> TTL for items in the cache in NiFi. -<4> Maximum number of concurrent entries in the cache in NiFi +<3> TTL for items in the cache in NiFi. Optional, defaults to 30 seconds. +<4> Maximum number of concurrent entries in the cache in NiFi. Optional, defaults to 10000 entries. [#defining-rego-rules] === Defining rego rules