Allow local access tokens to be used with OIDC backends (closes #1311 closes stalwartlabs/webadmin#52)

This commit is contained in:
mdecimus
2025-07-25 21:06:23 +02:00
parent 02f6a114e0
commit 654f296d45
37 changed files with 369 additions and 278 deletions

View File

@@ -12,7 +12,7 @@ use crate::{
};
use ahash::AHashSet;
use directory::{
Permission, Principal, PrincipalData, QueryBy, Type,
Permission, Principal, PrincipalData, QueryParams, Type,
backend::internal::{
lookup::DirectoryStore,
manage::{ChangedPrincipals, ManageDirectory},
@@ -72,6 +72,8 @@ impl Server {
if self.is_enterprise_edition() {
if let Some(tenant_id) = principal.tenant {
// Limit tenant permissions
use directory::QueryParams;
permissions.intersection(&self.get_role_permissions(tenant_id).await?.enabled);
// Obtain tenant quota
@@ -79,7 +81,7 @@ impl Server {
id: tenant_id,
quota: self
.store()
.query(QueryBy::Id(tenant_id), false)
.query(QueryParams::id(tenant_id).with_return_member_of(false))
.await
.caused_by(trc::location!())?
.ok_or_else(|| {
@@ -178,7 +180,11 @@ impl Server {
}
async fn build_access_token(&self, account_id: u32, revision: u64) -> trc::Result<AccessToken> {
let err = match self.directory().query(QueryBy::Id(account_id), true).await {
let err = match self
.directory()
.query(QueryParams::id(account_id).with_return_member_of(true))
.await
{
Ok(Some(principal)) => {
return self
.build_access_token_from_principal(principal, revision)

View File

@@ -4,22 +4,20 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{net::IpAddr, sync::Arc};
use crate::{Server, listener::limiter::ConcurrencyLimiter};
use directory::{
Directory, Permission, Permissions, Principal, QueryBy, Type,
Directory, FALLBACK_ADMIN_ID, Permission, Permissions, Principal, QueryParams, Type,
backend::internal::lookup::DirectoryStore, core::secret::verify_secret_hash,
};
use jmap_proto::types::collection::Collection;
use mail_send::Credentials;
use oauth::GrantType;
use std::{net::IpAddr, sync::Arc};
use utils::{
cache::CacheItemWeight,
map::{bitmap::Bitmap, vec_map::VecMap},
};
use crate::{Server, listener::limiter::ConcurrencyLimiter};
pub mod access_token;
pub mod oauth;
pub mod rate_limit;
@@ -102,7 +100,10 @@ impl Server {
) -> trc::Result<Principal> {
// First try to authenticate the user against the default directory
let result = match directory
.query(QueryBy::Credentials(&req.credentials), req.return_member_of)
.query(
QueryParams::credentials(&req.credentials)
.with_return_member_of(req.return_member_of),
)
.await
{
Ok(Some(principal)) => {
@@ -125,64 +126,104 @@ impl Server {
}
};
// Then check if the credentials match the fallback admin or master user
if let Credentials::Plain { username, secret } = &req.credentials {
match (&self.core.jmap.fallback_admin, &self.core.jmap.master_user) {
(Some((fallback_admin, fallback_pass)), _) if username == fallback_admin => {
if verify_secret_hash(fallback_pass, secret).await? {
trc::event!(
Auth(trc::AuthEvent::Success),
AccountName = username.clone(),
SpanId = req.session_id,
);
return Ok(Principal::fallback_admin(fallback_pass));
}
}
(_, Some((master_user, master_pass))) if username.ends_with(master_user) => {
if verify_secret_hash(master_pass, secret).await? {
let username = username.strip_suffix(master_user).unwrap();
let username = username.strip_suffix('%').unwrap_or(username);
if let Some(principal) = directory
.query(QueryBy::Name(username), req.return_member_of)
.await?
{
match &req.credentials {
Credentials::Plain { username, secret } => {
// Then check if the credentials match the fallback admin or master user
match (&self.core.jmap.fallback_admin, &self.core.jmap.master_user) {
(Some((fallback_admin, fallback_pass)), _) if username == fallback_admin => {
if verify_secret_hash(fallback_pass, secret).await? {
trc::event!(
Auth(trc::AuthEvent::Success),
AccountName = username.to_string(),
AccountName = username.clone(),
SpanId = req.session_id,
AccountId = principal.id(),
Type = principal.typ().as_str(),
);
return Ok(principal);
return Ok(Principal::fallback_admin(fallback_pass));
}
}
}
_ => {
// Validate API credentials
if req.allow_api_access {
if let Ok(Some(principal)) = self
.store()
.query(QueryBy::Credentials(&req.credentials), req.return_member_of)
.await
{
if principal.typ == Type::ApiKey {
(_, Some((master_user, master_pass))) if username.ends_with(master_user) => {
if verify_secret_hash(master_pass, secret).await? {
let username = username.strip_suffix(master_user).unwrap();
let username = username.strip_suffix('%').unwrap_or(username);
if let Some(principal) = directory
.query(
QueryParams::name(username)
.with_return_member_of(req.return_member_of),
)
.await?
{
trc::event!(
Auth(trc::AuthEvent::Success),
AccountName = principal.name().to_string(),
AccountId = principal.id(),
AccountName = username.to_string(),
SpanId = req.session_id,
AccountId = principal.id(),
Type = principal.typ().as_str(),
);
return Ok(principal);
}
}
}
_ => {
// Validate API credentials
if req.allow_api_access {
if let Ok(Some(principal)) = self
.store()
.query(
QueryParams::credentials(&req.credentials)
.with_return_member_of(req.return_member_of),
)
.await
{
if principal.typ == Type::ApiKey {
trc::event!(
Auth(trc::AuthEvent::Success),
AccountName = principal.name().to_string(),
AccountId = principal.id(),
SpanId = req.session_id,
);
return Ok(principal);
}
}
}
}
}
}
}
Credentials::OAuthBearer { token } if directory.has_bearer_token_support() => {
// Check for bearer tokens issued locally
if let Ok(token_info) = self
.validate_access_token(GrantType::AccessToken.into(), token)
.await
{
let principal = if token_info.account_id != FALLBACK_ADMIN_ID {
directory
.query(
QueryParams::id(token_info.account_id)
.with_return_member_of(req.return_member_of),
)
.await
.unwrap_or_default()
} else if let Some((_, fallback_pass)) = &self.core.jmap.fallback_admin {
Principal::fallback_admin(fallback_pass).into()
} else {
None
};
if let Some(principal) = principal {
trc::event!(
Auth(trc::AuthEvent::Success),
AccountName = principal.name().to_string(),
AccountId = principal.id(),
SpanId = req.session_id,
);
return Ok(principal);
}
}
}
_ => (),
};
if let Err(err) = result {
Err(err)

View File

@@ -4,11 +4,12 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::time::SystemTime;
use directory::QueryBy;
use super::{CLIENT_ID_MAX_LEN, GrantType, RANDOM_CODE_LEN, crypto::SymmetricEncrypt};
use crate::Server;
use directory::QueryParams;
use mail_builder::encoders::base64::base64_encode;
use mail_parser::decoders::base64::base64_decode;
use std::time::SystemTime;
use store::{
blake3,
rand::{Rng, rng},
@@ -16,10 +17,6 @@ use store::{
use trc::AddContext;
use utils::codec::leb128::{Leb128Iterator, Leb128Vec};
use crate::Server;
use super::{CLIENT_ID_MAX_LEN, GrantType, RANDOM_CODE_LEN, crypto::SymmetricEncrypt};
pub struct TokenInfo {
pub grant_type: GrantType,
pub account_id: u32,
@@ -223,7 +220,7 @@ impl Server {
self.core
.storage
.directory
.query(QueryBy::Id(account_id), false)
.query(QueryParams::id(account_id).with_return_member_of(false))
.await
.caused_by(trc::location!())?
.ok_or_else(|| {

View File

@@ -4,18 +4,16 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::sync::{Arc, LazyLock};
use crate::Server;
use ahash::AHashSet;
use directory::{
Permission, Permissions, QueryBy, ROLE_ADMIN, ROLE_TENANT_ADMIN, ROLE_USER,
Permission, Permissions, QueryParams, ROLE_ADMIN, ROLE_TENANT_ADMIN, ROLE_USER,
backend::internal::lookup::DirectoryStore,
};
use std::sync::{Arc, LazyLock};
use trc::AddContext;
use utils::cache::CacheItemWeight;
use crate::Server;
#[derive(Debug, Clone, Default)]
pub struct RolePermissions {
pub enabled: Permissions,
@@ -97,7 +95,7 @@ impl Server {
// Obtain principal
let mut principal = self
.store()
.query(QueryBy::Id(role_id), true)
.query(QueryParams::id(role_id).with_return_member_of(true))
.await
.caused_by(trc::location!())?
.ok_or_else(|| {

View File

@@ -16,7 +16,7 @@ use crate::{
},
ipc::{BroadcastEvent, StateEvent},
};
use directory::{Directory, QueryBy, Type, backend::internal::manage::ManageDirectory};
use directory::{Directory, QueryParams, Type, backend::internal::manage::ManageDirectory};
use jmap_proto::types::{
blob::BlobId,
collection::{Collection, SyncCollection},
@@ -451,7 +451,7 @@ impl Server {
.core
.storage
.directory
.query(QueryBy::Id(account_id), false)
.query(QueryParams::id(account_id).with_return_member_of(false))
.await
.add_context(|err| err.caused_by(trc::location!()).account_id(account_id))?
{
@@ -470,7 +470,7 @@ impl Server {
.core
.storage
.directory
.query(QueryBy::Id(tenant_id), false)
.query(QueryParams::id(tenant_id).with_return_member_of(false))
.await
.add_context(|err| {
err.caused_by(trc::location!()).account_id(tenant_id)

View File

@@ -14,14 +14,12 @@ pub mod license;
pub mod llm;
pub mod undelete;
use std::{sync::Arc, time::Duration};
use ahash::{AHashMap, AHashSet};
use directory::{QueryBy, Type, backend::internal::lookup::DirectoryStore};
use directory::{QueryParams, Type, backend::internal::lookup::DirectoryStore};
use license::LicenseKey;
use llm::AiApiConfig;
use mail_parser::DateTime;
use std::{sync::Arc, time::Duration};
use store::Store;
use trc::{AddContext, EventType, MetricType};
use utils::{HttpLimitResponse, config::cron::SimpleCron, template::Template};
@@ -165,7 +163,7 @@ impl Server {
// Try fetching the logo for the domain
let logo_url = if let Some(mut principal) = self
.store()
.query(QueryBy::Name(domain), false)
.query(QueryParams::name(domain).with_return_member_of(false))
.await
.caused_by(trc::location!())?
.filter(|p| p.typ() == Type::Domain)
@@ -175,7 +173,7 @@ impl Server {
} else if let Some(tenant_id) = principal.tenant {
if let Some(logo) = self
.store()
.query(QueryBy::Id(tenant_id), false)
.query(QueryParams::id(tenant_id).with_return_member_of(false))
.await
.caused_by(trc::location!())?
.and_then(|mut p| p.picture_mut().map(std::mem::take))

View File

@@ -36,7 +36,7 @@ use std::{
hash::{BuildHasher, Hash, Hasher},
net::{IpAddr, Ipv4Addr, Ipv6Addr},
sync::{Arc, atomic::AtomicBool},
time::Duration,
time::{Duration, Instant},
};
use tinyvec::TinyVec;
use tokio::sync::{Notify, Semaphore, mpsc};
@@ -239,10 +239,11 @@ pub struct MailboxCache {
pub acls: TinyVec<[AclGrant; 2]>,
}
#[derive(Debug, Clone, Default)]
#[derive(Debug, Clone)]
pub struct HttpAuthCache {
pub account_id: u32,
pub revision: u64,
pub expires: Instant,
}
pub struct Ipc {

View File

@@ -4,8 +4,9 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{Server, auth::AccessToken};
use directory::{
QueryBy, Type,
QueryParams, Type,
backend::internal::{
PrincipalField,
manage::{ChangedPrincipals, ManageDirectory},
@@ -21,8 +22,6 @@ use jmap_proto::{
};
use utils::map::bitmap::Bitmap;
use crate::{Server, auth::AccessToken};
impl Server {
pub async fn acl_set(
&self,
@@ -215,7 +214,7 @@ impl Server {
.core
.storage
.directory
.query(QueryBy::Name(account_name), false)
.query(QueryParams::name(account_name).with_return_member_of(false))
.await
{
Ok(Some(principal)) => {
@@ -256,7 +255,7 @@ impl Server {
.core
.storage
.directory
.query(QueryBy::Name(account_name), false)
.query(QueryParams::name(account_name).with_return_member_of(false))
.await
{
Ok(Some(principal)) => Ok((