use std::collections::HashMap;
use crate::validation::context::ValidationContext as Ctx;
use crate::validation::model::RealmConfigError;
use crate::validation::realm_errors;
use crate::{ClientRepresentation, RealmRepresentation};
pub async fn validate_realm(ctx: &Ctx<'_>) -> anyhow::Result<Option<Vec<RealmConfigError>>> {
let mut errors = vec![];
let realm = ctx.cfg().realm();
let client_id = ctx.cfg().client_id();
tracing::info!("validating realm '{realm}'");
check_realm_settings(ctx, realm, &mut errors).await?;
tracing::info!("validating realm client '{client_id}'");
check_client(ctx, realm, client_id, &mut errors).await?;
Ok(Some(errors))
}
async fn check_realm_settings(
ctx: &Ctx<'_>,
realm: &str,
errors: &mut Vec<RealmConfigError>,
) -> anyhow::Result<()> {
let rep: RealmRepresentation = ctx.keycloak().realm_by_name(realm).await?;
if let Some(locale) = &rep.default_locale {
if locale != "de" {
add_error(
realm_errors::REALM_DEFAULT_LOCALE_INVALID_ID,
realm_errors::REALM_DEFAULT_LOCALE_INVALID_KEY,
errors,
);
}
} else {
add_error(
realm_errors::REALM_DEFAULT_LOCALE_MISSING_ID,
realm_errors::REALM_DEFAULT_LOCALE_MISSING_KEY,
errors,
);
}
if !rep.internationalization_enabled.unwrap_or(false) {
add_error(
realm_errors::REALM_INTERNATIONALIZATION_ENABLED_ID,
realm_errors::REALM_INTERNATIONALIZATION_ENABLED_KEY,
errors,
);
}
if let Some(theme) = &rep.login_theme {
if theme != ctx.keycloak().config().theme() {
add_error(
realm_errors::REALM_LOGIN_THEME_INVALID_ID,
realm_errors::REALM_LOGIN_THEME_INVALID_KEY,
errors,
);
}
} else {
add_error(
realm_errors::REALM_LOGIN_THEME_MISSING_ID,
realm_errors::REALM_LOGIN_THEME_MISSING_KEY,
errors,
);
}
if let Some(email_theme) = &rep.email_theme {
if email_theme != ctx.keycloak().config().email_theme() {
add_error(
realm_errors::REALM_EMAIL_THEME_INVALID_ID,
realm_errors::REALM_EMAIL_THEME_INVALID_KEY,
errors,
);
}
} else {
add_error(
realm_errors::REALM_EMAIL_THEME_MISSING_ID,
realm_errors::REALM_EMAIL_THEME_MISSING_KEY,
errors,
);
}
if let Some(policy) = &rep.password_policy {
if !policy.contains("length(8)") {
add_error(
realm_errors::REALM_PASSWORD_POLICY_LENGTH_ID,
realm_errors::REALM_PASSWORD_POLICY_LENGTH_KEY,
errors,
);
}
if !policy.contains("specialChars(1)") {
add_error(
realm_errors::REALM_PASSWORD_POLICY_SYMBOL_ID,
realm_errors::REALM_PASSWORD_POLICY_SYMBOL_KEY,
errors,
);
}
if !policy.contains("upperCase(1)") {
add_error(
realm_errors::REALM_PASSWORD_POLICY_UPPERCASE_ID,
realm_errors::REALM_PASSWORD_POLICY_UPPERCASE_KEY,
errors,
);
}
if !policy.contains("lowerCase(1)") {
add_error(
realm_errors::REALM_PASSWORD_POLICY_LOWERCASE_ID,
realm_errors::REALM_PASSWORD_POLICY_LOWERCASE_KEY,
errors,
);
}
if !policy.contains("digits(1)") {
add_error(
realm_errors::REALM_PASSWORD_POLICY_DIGIT_ID,
realm_errors::REALM_PASSWORD_POLICY_DIGIT_KEY,
errors,
);
}
} else {
add_error(
realm_errors::REALM_PASSWORD_POLICY_MISSING_ID,
realm_errors::REALM_PASSWORD_POLICY_MISSING_KEY,
errors,
);
}
if !rep.remember_me.unwrap_or(false) {
add_error(
realm_errors::REALM_REMEMBER_ME_ID,
realm_errors::REALM_REMEMBER_ME_KEY,
errors,
);
}
if rep.registration_allowed.unwrap_or(false) {
add_error(
realm_errors::REALM_REGISTRATION_ALLOWED_ID,
realm_errors::REALM_REGISTRATION_ALLOWED_KEY,
errors,
);
}
if !rep.reset_password_allowed.unwrap_or(false) {
add_error(
realm_errors::REALM_RESET_PASSWORD_ALLOWED_ID,
realm_errors::REALM_RESET_PASSWORD_ALLOWED_KEY,
errors,
);
}
if let Some(locales) = &rep.supported_locales {
if !locales.contains(&"de".to_string()) {
add_error(
realm_errors::REALM_SUPPORTED_LOCALES_INVALID_ID,
realm_errors::REALM_SUPPORTED_LOCALES_INVALID_KEY,
errors,
);
}
} else {
add_error(
realm_errors::REALM_SUPPORTED_LOCALES_MISSING_ID,
realm_errors::REALM_SUPPORTED_LOCALES_MISSING_KEY,
errors,
);
}
if let Some(smtp_server) = &rep.smtp_server {
check_realm_smtp_settings(ctx, smtp_server, errors);
} else {
add_error(
realm_errors::REALM_SMTP_SERVER_MISSING_ID,
realm_errors::REALM_SMTP_SERVER_MISSING_KEY,
errors,
);
}
let authentication_flows = ctx.keycloak().get_authentication_flows(realm).await?;
let browser_flow_config = ctx.keycloak().config().browser_flow();
if browser_flow_config == "browser_email_otp"
&& !authentication_flows
.iter()
.any(|flow| flow.alias.as_deref() == Some("browser_email_otp"))
{
add_error(
realm_errors::REALM_AUTHENTICATION_FLOW_2FAEMAIL_MISSING_ID,
realm_errors::REALM_AUTHENTICATION_FLOW_2FAEMAIL_MISSING_KEY,
errors,
);
}
if let Some(browser_flow) = &rep.browser_flow {
if browser_flow != browser_flow_config {
add_error(
realm_errors::REALM_BROWSER_FLOW_INVALID_ID,
realm_errors::REALM_BROWSER_FLOW_INVALID_KEY,
errors,
);
}
} else {
add_error(
realm_errors::REALM_BROWSER_FLOW_MISSING_ID,
realm_errors::REALM_BROWSER_FLOW_MISSING_KEY,
errors,
);
}
if rep.duplicate_emails_allowed.unwrap_or_default()
!= ctx.keycloak().config().duplicate_emails_allowed()
{
add_error(
realm_errors::REALM_DUPLICATE_EMAILS_ALLOWED_MISMATCHED_ID,
realm_errors::REALM_DUPLICATE_EMAILS_ALLOWED_MISMATCHED_KEY,
errors,
);
}
if rep.edit_username_allowed.unwrap_or_default()
!= ctx.keycloak().config().edit_username_allowed()
{
add_error(
realm_errors::REALM_EDIT_USERNAME_ALLOWED_MISMATCHED_ID,
realm_errors::REALM_EDIT_USERNAME_ALLOWED_MISMATCHED_KEY,
errors,
);
}
Ok(())
}
async fn check_client(
ctx: &Ctx<'_>,
realm: &str,
client_id: &str,
errors: &mut Vec<RealmConfigError>,
) -> anyhow::Result<()> {
let rep: Option<ClientRepresentation> = ctx
.keycloak()
.get_client(realm) .await?;
if let Some(client) = rep {
if let Some(attributes) = &client.attributes {
let oauth2_device_authorization_opt =
attributes.get("oauth2.device.authorization.grant.enabled");
let backchannel_logout_opt = attributes.get("backchannel.logout.url");
if let Some(oauth2_device_authorization) = oauth2_device_authorization_opt {
if oauth2_device_authorization.as_str() != "false" {
add_error(
realm_errors::CLIENTS_CLIENT_ATTRIBUTES_OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED_INVALID_ID,
realm_errors::CLIENTS_CLIENT_ATTRIBUTES_OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED_INVALID_KEY,
errors,
);
}
} else {
add_error(
realm_errors::CLIENTS_CLIENT_ATTRIBUTES_OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED_MISSING_ID,
realm_errors::CLIENTS_CLIENT_ATTRIBUTES_OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED_MISSING_KEY,
errors,
);
}
if let Some(backchannel_logout) = backchannel_logout_opt {
if backchannel_logout.as_str().is_empty() {
add_error(
realm_errors::CLIENTS_CLIENT_ATTRIBUTES_BACKCHANNEL_LOGOUT_DISABLED_ID,
realm_errors::CLIENTS_CLIENT_ATTRIBUTES_BACKCHANNEL_LOGOUT_DISABLED_KEY,
errors,
)
}
} else {
add_error(
realm_errors::CLIENTS_CLIENT_ATTRIBUTES_BACKCHANNEL_LOGOUT_DISABLED_ID,
realm_errors::CLIENTS_CLIENT_ATTRIBUTES_BACKCHANNEL_LOGOUT_DISABLED_KEY,
errors,
)
}
} else {
add_error(
realm_errors::CLIENTS_CLIENT_ATTRIBUTES_MISSING_ID,
realm_errors::CLIENTS_CLIENT_ATTRIBUTES_MISSING_KEY,
errors,
);
}
if let Some(url) = &client.base_url {
if url.trim_end_matches('/') != ctx.cfg().base_url().trim_end_matches('/') {
tracing::info!(
"[{}]: Expected the 'base_url' value to be '{}' but was '{}'",
realm,
ctx.cfg().base_url().trim_end_matches('/'),
url.trim_end_matches('/')
);
add_error(
realm_errors::CLIENTS_CLIENT_BASE_URL_INVALID_ID,
realm_errors::CLIENTS_CLIENT_BASE_URL_INVALID_KEY,
errors,
);
}
} else {
add_error(
realm_errors::CLIENTS_CLIENT_BASE_URL_MISSING_ID,
realm_errors::CLIENTS_CLIENT_BASE_URL_MISSING_KEY,
errors,
);
}
if client.client_id.unwrap_or_default() != client_id {
add_error(
realm_errors::CLIENTS_CLIENT_CLIENT_ID_ID,
realm_errors::CLIENTS_CLIENT_CLIENT_ID_KEY,
errors,
);
}
if client.consent_required.unwrap_or(false) {
add_error(
realm_errors::CLIENTS_CLIENT_CONSENT_REQUIRED_ID,
realm_errors::CLIENTS_CLIENT_CONSENT_REQUIRED_KEY,
errors,
);
}
if !client.enabled.unwrap_or(false) {
add_error(
realm_errors::CLIENTS_CLIENT_ENABLED_ID,
realm_errors::CLIENTS_CLIENT_ENABLED_KEY,
errors,
);
}
if client.implicit_flow_enabled.unwrap_or(false) {
add_error(
realm_errors::CLIENTS_CLIENT_IMPLICIT_FLOW_ENABLED_ID,
realm_errors::CLIENTS_CLIENT_IMPLICIT_FLOW_ENABLED_KEY,
errors,
);
}
if !client.public_client.unwrap_or(false) {
add_error(
realm_errors::CLIENTS_CLIENT_PUBLIC_CLIENT_ID,
realm_errors::CLIENTS_CLIENT_PUBLIC_CLIENT_KEY,
errors,
);
}
if let Some(urls) = &client.redirect_uris {
if !urls.iter().all(|url| {
ctx.cfg().public_urls().contains(&&**url)
|| ctx
.cfg()
.public_urls()
.contains(&&*url.trim_end_matches(['*', '/']))
}) {
tracing::info!(
"[{}]: Expected the 'redirect_uris' values '{:?}' to match matches '{:?}'",
realm,
urls,
ctx.cfg().public_urls()
);
add_error(
realm_errors::CLIENTS_CLIENT_REDIRECT_URIS_INVALID_ID,
realm_errors::CLIENTS_CLIENT_REDIRECT_URIS_INVALID_KEY,
errors,
);
}
} else {
add_error(
realm_errors::CLIENTS_CLIENT_REDIRECT_URIS_MISSING_ID,
realm_errors::CLIENTS_CLIENT_REDIRECT_URIS_MISSING_KEY,
errors,
);
}
if let Some(url) = &client.root_url {
if url.trim_end_matches('/') != ctx.cfg().base_url().trim_end_matches('/') {
tracing::info!(
"[{}]: Expected the 'root_url' value to be '{}' but was '{}'",
realm,
ctx.cfg().base_url().trim_end_matches('/'),
url.trim_end_matches('/')
);
add_error(
realm_errors::CLIENTS_CLIENT_ROOT_URL_INVALID_ID,
realm_errors::CLIENTS_CLIENT_ROOT_URL_INVALID_KEY,
errors,
);
}
} else {
add_error(
realm_errors::CLIENTS_CLIENT_ROOT_URL_MISSING_ID,
realm_errors::CLIENTS_CLIENT_ROOT_URL_MISSING_KEY,
errors,
);
}
if client.service_accounts_enabled.unwrap_or(false) {
add_error(
realm_errors::CLIENTS_CLIENT_SERVICE_ACCOUNTS_ENABLED_ID,
realm_errors::CLIENTS_CLIENT_SERVICE_ACCOUNTS_ENABLED_KEY,
errors,
);
}
if !client.standard_flow_enabled.unwrap_or(false) {
add_error(
realm_errors::CLIENTS_CLIENT_STANDARD_FLOW_ENABLED_ID,
realm_errors::CLIENTS_CLIENT_STANDARD_FLOW_ENABLED_KEY,
errors,
);
}
if client.frontchannel_logout.unwrap_or(false) {
add_error(
realm_errors::CLIENTS_CLIENT_FRONTCHANNEL_LOGOUT_ENABLED_ID,
realm_errors::CLIENTS_CLIENT_FRONTCHANNEL_LOGOUT_ENABLED_KEY,
errors,
);
}
} else {
add_error(
realm_errors::CLIENTS_CLIENT_MISSING_ID,
realm_errors::CLIENTS_CLIENT_MISSING_KEY,
errors,
);
}
Ok(())
}
fn add_error<S>(error_id: S, error_key: S, errors: &mut Vec<RealmConfigError>)
where
S: Into<String>,
{
errors.push(RealmConfigError::new(error_id.into(), error_key.into()));
}
fn check_realm_smtp_settings(
ctx: &Ctx<'_>,
smtp_server: &HashMap<String, String>,
errors: &mut Vec<RealmConfigError>,
) {
if let Some(configured_reply_to_display_name) =
ctx.cfg().keycloak().smtp_reply_to_display_name()
{
if let Some(reply_to_display_name) = smtp_server.get("replyToDisplayName") {
if configured_reply_to_display_name != reply_to_display_name {
tracing::info!(
"The configured 'KEYCLOAK_SMTP_REPLY_TO_DISPLAY_NAME' '{}' does not match with the value from keycloak '{}'",
configured_reply_to_display_name,
reply_to_display_name
);
add_error(
realm_errors::REALM_SMTP_SERVER_REPLY_TO_DISPLAY_NAME_MISMATCHED_ID,
realm_errors::REALM_SMTP_SERVER_REPLY_TO_DISPLAY_NAME_MISMATCHED_KEY,
errors,
);
}
} else {
add_error(
realm_errors::REALM_SMTP_SERVER_REPLY_TO_DISPLAY_NAME_MISSING_ID,
realm_errors::REALM_SMTP_SERVER_REPLY_TO_DISPLAY_NAME_MISSING_KEY,
errors,
);
}
}
if let Some(starttls_value) = smtp_server.get("starttls") {
let starttls = get_bool_from_string_value(starttls_value);
if let Some(configured_starttls) = ctx.cfg().keycloak().smtp_starttls() {
if configured_starttls != &starttls {
tracing::info!(
"The configured 'KEYCLOAK_SMTP_STARTTLS' '{}' does not match with the value from keycloak '{}'",
configured_starttls,
starttls
);
add_error(
realm_errors::REALM_SMTP_SERVER_STARTTLS_MISMATCHED_ID,
realm_errors::REALM_SMTP_SERVER_STARTTLS_MISMATCHED_KEY,
errors,
);
}
} else if starttls {
add_error(
realm_errors::REALM_SMTP_SERVER_STARTTLS_INVALID_ID,
realm_errors::REALM_SMTP_SERVER_STARTTLS_INVALID_KEY,
errors,
);
}
} else {
add_error(
realm_errors::REALM_SMTP_SERVER_STARTTLS_MISSING_ID,
realm_errors::REALM_SMTP_SERVER_STARTTLS_MISSING_KEY,
errors,
);
}
if let Some(port_value) = smtp_server.get("port") {
let port = get_u16_from_value(port_value);
if let Some(configured_port) = ctx.cfg().keycloak().smtp_port() {
if configured_port != &port {
tracing::info!(
"The configured 'KEYCLOAK_SMTP_PORT' '{}' does not match with the value from keycloak '{}'",
configured_port,
port
);
add_error(
realm_errors::REALM_SMTP_SERVER_PORT_MISMATCHED_ID,
realm_errors::REALM_SMTP_SERVER_PORT_MISMATCHED_KEY,
errors,
);
}
} else if port != 1025 {
add_error(
realm_errors::REALM_SMTP_SERVER_PORT_INVALID_ID,
realm_errors::REALM_SMTP_SERVER_PORT_INVALID_KEY,
errors,
);
}
} else {
add_error(
realm_errors::REALM_SMTP_SERVER_PORT_MISSING_ID,
realm_errors::REALM_SMTP_SERVER_PORT_MISSING_KEY,
errors,
);
}
if let Some(host) = smtp_server.get("host") {
if let Some(configured_host) = ctx.cfg().keycloak().smtp_host() {
if configured_host != host {
tracing::info!(
"The configured 'KEYCLOAK_SMTP_HOST' '{}' does not match with the value from keycloak '{}'",
configured_host,
host
);
add_error(
realm_errors::REALM_SMTP_SERVER_HOST_MISMATCHED_ID,
realm_errors::REALM_SMTP_SERVER_HOST_MISMATCHED_KEY,
errors,
);
}
} else if host != "smtp" {
add_error(
realm_errors::REALM_SMTP_SERVER_HOST_INVALID_ID,
realm_errors::REALM_SMTP_SERVER_HOST_INVALID_KEY,
errors,
);
}
} else {
add_error(
realm_errors::REALM_SMTP_SERVER_HOST_MISSING_ID,
realm_errors::REALM_SMTP_SERVER_HOST_MISSING_KEY,
errors,
);
}
if let Some(configured_reply_to) = ctx.cfg().keycloak().smtp_reply_to() {
if let Some(reply_to) = smtp_server.get("replyTo") {
if configured_reply_to != reply_to {
tracing::info!(
"The configured 'KEYCLOAK_SMTP_REPLY_TO' '{}' does not match with the value in keycloak '{}'",
configured_reply_to,
reply_to
);
add_error(
realm_errors::REALM_SMTP_SERVER_REPLY_TO_MISMATCHED_ID,
realm_errors::REALM_SMTP_SERVER_REPLY_TO_MISMATCHED_KEY,
errors,
);
}
} else {
add_error(
realm_errors::REALM_SMTP_SERVER_REPLY_TO_MISSING_ID,
realm_errors::REALM_SMTP_SERVER_REPLY_TO_MISSING_KEY,
errors,
);
}
}
if let Some(from) = smtp_server.get("from") {
if let Some(configured_from) = ctx.cfg().keycloak().smtp_from() {
if configured_from != from {
tracing::info!(
"The configured 'KEYCLOAK_SMTP_FROM' '{}' does not match with the value from keycloak '{}'",
configured_from,
from
);
add_error(
realm_errors::REALM_SMTP_SERVER_FROM_MISMATCHED_ID,
realm_errors::REALM_SMTP_SERVER_FROM_MISMATCHED_KEY,
errors,
);
}
} else if from != "noreply@qm.local" {
add_error(
realm_errors::REALM_SMTP_SERVER_FROM_INVALID_ID,
realm_errors::REALM_SMTP_SERVER_FROM_INVALID_KEY,
errors,
);
}
} else {
add_error(
realm_errors::REALM_SMTP_SERVER_FROM_MISSING_ID,
realm_errors::REALM_SMTP_SERVER_FROM_MISSING_KEY,
errors,
);
}
if let Some(configured_from_display_name) = ctx.cfg().keycloak().smtp_from_display_name() {
if let Some(from_display_name) = smtp_server.get("fromDisplayName") {
if configured_from_display_name != from_display_name {
tracing::info!(
"The configured 'KEYCLOAK_SMTP_FROM_DISPLAY_NAME' '{}' does not match with the value in keycloak '{}'",
configured_from_display_name,
from_display_name
);
add_error(
realm_errors::REALM_SMTP_SERVER_FROM_DISPLAY_NAME_MISMATCHED_ID,
realm_errors::REALM_SMTP_SERVER_FROM_DISPLAY_NAME_MISMATCHED_KEY,
errors,
);
}
} else {
add_error(
realm_errors::REALM_SMTP_SERVER_FROM_DISPLAY_NAME_MISSING_ID,
realm_errors::REALM_SMTP_SERVER_FROM_DISPLAY_NAME_MISSING_KEY,
errors,
);
}
}
if let Some(ssl_value) = smtp_server.get("ssl") {
let ssl = get_bool_from_string_value(ssl_value);
if let Some(configured_ssl) = ctx.cfg().keycloak().smtp_ssl() {
if configured_ssl != &ssl {
tracing::info!(
"The configured 'KEYCLOAK_SMTP_SSL' '{}' does not match with the value from keycloak '{}'",
configured_ssl,
ssl
);
add_error(
realm_errors::REALM_SMTP_SERVER_SSL_MISMATCHED_ID,
realm_errors::REALM_SMTP_SERVER_SSL_MISMATCHED_KEY,
errors,
);
}
} else if ssl {
add_error(
realm_errors::REALM_SMTP_SERVER_SSL_INVALID_ID,
realm_errors::REALM_SMTP_SERVER_SSL_INVALID_KEY,
errors,
);
}
} else {
add_error(
realm_errors::REALM_SMTP_SERVER_SSL_MISSING_ID,
realm_errors::REALM_SMTP_SERVER_SSL_MISSING_KEY,
errors,
);
}
}
fn get_bool_from_string_value(value: &str) -> bool {
matches!(value, "true")
}
fn get_u16_from_value(value: &str) -> u16 {
value.parse::<u16>().unwrap_or(0)
}