|
| 1 | +use serde::{Deserialize, Serialize}; |
| 2 | +use snafu::{ResultExt, Snafu}; |
| 3 | +use stackable_operator::commons::authentication::AuthenticationClassProvider; |
| 4 | +use stackable_operator::{ |
| 5 | + client::Client, |
| 6 | + commons::authentication::AuthenticationClass, |
| 7 | + kube::runtime::reflector::ObjectRef, |
| 8 | + schemars::{self, JsonSchema}, |
| 9 | +}; |
| 10 | + |
| 11 | +const SUPPORTED_AUTHENTICATION_CLASS_PROVIDERS: [&str; 1] = ["LDAP"]; |
| 12 | + |
| 13 | +#[derive(Snafu, Debug)] |
| 14 | +pub enum Error { |
| 15 | + #[snafu(display("Failed to retrieve AuthenticationClass {authentication_class}"))] |
| 16 | + AuthenticationClassRetrieval { |
| 17 | + source: stackable_operator::error::Error, |
| 18 | + authentication_class: ObjectRef<AuthenticationClass>, |
| 19 | + }, |
| 20 | + // TODO: Adapt message if multiple authentication classes are supported simultaneously |
| 21 | + #[snafu(display("Only one authentication class is currently supported at a time"))] |
| 22 | + MultipleAuthenticationClassesProvided, |
| 23 | + #[snafu(display( |
| 24 | + "Failed to use authentication provider [{provider}] for authentication class [{authentication_class}] - supported providers: {SUPPORTED_AUTHENTICATION_CLASS_PROVIDERS:?}", |
| 25 | + ))] |
| 26 | + AuthenticationProviderNotSupported { |
| 27 | + authentication_class: ObjectRef<AuthenticationClass>, |
| 28 | + provider: String, |
| 29 | + }, |
| 30 | +} |
| 31 | + |
| 32 | +type Result<T, E = Error> = std::result::Result<T, E>; |
| 33 | + |
| 34 | +/// Resolved counter part for `SuperSetAuthenticationConfig`. |
| 35 | +pub struct SuperSetAuthenticationConfigResolved { |
| 36 | + pub authentication_class: Option<AuthenticationClass>, |
| 37 | + pub user_registration: bool, |
| 38 | + pub user_registration_role: String, |
| 39 | + pub sync_roles_at: FlaskRolesSyncMoment, |
| 40 | +} |
| 41 | + |
| 42 | +#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] |
| 43 | +#[serde(rename_all = "camelCase")] |
| 44 | +pub struct SupersetAuthentication { |
| 45 | + #[serde(default)] |
| 46 | + authentication: Vec<SuperSetAuthenticationConfig>, |
| 47 | +} |
| 48 | + |
| 49 | +impl SupersetAuthentication { |
| 50 | + pub fn authentication_class_names(&self) -> Vec<&str> { |
| 51 | + let mut auth_classes = vec![]; |
| 52 | + for config in &self.authentication { |
| 53 | + if let Some(auth_config) = &config.authentication_class { |
| 54 | + auth_classes.push(auth_config.as_str()); |
| 55 | + } |
| 56 | + } |
| 57 | + auth_classes |
| 58 | + } |
| 59 | +} |
| 60 | + |
| 61 | +#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] |
| 62 | +#[serde(rename_all = "camelCase")] |
| 63 | +pub struct SuperSetAuthenticationConfig { |
| 64 | + /// Name of the AuthenticationClass used to authenticate the users. |
| 65 | + /// At the moment only LDAP is supported. |
| 66 | + /// If not specified the default authentication (AUTH_DB) will be used. |
| 67 | + pub authentication_class: Option<String>, |
| 68 | + /// Allow users who are not already in the FAB DB. |
| 69 | + /// Gets mapped to `AUTH_USER_REGISTRATION` |
| 70 | + #[serde(default = "default_user_registration")] |
| 71 | + pub user_registration: bool, |
| 72 | + /// This role will be given in addition to any AUTH_ROLES_MAPPING. |
| 73 | + /// Gets mapped to `AUTH_USER_REGISTRATION_ROLE` |
| 74 | + #[serde(default = "default_user_registration_role")] |
| 75 | + pub user_registration_role: String, |
| 76 | + /// If we should replace ALL the user's roles each login, or only on registration. |
| 77 | + /// Gets mapped to `AUTH_ROLES_SYNC_AT_LOGIN` |
| 78 | + #[serde(default = "default_sync_roles_at")] |
| 79 | + pub sync_roles_at: FlaskRolesSyncMoment, |
| 80 | +} |
| 81 | + |
| 82 | +pub fn default_user_registration() -> bool { |
| 83 | + true |
| 84 | +} |
| 85 | + |
| 86 | +pub fn default_user_registration_role() -> String { |
| 87 | + "Public".to_string() |
| 88 | +} |
| 89 | + |
| 90 | +/// Matches Flask's default mode of syncing at registration |
| 91 | +pub fn default_sync_roles_at() -> FlaskRolesSyncMoment { |
| 92 | + FlaskRolesSyncMoment::Registration |
| 93 | +} |
| 94 | + |
| 95 | +#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] |
| 96 | +pub enum FlaskRolesSyncMoment { |
| 97 | + Registration, |
| 98 | + Login, |
| 99 | +} |
| 100 | + |
| 101 | +impl SupersetAuthentication { |
| 102 | + /// Retrieve all provided `AuthenticationClass` references. |
| 103 | + pub async fn resolve( |
| 104 | + &self, |
| 105 | + client: &Client, |
| 106 | + ) -> Result<Vec<SuperSetAuthenticationConfigResolved>> { |
| 107 | + let mut resolved = vec![]; |
| 108 | + |
| 109 | + // TODO: adapt if multiple authentication classes are supported by superset. |
| 110 | + // This is currently not possible due to the Flask App Builder not supporting it. |
| 111 | + if self.authentication.len() > 1 { |
| 112 | + return Err(Error::MultipleAuthenticationClassesProvided); |
| 113 | + } |
| 114 | + |
| 115 | + for config in &self.authentication { |
| 116 | + let auth_class = if let Some(auth_class) = &config.authentication_class { |
| 117 | + let resolved = AuthenticationClass::resolve(client, auth_class) |
| 118 | + .await |
| 119 | + .context(AuthenticationClassRetrievalSnafu { |
| 120 | + authentication_class: ObjectRef::<AuthenticationClass>::new(auth_class), |
| 121 | + })?; |
| 122 | + |
| 123 | + // Checking for supported AuthenticationClass here is a little out of place, but is does not |
| 124 | + // make sense to iterate further after finding an unsupported AuthenticationClass. |
| 125 | + Some(match resolved.spec.provider { |
| 126 | + AuthenticationClassProvider::Ldap(_) => resolved, |
| 127 | + AuthenticationClassProvider::Tls(_) |
| 128 | + | AuthenticationClassProvider::Static(_) => { |
| 129 | + return Err(Error::AuthenticationProviderNotSupported { |
| 130 | + authentication_class: ObjectRef::from_obj(&resolved), |
| 131 | + provider: resolved.spec.provider.to_string(), |
| 132 | + }) |
| 133 | + } |
| 134 | + }) |
| 135 | + } else { |
| 136 | + None |
| 137 | + }; |
| 138 | + |
| 139 | + resolved.push(SuperSetAuthenticationConfigResolved { |
| 140 | + authentication_class: auth_class, |
| 141 | + user_registration: config.user_registration, |
| 142 | + user_registration_role: config.user_registration_role.clone(), |
| 143 | + sync_roles_at: config.sync_roles_at.clone(), |
| 144 | + }) |
| 145 | + } |
| 146 | + |
| 147 | + Ok(resolved) |
| 148 | + } |
| 149 | +} |
0 commit comments