Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

OpenID Connect Support #423

Merged
merged 44 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
708139f
Support OIDC
siegfriedweber Oct 24, 2023
e36417b
Merge remote-tracking branch 'origin/main' into feat/oidc
sbernauer Nov 17, 2023
7b5bba3
Rework to use new structs from operator-rs
sbernauer Nov 17, 2023
b70524e
charts
sbernauer Nov 17, 2023
8a69005
handle tls settings
sbernauer Nov 17, 2023
db3fd1b
charts
sbernauer Nov 17, 2023
3412c6e
Merge remote-tracking branch 'origin/main' into feat/oidc
sbernauer Nov 20, 2023
ce3d512
new operator-rs
sbernauer Nov 20, 2023
133530d
update operator-rs
sbernauer Nov 23, 2023
0ea4d0a
fix: Move oidcApiPath to the correct location
sbernauer Nov 23, 2023
8686c4b
chore: update operator-rs
NickLarsenNZ Nov 24, 2023
b47a58d
feat: erorr out if superset is configured for oidc+keycloak with a pr…
NickLarsenNZ Nov 24, 2023
d137770
update operator-rs
sbernauer Nov 27, 2023
fbd82b6
bump
sbernauer Dec 4, 2023
76c01ca
Merge remote-tracking branch 'origin/main' into feat/oidc
sbernauer Jan 2, 2024
1d771b5
Update operator-rs
sbernauer Jan 3, 2024
8130ec7
fix tests
sbernauer Jan 3, 2024
c8e5712
Merge remote-tracking branch 'origin/main' into feat/oidc
sbernauer Jan 3, 2024
7fb873f
Make authentication configuration optional
siegfriedweber Jan 11, 2024
b87bbd3
Rename SupersetAuthenticationConfigResolved to SupersetClientAuthenti…
siegfriedweber Jan 12, 2024
e00535d
Improve the data type of the authentication configuration
siegfriedweber Jan 12, 2024
b8829d3
Regenerate charts
siegfriedweber Jan 12, 2024
e7bca8e
Revise the resolution of OIDC authentication details
siegfriedweber Jan 16, 2024
8f89ca7
Merge branch 'main' into feat/oidc
siegfriedweber Jan 17, 2024
44a9bb6
Add OIDC test
siegfriedweber Jan 23, 2024
f088ffe
Use TLS for Keycloak in the OIDC test
siegfriedweber Jan 24, 2024
03bdd3b
Check the user info in the OIDC test
siegfriedweber Jan 25, 2024
efe1ac0
Fix linter warnings
siegfriedweber Jan 25, 2024
d505309
Fix secret scope in the OIDC test
siegfriedweber Jan 25, 2024
0369b12
Refactor the check that TLS verification cannot be disabled
siegfriedweber Jan 25, 2024
054e580
Allow multiple OIDC providers
siegfriedweber Jan 26, 2024
c97ac70
Add documentation for OIDC in Superset
siegfriedweber Jan 29, 2024
cee7130
Use main branch of operator-rs
siegfriedweber Jan 29, 2024
b5f640f
Merge branch 'main' into feat/oidc
siegfriedweber Jan 29, 2024
34e6185
Fix spelling
siegfriedweber Jan 30, 2024
71007aa
Add comments
siegfriedweber Jan 30, 2024
4e622b4
Move the creation of EnvVars into a separate function
siegfriedweber Jan 31, 2024
9830a69
Update docs/modules/superset/pages/usage-guide/security.adoc
siegfriedweber Jan 31, 2024
bf1c875
Update docs/modules/superset/pages/usage-guide/security.adoc
siegfriedweber Jan 31, 2024
fdf8fbc
Update docs/modules/superset/pages/usage-guide/security.adoc
siegfriedweber Jan 31, 2024
24e54ef
Upgrade operator-rs to version 0.64.0
siegfriedweber Jan 31, 2024
171889a
Merge branch 'main' into feat/oidc
siegfriedweber Jan 31, 2024
73a21be
Update changelog
siegfriedweber Jan 31, 2024
1605b48
Fix external link icons in the documentation
siegfriedweber Jan 31, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
592 changes: 286 additions & 306 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,5 @@ strum = { version = "0.25", features = ["derive"] }
tokio = { version = "1.29", features = ["full"] }
tracing = "0.1"

