2026-03-31 19:19:04 +08:00
|
|
|
use async_trait::async_trait;
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
|
|
|
|
pub struct StoredOAuthProviderConfig {
|
|
|
|
|
pub provider_type: String,
|
|
|
|
|
pub display_name: String,
|
|
|
|
|
pub client_id: String,
|
|
|
|
|
pub client_secret_encrypted: Option<String>,
|
|
|
|
|
pub authorization_url_override: Option<String>,
|
|
|
|
|
pub token_url_override: Option<String>,
|
|
|
|
|
pub userinfo_url_override: Option<String>,
|
|
|
|
|
pub scopes: Option<Vec<String>>,
|
|
|
|
|
pub redirect_uri: String,
|
|
|
|
|
pub frontend_callback_url: String,
|
|
|
|
|
pub attribute_mapping: Option<serde_json::Value>,
|
|
|
|
|
pub extra_config: Option<serde_json::Value>,
|
2026-05-17 21:22:20 +08:00
|
|
|
pub icon_url: Option<String>,
|
2026-03-31 19:19:04 +08:00
|
|
|
pub is_enabled: bool,
|
2026-04-10 01:46:14 +08:00
|
|
|
pub created_at_unix_ms: Option<u64>,
|
2026-03-31 19:19:04 +08:00
|
|
|
pub updated_at_unix_secs: Option<u64>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl StoredOAuthProviderConfig {
|
|
|
|
|
pub fn new(
|
|
|
|
|
provider_type: String,
|
|
|
|
|
display_name: String,
|
|
|
|
|
client_id: String,
|
|
|
|
|
redirect_uri: String,
|
|
|
|
|
frontend_callback_url: String,
|
|
|
|
|
) -> Result<Self, crate::DataLayerError> {
|
|
|
|
|
if provider_type.trim().is_empty() {
|
|
|
|
|
return Err(crate::DataLayerError::UnexpectedValue(
|
|
|
|
|
"oauth_providers.provider_type is empty".to_string(),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
if display_name.trim().is_empty() {
|
|
|
|
|
return Err(crate::DataLayerError::UnexpectedValue(
|
|
|
|
|
"oauth_providers.display_name is empty".to_string(),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
if client_id.trim().is_empty() {
|
|
|
|
|
return Err(crate::DataLayerError::UnexpectedValue(
|
|
|
|
|
"oauth_providers.client_id is empty".to_string(),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
if redirect_uri.trim().is_empty() {
|
|
|
|
|
return Err(crate::DataLayerError::UnexpectedValue(
|
|
|
|
|
"oauth_providers.redirect_uri is empty".to_string(),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
if frontend_callback_url.trim().is_empty() {
|
|
|
|
|
return Err(crate::DataLayerError::UnexpectedValue(
|
|
|
|
|
"oauth_providers.frontend_callback_url is empty".to_string(),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(Self {
|
|
|
|
|
provider_type,
|
|
|
|
|
display_name,
|
|
|
|
|
client_id,
|
|
|
|
|
client_secret_encrypted: None,
|
|
|
|
|
authorization_url_override: None,
|
|
|
|
|
token_url_override: None,
|
|
|
|
|
userinfo_url_override: None,
|
|
|
|
|
scopes: None,
|
|
|
|
|
redirect_uri,
|
|
|
|
|
frontend_callback_url,
|
|
|
|
|
attribute_mapping: None,
|
|
|
|
|
extra_config: None,
|
2026-05-17 21:22:20 +08:00
|
|
|
icon_url: None,
|
2026-03-31 19:19:04 +08:00
|
|
|
is_enabled: false,
|
2026-04-10 01:46:14 +08:00
|
|
|
created_at_unix_ms: None,
|
2026-03-31 19:19:04 +08:00
|
|
|
updated_at_unix_secs: None,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
|
|
|
pub fn with_config_fields(
|
|
|
|
|
mut self,
|
|
|
|
|
client_secret_encrypted: Option<String>,
|
|
|
|
|
authorization_url_override: Option<String>,
|
|
|
|
|
token_url_override: Option<String>,
|
|
|
|
|
userinfo_url_override: Option<String>,
|
|
|
|
|
scopes: Option<Vec<String>>,
|
|
|
|
|
attribute_mapping: Option<serde_json::Value>,
|
|
|
|
|
extra_config: Option<serde_json::Value>,
|
2026-05-17 21:22:20 +08:00
|
|
|
icon_url: Option<String>,
|
2026-03-31 19:19:04 +08:00
|
|
|
is_enabled: bool,
|
|
|
|
|
) -> Self {
|
|
|
|
|
self.client_secret_encrypted = client_secret_encrypted;
|
|
|
|
|
self.authorization_url_override = authorization_url_override;
|
|
|
|
|
self.token_url_override = token_url_override;
|
|
|
|
|
self.userinfo_url_override = userinfo_url_override;
|
|
|
|
|
self.scopes = scopes;
|
|
|
|
|
self.attribute_mapping = attribute_mapping;
|
|
|
|
|
self.extra_config = extra_config;
|
2026-05-17 21:22:20 +08:00
|
|
|
self.icon_url = icon_url;
|
2026-03-31 19:19:04 +08:00
|
|
|
self.is_enabled = is_enabled;
|
|
|
|
|
self
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn with_timestamps(
|
|
|
|
|
mut self,
|
2026-04-10 01:46:14 +08:00
|
|
|
created_at_unix_ms: Option<u64>,
|
2026-03-31 19:19:04 +08:00
|
|
|
updated_at_unix_secs: Option<u64>,
|
|
|
|
|
) -> Self {
|
2026-04-10 01:46:14 +08:00
|
|
|
self.created_at_unix_ms = created_at_unix_ms;
|
2026-03-31 19:19:04 +08:00
|
|
|
self.updated_at_unix_secs = updated_at_unix_secs;
|
|
|
|
|
self
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
|
|
|
|
|
pub enum EncryptedSecretUpdate {
|
|
|
|
|
#[default]
|
|
|
|
|
Preserve,
|
|
|
|
|
Clear,
|
|
|
|
|
Set(String),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl EncryptedSecretUpdate {
|
|
|
|
|
pub fn mode_name(&self) -> &'static str {
|
|
|
|
|
match self {
|
|
|
|
|
Self::Preserve => "preserve",
|
|
|
|
|
Self::Clear => "clear",
|
|
|
|
|
Self::Set(_) => "set",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn value(&self) -> Option<&str> {
|
|
|
|
|
match self {
|
|
|
|
|
Self::Set(value) => Some(value.as_str()),
|
|
|
|
|
Self::Preserve | Self::Clear => None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
|
|
|
|
pub struct UpsertOAuthProviderConfigRecord {
|
|
|
|
|
pub provider_type: String,
|
|
|
|
|
pub display_name: String,
|
|
|
|
|
pub client_id: String,
|
|
|
|
|
pub client_secret_encrypted: EncryptedSecretUpdate,
|
|
|
|
|
pub authorization_url_override: Option<String>,
|
|
|
|
|
pub token_url_override: Option<String>,
|
|
|
|
|
pub userinfo_url_override: Option<String>,
|
|
|
|
|
pub scopes: Option<Vec<String>>,
|
|
|
|
|
pub redirect_uri: String,
|
|
|
|
|
pub frontend_callback_url: String,
|
|
|
|
|
pub attribute_mapping: Option<serde_json::Value>,
|
|
|
|
|
pub extra_config: Option<serde_json::Value>,
|
2026-05-17 21:22:20 +08:00
|
|
|
pub icon_url: Option<String>,
|
2026-03-31 19:19:04 +08:00
|
|
|
pub is_enabled: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl UpsertOAuthProviderConfigRecord {
|
|
|
|
|
pub fn validate(&self) -> Result<(), crate::DataLayerError> {
|
|
|
|
|
if self.provider_type.trim().is_empty() {
|
|
|
|
|
return Err(crate::DataLayerError::InvalidInput(
|
|
|
|
|
"provider_type is required".to_string(),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
if self.display_name.trim().is_empty() {
|
|
|
|
|
return Err(crate::DataLayerError::InvalidInput(
|
|
|
|
|
"display_name is required".to_string(),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
if self.client_id.trim().is_empty() {
|
|
|
|
|
return Err(crate::DataLayerError::InvalidInput(
|
|
|
|
|
"client_id is required".to_string(),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
if self.redirect_uri.trim().is_empty() {
|
|
|
|
|
return Err(crate::DataLayerError::InvalidInput(
|
|
|
|
|
"redirect_uri is required".to_string(),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
if self.frontend_callback_url.trim().is_empty() {
|
|
|
|
|
return Err(crate::DataLayerError::InvalidInput(
|
|
|
|
|
"frontend_callback_url is required".to_string(),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
if let Some(scopes) = &self.scopes {
|
|
|
|
|
for scope in scopes {
|
|
|
|
|
if scope.trim().is_empty() {
|
|
|
|
|
return Err(crate::DataLayerError::InvalidInput(
|
|
|
|
|
"scopes must not contain empty values".to_string(),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[async_trait]
|
|
|
|
|
pub trait OAuthProviderReadRepository: Send + Sync {
|
|
|
|
|
async fn list_oauth_provider_configs(
|
|
|
|
|
&self,
|
|
|
|
|
) -> Result<Vec<StoredOAuthProviderConfig>, crate::DataLayerError>;
|
|
|
|
|
|
|
|
|
|
async fn get_oauth_provider_config(
|
|
|
|
|
&self,
|
|
|
|
|
provider_type: &str,
|
|
|
|
|
) -> Result<Option<StoredOAuthProviderConfig>, crate::DataLayerError>;
|
|
|
|
|
|
|
|
|
|
async fn count_locked_users_if_provider_disabled(
|
|
|
|
|
&self,
|
|
|
|
|
provider_type: &str,
|
|
|
|
|
ldap_exclusive: bool,
|
|
|
|
|
) -> Result<usize, crate::DataLayerError>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[async_trait]
|
|
|
|
|
pub trait OAuthProviderWriteRepository: Send + Sync {
|
|
|
|
|
async fn upsert_oauth_provider_config(
|
|
|
|
|
&self,
|
|
|
|
|
record: &UpsertOAuthProviderConfigRecord,
|
|
|
|
|
) -> Result<StoredOAuthProviderConfig, crate::DataLayerError>;
|
|
|
|
|
|
|
|
|
|
async fn delete_oauth_provider_config(
|
|
|
|
|
&self,
|
|
|
|
|
provider_type: &str,
|
|
|
|
|
) -> Result<bool, crate::DataLayerError>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub trait OAuthProviderRepository:
|
|
|
|
|
OAuthProviderReadRepository + OAuthProviderWriteRepository + Send + Sync
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T> OAuthProviderRepository for T where
|
|
|
|
|
T: OAuthProviderReadRepository + OAuthProviderWriteRepository + Send + Sync
|
|
|
|
|
{
|
|
|
|
|
}
|