Allow local access tokens to be used with OIDC backends (closes #1311 closes stalwartlabs/webadmin#52)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(|| {
|
||||
|
||||
@@ -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(|| {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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((
|
||||
|
||||
Reference in New Issue
Block a user