# [patch."https://github.com/stackabletech/operator-rs.git"]
# stackable-operator = { git = "https://github.com/stackabletech//operator-rs.git", branch = "main" }
[patch."https://github.com/stackabletech/operator-rs.git"]
sbernauer marked this conversation as resolved.
Show resolved Hide resolved
stackable-operator = { git = "https://github.com/stackabletech//operator-rs.git", branch = "feat/sso-auth-classes" }
87 changes: 67 additions & 20 deletions deploy/helm/superset-operator/crds/crds.yaml

Large diffs are not rendered by default.

206 changes: 123 additions & 83 deletions rust/crd/src/authentication.rs
Original file line number Diff line number Diff line change
@@ -1,84 +1,92 @@
use serde::{Deserialize, Serialize};
use snafu::{ResultExt, Snafu};
use stackable_operator::commons::authentication::AuthenticationClassProvider;
use stackable_operator::commons::authentication::{
ldap, oidc, AuthenticationClassProvider, ClientAuthenticationDetails,
};
use stackable_operator::kube::ResourceExt;
use stackable_operator::{
client::Client,
commons::authentication::AuthenticationClass,
kube::runtime::reflector::ObjectRef,
schemars::{self, JsonSchema},
};

const SUPPORTED_AUTHENTICATION_CLASS_PROVIDERS: [&str; 1] = ["LDAP"];

#[derive(Snafu, Debug)]
pub enum Error {
#[snafu(display("Failed to retrieve AuthenticationClass {authentication_class}"))]
#[snafu(display("Failed to retrieve AuthenticationClass"))]
AuthenticationClassRetrieval {
source: stackable_operator::error::Error,
authentication_class: ObjectRef<AuthenticationClass>,
},

// TODO: Adapt message if multiple authentication classes are supported simultaneously
#[snafu(display("Only one authentication class is currently supported at a time"))]
MultipleAuthenticationClassesProvided,

#[snafu(display(
"Failed to use authentication provider [{provider}] for authentication class [{authentication_class}] - supported providers: {SUPPORTED_AUTHENTICATION_CLASS_PROVIDERS:?}",
"Failed to use authentication provider {provider:?} for authentication class {auth_class:?} - supported providers: {SUPPORTED_AUTHENTICATION_CLASS_PROVIDERS:?}",
))]
AuthenticationProviderNotSupported {
authentication_class: ObjectRef<AuthenticationClass>,
auth_class: String,
provider: String,
},
}

type Result<T, E = Error> = std::result::Result<T, E>;
#[snafu(display("Invalid OIDC configuration"))]
OidcConfiguration {
source: stackable_operator::error::Error,
},

/// Resolved counter part for `SuperSetAuthenticationConfig`.
pub struct SuperSetAuthenticationConfigResolved {
pub authentication_class: Option<AuthenticationClass>,
pub user_registration: bool,
pub user_registration_role: String,
pub sync_roles_at: FlaskRolesSyncMoment,
#[snafu(display(
"{configured:?} is not a supported principalClaim in superset for the keycloak oidc provider. Please use {supported:?} in the AuthenticationClass {auth_class_name:?}"
))]
OidcPrincipalClaimNotSupported {
configured: String,
supported: String,
auth_class_name: String,
},
}

#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SupersetAuthentication {
#[serde(default)]
authentication: Vec<SuperSetAuthenticationConfig>,
}
type Result<T, E = Error> = std::result::Result<T, E>;

impl SupersetAuthentication {
pub fn authentication_class_names(&self) -> Vec<&str> {
let mut auth_classes = vec![];
for config in &self.authentication {
if let Some(auth_config) = &config.authentication_class {
auth_classes.push(auth_config.as_str());
}
}
auth_classes
}
pub enum SupersetAuthenticationClassResolved {
Ldap {
provider: ldap::AuthenticationProvider,
},
Oidc {
provider: oidc::AuthenticationProvider,
oidc: oidc::ClientAuthenticationOptions<SupersetOidcExtraFields>,
},
}

#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SuperSetAuthenticationConfig {
/// Name of the AuthenticationClass used to authenticate the users.
/// At the moment only LDAP is supported.
/// If not specified the default authentication (AUTH_DB) will be used.
pub authentication_class: Option<String>,
pub struct SupersetClientAuthenticationDetails {
#[serde(flatten)]
pub common: ClientAuthenticationDetails<SupersetOidcExtraFields>,

/// Allow users who are not already in the FAB DB.
/// Gets mapped to `AUTH_USER_REGISTRATION`
#[serde(default = "default_user_registration")]
pub user_registration: bool,

/// This role will be given in addition to any AUTH_ROLES_MAPPING.
/// Gets mapped to `AUTH_USER_REGISTRATION_ROLE`
#[serde(default = "default_user_registration_role")]
pub user_registration_role: String,

/// If we should replace ALL the user's roles each login, or only on registration.
/// Gets mapped to `AUTH_ROLES_SYNC_AT_LOGIN`
#[serde(default = "default_sync_roles_at")]
#[serde(default)]
pub sync_roles_at: FlaskRolesSyncMoment,
}

#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SupersetOidcExtraFields {
/// Path appended to the root path
#[serde(default = "default_oidc_api_path")]
pub oidc_api_path: String,
}

