Centralize and harden user auth config handling
Move authentication-mode resolution into typed config parsing with ConfigUserAuth, so downstream login setup consumes validated credentials instead of re-checking raw optional fields. Enforce explicit password-vs-token selection, validate token/device/user-id requirements in one place, and normalize empty auth env overrides to unset values for consistent behavior across YAML and environment input.
This commit is contained in:
@@ -6,7 +6,7 @@ use mxlink::matrix_sdk::Room;
|
|||||||
use mxlink::matrix_sdk::media::{MediaFormat, MediaRequestParameters};
|
use mxlink::matrix_sdk::media::{MediaFormat, MediaRequestParameters};
|
||||||
use mxlink::matrix_sdk::ruma::api::client::profile::{AvatarUrl, DisplayName};
|
use mxlink::matrix_sdk::ruma::api::client::profile::{AvatarUrl, DisplayName};
|
||||||
use mxlink::matrix_sdk::ruma::{
|
use mxlink::matrix_sdk::ruma::{
|
||||||
MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedUserId, events::room::MediaSource,
|
MilliSecondsSinceUnixEpoch, OwnedUserId, events::room::MediaSource,
|
||||||
};
|
};
|
||||||
|
|
||||||
use mxlink::{
|
use mxlink::{
|
||||||
@@ -25,7 +25,7 @@ use crate::agent::Manager as AgentManager;
|
|||||||
use crate::entity::catch_up_marker::{
|
use crate::entity::catch_up_marker::{
|
||||||
CatchUpMarker, CatchUpMarkerManager, DelayedCatchUpMarkerManager,
|
CatchUpMarker, CatchUpMarkerManager, DelayedCatchUpMarkerManager,
|
||||||
};
|
};
|
||||||
use crate::entity::cfg::{Avatar, Config};
|
use crate::entity::cfg::{Avatar, Config, ConfigUserAuth};
|
||||||
use crate::entity::globalconfig::{GlobalConfig, GlobalConfigurationManager};
|
use crate::entity::globalconfig::{GlobalConfig, GlobalConfigurationManager};
|
||||||
use crate::entity::roomconfig::{RoomConfig, RoomConfigurationManager};
|
use crate::entity::roomconfig::{RoomConfig, RoomConfigurationManager};
|
||||||
|
|
||||||
@@ -395,44 +395,21 @@ async fn create_matrix_link(config: &Config) -> anyhow::Result<MatrixLink> {
|
|||||||
let session_encryption_key = config.persistence.session_encryption_key()?;
|
let session_encryption_key = config.persistence.session_encryption_key()?;
|
||||||
let db_dir_path: std::path::PathBuf = config.persistence.db_dir_path()?;
|
let db_dir_path: std::path::PathBuf = config.persistence.db_dir_path()?;
|
||||||
|
|
||||||
let login_creds = if let Some(access_token) = config
|
let user_auth = config.user.auth_config(&config.homeserver.server_name)?;
|
||||||
.user
|
|
||||||
.access_token
|
let login_creds = match user_auth {
|
||||||
.as_deref()
|
ConfigUserAuth::UserPassword { username, password } => {
|
||||||
.filter(|token| !token.is_empty())
|
LoginCredentials::UserPassword(username, password)
|
||||||
{
|
}
|
||||||
let server_name = &config.homeserver.server_name;
|
ConfigUserAuth::AccessToken {
|
||||||
let localpart = &config.user.mxid_localpart;
|
|
||||||
let user_id = OwnedUserId::try_from(format!("@{localpart}:{server_name}"))
|
|
||||||
.map_err(|e| anyhow::anyhow!("Invalid user ID: {e}"))?;
|
|
||||||
let device_id = OwnedDeviceId::from(
|
|
||||||
config
|
|
||||||
.user
|
|
||||||
.device_id
|
|
||||||
.as_deref()
|
|
||||||
.filter(|device_id| !device_id.is_empty())
|
|
||||||
.ok_or_else(|| {
|
|
||||||
anyhow::anyhow!("user.device_id must be set for access token authentication")
|
|
||||||
})?,
|
|
||||||
);
|
|
||||||
LoginCredentials::AccessToken {
|
|
||||||
user_id,
|
user_id,
|
||||||
device_id,
|
device_id,
|
||||||
access_token: access_token.to_owned(),
|
access_token,
|
||||||
}
|
} => LoginCredentials::AccessToken {
|
||||||
} else {
|
user_id,
|
||||||
LoginCredentials::UserPassword(
|
device_id,
|
||||||
config.user.mxid_localpart.to_owned(),
|
access_token,
|
||||||
config
|
},
|
||||||
.user
|
|
||||||
.password
|
|
||||||
.as_deref()
|
|
||||||
.filter(|password| !password.is_empty())
|
|
||||||
.ok_or_else(|| {
|
|
||||||
anyhow::anyhow!("user.password must be set for password authentication")
|
|
||||||
})?
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let login_encryption = LoginEncryption::new(
|
let login_encryption = LoginEncryption::new(
|
||||||
|
|||||||
@@ -29,9 +29,15 @@ pub fn load() -> anyhow::Result<Config> {
|
|||||||
cfg_env::BAIBOT_HOMESERVER_SERVER_NAME => config.homeserver.server_name = value,
|
cfg_env::BAIBOT_HOMESERVER_SERVER_NAME => config.homeserver.server_name = value,
|
||||||
cfg_env::BAIBOT_HOMESERVER_URL => config.homeserver.url = value,
|
cfg_env::BAIBOT_HOMESERVER_URL => config.homeserver.url = value,
|
||||||
cfg_env::BAIBOT_USER_MXID_LOCALPART => config.user.mxid_localpart = value,
|
cfg_env::BAIBOT_USER_MXID_LOCALPART => config.user.mxid_localpart = value,
|
||||||
cfg_env::BAIBOT_USER_PASSWORD => config.user.password = Some(value),
|
cfg_env::BAIBOT_USER_PASSWORD => {
|
||||||
cfg_env::BAIBOT_USER_ACCESS_TOKEN => config.user.access_token = Some(value),
|
config.user.password = optional_non_empty(value);
|
||||||
cfg_env::BAIBOT_USER_DEVICE_ID => config.user.device_id = Some(value),
|
}
|
||||||
|
cfg_env::BAIBOT_USER_ACCESS_TOKEN => {
|
||||||
|
config.user.access_token = optional_non_empty(value);
|
||||||
|
}
|
||||||
|
cfg_env::BAIBOT_USER_DEVICE_ID => {
|
||||||
|
config.user.device_id = optional_non_empty(value);
|
||||||
|
}
|
||||||
cfg_env::BAIBOT_USER_ENCRYPTION_RECOVERY_PASSPHRASE => {
|
cfg_env::BAIBOT_USER_ENCRYPTION_RECOVERY_PASSPHRASE => {
|
||||||
config.user.encryption.recovery_passphrase = Some(value);
|
config.user.encryption.recovery_passphrase = Some(value);
|
||||||
}
|
}
|
||||||
@@ -122,3 +128,7 @@ pub fn load() -> anyhow::Result<Config> {
|
|||||||
|
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn optional_non_empty(value: String) -> Option<String> {
|
||||||
|
if value.is_empty() { None } else { Some(value) }
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use mxlink::helpers::encryption::EncryptionKey;
|
use mxlink::helpers::encryption::EncryptionKey;
|
||||||
|
use mxlink::matrix_sdk::ruma::{OwnedDeviceId, OwnedUserId};
|
||||||
use serde::{Deserialize, Deserializer, Serialize};
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -38,7 +39,7 @@ pub struct Config {
|
|||||||
impl Config {
|
impl Config {
|
||||||
pub fn validate(&self) -> anyhow::Result<()> {
|
pub fn validate(&self) -> anyhow::Result<()> {
|
||||||
self.homeserver.validate()?;
|
self.homeserver.validate()?;
|
||||||
self.user.validate()?;
|
self.user.validate(&self.homeserver.server_name)?;
|
||||||
self.persistence.validate()?;
|
self.persistence.validate()?;
|
||||||
self.room.validate()?;
|
self.room.validate()?;
|
||||||
self.access.validate()?;
|
self.access.validate()?;
|
||||||
@@ -57,6 +58,19 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ConfigUserAuth {
|
||||||
|
UserPassword {
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
},
|
||||||
|
AccessToken {
|
||||||
|
user_id: OwnedUserId,
|
||||||
|
device_id: OwnedDeviceId,
|
||||||
|
access_token: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct ConfigHomeserver {
|
pub struct ConfigHomeserver {
|
||||||
pub server_name: String,
|
pub server_name: String,
|
||||||
@@ -148,7 +162,7 @@ pub struct ConfigUser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigUser {
|
impl ConfigUser {
|
||||||
pub fn validate(&self) -> anyhow::Result<()> {
|
pub fn validate(&self, homeserver_server_name: &str) -> anyhow::Result<()> {
|
||||||
if self.mxid_localpart.is_empty() {
|
if self.mxid_localpart.is_empty() {
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
"The user.mxid_localpart ({}) configuration must be set",
|
"The user.mxid_localpart ({}) configuration must be set",
|
||||||
@@ -156,7 +170,7 @@ impl ConfigUser {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.validate_auth()?;
|
self.auth_config(homeserver_server_name)?;
|
||||||
|
|
||||||
if self.name.is_empty() {
|
if self.name.is_empty() {
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
@@ -170,27 +184,55 @@ impl ConfigUser {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_auth(&self) -> anyhow::Result<()> {
|
pub fn auth_config(&self, homeserver_server_name: &str) -> anyhow::Result<ConfigUserAuth> {
|
||||||
let has_password = self.password.as_deref().is_some_and(|p| !p.is_empty());
|
let password = self.password.as_deref().filter(|value| !value.is_empty());
|
||||||
let has_access_token = self.access_token.as_deref().is_some_and(|t| !t.is_empty());
|
let access_token = self
|
||||||
let has_device_id = self.device_id.as_deref().is_some_and(|d| !d.is_empty());
|
.access_token
|
||||||
|
.as_deref()
|
||||||
|
.filter(|value| !value.is_empty());
|
||||||
|
|
||||||
if !has_password && !has_access_token {
|
match (password, access_token) {
|
||||||
return Err(anyhow::anyhow!(
|
(Some(_), Some(_)) => Err(anyhow::anyhow!(
|
||||||
"Either user.password ({}) or user.access_token ({}) must be set",
|
"Set exactly one authentication method: either user.password ({}) OR user.access_token ({}) + user.device_id ({})",
|
||||||
super::env::BAIBOT_USER_PASSWORD,
|
super::env::BAIBOT_USER_PASSWORD,
|
||||||
super::env::BAIBOT_USER_ACCESS_TOKEN
|
super::env::BAIBOT_USER_ACCESS_TOKEN,
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if has_access_token && !has_device_id {
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"user.device_id ({}) must be set when using access token authentication",
|
|
||||||
super::env::BAIBOT_USER_DEVICE_ID
|
super::env::BAIBOT_USER_DEVICE_ID
|
||||||
));
|
)),
|
||||||
}
|
(None, None) => Err(anyhow::anyhow!(
|
||||||
|
"Set one authentication method: either user.password ({}) OR user.access_token ({}) + user.device_id ({})",
|
||||||
|
super::env::BAIBOT_USER_PASSWORD,
|
||||||
|
super::env::BAIBOT_USER_ACCESS_TOKEN,
|
||||||
|
super::env::BAIBOT_USER_DEVICE_ID
|
||||||
|
)),
|
||||||
|
(Some(password), None) => Ok(ConfigUserAuth::UserPassword {
|
||||||
|
username: self.mxid_localpart.to_owned(),
|
||||||
|
password: password.to_owned(),
|
||||||
|
}),
|
||||||
|
(None, Some(access_token)) => {
|
||||||
|
let device_id = self
|
||||||
|
.device_id
|
||||||
|
.as_deref()
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.ok_or_else(|| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"user.device_id ({}) must be set when using access token authentication",
|
||||||
|
super::env::BAIBOT_USER_DEVICE_ID
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
Ok(())
|
let user_id = OwnedUserId::try_from(format!(
|
||||||
|
"@{}:{}",
|
||||||
|
self.mxid_localpart, homeserver_server_name
|
||||||
|
))
|
||||||
|
.map_err(|e| anyhow::anyhow!("Invalid user ID: {e}"))?;
|
||||||
|
|
||||||
|
Ok(ConfigUserAuth::AccessToken {
|
||||||
|
user_id,
|
||||||
|
device_id: OwnedDeviceId::from(device_id),
|
||||||
|
access_token: access_token.to_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ mod config;
|
|||||||
pub mod defaults;
|
pub mod defaults;
|
||||||
pub mod env;
|
pub mod env;
|
||||||
|
|
||||||
pub use config::{Avatar, Config};
|
pub use config::{Avatar, Config, ConfigUserAuth};
|
||||||
|
|||||||
Reference in New Issue
Block a user