pub fn default_user_registration() -> bool {
true
}
Expand All @@ -87,63 +95,95 @@ pub fn default_user_registration_role() -> String {
"Public".to_string()
}

/// Matches Flask's default mode of syncing at registration
pub fn default_sync_roles_at() -> FlaskRolesSyncMoment {
FlaskRolesSyncMoment::Registration
pub fn default_oidc_api_path() -> String {
"protocol".to_string()
}

#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
pub enum FlaskRolesSyncMoment {
Registration,
Login,
/// Resolved counter part for `SuperSetAuthenticationConfig`.
pub struct SupersetAuthenticationConfigResolved {
pub authentication_class_resolved: Option<SupersetAuthenticationClassResolved>,
pub user_registration: bool,
pub user_registration_role: String,
pub sync_roles_at: FlaskRolesSyncMoment,
}

impl SupersetAuthentication {
/// Retrieve all provided `AuthenticationClass` references.
pub async fn resolve(
&self,
impl SupersetAuthenticationConfigResolved {
pub async fn from(
auth_details: &Vec<SupersetClientAuthenticationDetails>,
client: &Client,
) -> Result<Vec<SuperSetAuthenticationConfigResolved>> {
let mut resolved = vec![];

// TODO: adapt if multiple authentication classes are supported by superset.
// This is currently not possible due to the Flask App Builder not supporting it.
if self.authentication.len() > 1 {
) -> Result<SupersetAuthenticationConfigResolved> {
// TODO: Adapt if multiple authentication types are supported by Superset.
// This is currently not possible due to the Flask-AppBuilder not supporting it,
// see https://github.com/dpgaspar/Flask-AppBuilder/issues/1924.
if auth_details.len() > 1 {
return Err(Error::MultipleAuthenticationClassesProvided);
}

for config in &self.authentication {
let auth_class = if let Some(auth_class) = &config.authentication_class {
let resolved = AuthenticationClass::resolve(client, auth_class)
let mut user_registration = true;
let mut user_registration_role = Default::default();
let mut sync_roles_at = Default::default();

let authentication_class_resolved = match auth_details.first() {
Some(auth_details) => {
let auth_class = auth_details
.common
.resolve_class(client)
.await
.context(AuthenticationClassRetrievalSnafu {
authentication_class: ObjectRef::<AuthenticationClass>::new(auth_class),
})?;

// Checking for supported AuthenticationClass here is a little out of place, but is does not
// make sense to iterate further after finding an unsupported AuthenticationClass.
Some(match resolved.spec.provider {
AuthenticationClassProvider::Ldap(_) => resolved,
AuthenticationClassProvider::Tls(_)
| AuthenticationClassProvider::Static(_) => {
.context(AuthenticationClassRetrievalSnafu)?;
let auth_class_name = auth_class.name_any();

user_registration = auth_details.user_registration;
user_registration_role = auth_details.user_registration_role.clone();
sync_roles_at = auth_details.sync_roles_at.clone();

Some(match auth_class.spec.provider {
AuthenticationClassProvider::Ldap(provider) => {
SupersetAuthenticationClassResolved::Ldap { provider }
}
AuthenticationClassProvider::Oidc(provider) => {
if &provider.principal_claim != "preferred_username" {
return OidcPrincipalClaimNotSupportedSnafu {
configured: provider.principal_claim.clone(),
supported: "preferred_username".to_owned(),
auth_class_name: auth_class_name,
}
.fail();
}
SupersetAuthenticationClassResolved::Oidc {
provider,
oidc: auth_details
.common
.oidc_or_error(&auth_class_name)
.context(OidcConfigurationSnafu)?
.clone(),
}
}
_ => {
// Checking for supported AuthenticationClass here is a little out of place,
// but is does not make sense to iterate further after finding an unsupported
// AuthenticationClass.
return Err(Error::AuthenticationProviderNotSupported {
authentication_class: ObjectRef::from_obj(&resolved),
provider: resolved.spec.provider.to_string(),
})
auth_class: auth_class_name,
provider: auth_class.spec.provider.to_string(),
});
}
})
} else {
None
};

resolved.push(SuperSetAuthenticationConfigResolved {
authentication_class: auth_class,
user_registration: config.user_registration,
user_registration_role: config.user_registration_role.clone(),
sync_roles_at: config.sync_roles_at.clone(),
})
}

Ok(resolved)
}
None => None,
};

Ok(SupersetAuthenticationConfigResolved {
authentication_class_resolved,
user_registration,
user_registration_role,
sync_roles_at,
})
}
}

#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
pub enum FlaskRolesSyncMoment {
#[default]
Registration,
Login,
}
13 changes: 10 additions & 3 deletions rust/crd/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::collections::BTreeMap;

use authentication::SupersetClientAuthenticationDetails;
use product_config::flask_app_config_writer::{FlaskAppConfigOptions, PythonType};
use serde::{Deserialize, Serialize};
use snafu::{OptionExt, ResultExt, Snafu};
Expand All @@ -26,7 +27,7 @@ use stackable_operator::{
};
use strum::{Display, EnumIter, EnumString, IntoEnumIterator};

use crate::{affinity::get_affinity, authentication::SupersetAuthentication};
use crate::affinity::get_affinity;

pub mod affinity;
pub mod authentication;
Expand Down Expand Up @@ -62,6 +63,7 @@ pub enum SupersetConfigOptions {
StatsLogger,
RowLimit,
MapboxApiKey,
OauthProviders,
SupersetWebserverTimeout,
LoggingConfigurator,
AuthType,
Expand Down Expand Up @@ -108,6 +110,7 @@ impl FlaskAppConfigOptions for SupersetConfigOptions {
SupersetConfigOptions::StatsLogger => PythonType::Expression,
SupersetConfigOptions::RowLimit => PythonType::IntLiteral,
SupersetConfigOptions::MapboxApiKey => PythonType::Expression,
SupersetConfigOptions::OauthProviders => PythonType::Expression,
SupersetConfigOptions::SupersetWebserverTimeout => PythonType::IntLiteral,
SupersetConfigOptions::LoggingConfigurator => PythonType::Expression,
SupersetConfigOptions::AuthType => PythonType::Expression,
Expand Down Expand Up @@ -162,12 +165,14 @@ pub struct SupersetClusterSpec {
#[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SupersetClusterConfig {
#[serde(flatten)]
pub authentication: SupersetAuthentication,
pub authentication: Vec<SupersetClientAuthenticationDetails>,

pub credentials_secret: String,

/// Cluster operations like pause reconciliation or cluster stop.
#[serde(default)]
pub cluster_operation: ClusterOperation,

/// This field controls which type of Service the Operator creates for this SupersetCluster:
///
/// * cluster-internal: Use a ClusterIP service
Expand All @@ -181,8 +186,10 @@ pub struct SupersetClusterConfig {
/// will be used to expose the service, and ListenerClass names will stay the same, allowing for a non-breaking change.
#[serde(default)]
pub listener_class: CurrentlySupportedListenerClasses,

#[serde(skip_serializing_if = "Option::is_none")]
pub mapbox_secret: Option<String>,

/// Name of the Vector aggregator discovery ConfigMap.
/// It must contain the key `ADDRESS` with the address of the Vector aggregator.
#[serde(skip_serializing_if = "Option::is_none")]
Expand Down
12 changes: 12 additions & 0 deletions rust/operator-binary/src/commands.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/// Adds a CA file from `cert_file` into a truststore named `truststore.p12` in `destination_directory`
/// under the alias `alias_name`.
pub fn add_cert_to_system_truststore_command(cert_file: &str) -> String {
format!(
"mkdir -p /stackable/certs/
HASH=$(openssl x509 -subject_hash -in /stackable/secrets/tls/ca.crt -nocert)
cp {cert_file} /stackable/certs/${{HASH}}.0
# cp {cert_file} /stackable/certs/${{HASH}}.0
# TODO call python -c \"import ssl; print(ssl.get_default_verify_paths())\"
cat {cert_file} >> \"$(python -c 'import certifi; print(certifi.where())')\""
)
}
Loading