Allow APIs to be used with external directories (closes #1815)

This commit is contained in:
mdecimus
2025-07-14 14:45:24 +02:00
parent 7afb97537b
commit 6568d61927
2 changed files with 70 additions and 42 deletions

View File

@@ -7,7 +7,8 @@
use std::{net::IpAddr, sync::Arc}; use std::{net::IpAddr, sync::Arc};
use directory::{ use directory::{
Directory, Permission, Permissions, Principal, QueryBy, core::secret::verify_secret_hash, Directory, Permission, Permissions, Principal, QueryBy, Type,
backend::internal::lookup::DirectoryStore, core::secret::verify_secret_hash,
}; };
use jmap_proto::types::collection::Collection; use jmap_proto::types::collection::Collection;
use mail_send::Credentials; use mail_send::Credentials;
@@ -62,6 +63,7 @@ pub struct AuthRequest<'x> {
session_id: u64, session_id: u64,
remote_ip: IpAddr, remote_ip: IpAddr,
return_member_of: bool, return_member_of: bool,
allow_api_access: bool,
directory: Option<&'x Directory>, directory: Option<&'x Directory>,
} }
@@ -124,48 +126,62 @@ impl Server {
}; };
// Then check if the credentials match the fallback admin or master user // Then check if the credentials match the fallback admin or master user
match ( if let Credentials::Plain { username, secret } = &req.credentials {
&self.core.jmap.fallback_admin, match (&self.core.jmap.fallback_admin, &self.core.jmap.master_user) {
&self.core.jmap.master_user, (Some((fallback_admin, fallback_pass)), _) if username == fallback_admin => {
&req.credentials, if verify_secret_hash(fallback_pass, secret).await? {
) {
(Some((fallback_admin, fallback_pass)), _, Credentials::Plain { username, secret })
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)), Credentials::Plain { username, secret })
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?
{
trc::event!( trc::event!(
Auth(trc::AuthEvent::Success), Auth(trc::AuthEvent::Success),
AccountName = username.to_string(), AccountName = username.clone(),
SpanId = req.session_id, SpanId = req.session_id,
AccountId = principal.id(),
Type = principal.typ().as_str(),
); );
return Ok(principal); 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?
{
trc::event!(
Auth(trc::AuthEvent::Success),
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(QueryBy::Credentials(&req.credentials), 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);
}
}
} }
} }
} }
_ => {}
} }
if let Err(err) = result { if let Err(err) = result {
@@ -205,6 +221,7 @@ impl<'x> AuthRequest<'x> {
remote_ip, remote_ip,
return_member_of: true, return_member_of: true,
directory: None, directory: None,
allow_api_access: false,
} }
} }
@@ -233,6 +250,11 @@ impl<'x> AuthRequest<'x> {
self.directory = Some(directory); self.directory = Some(directory);
self self
} }
pub fn with_api_access(mut self, allow_api_access: bool) -> Self {
self.allow_api_access = allow_api_access;
self
}
} }
impl CacheItemWeight for AccessToken { impl CacheItemWeight for AccessToken {

View File

@@ -43,6 +43,9 @@ impl Authenticator for Server {
.is_http_authenticated_request_allowed(&access_token) .is_http_authenticated_request_allowed(&access_token)
.await .await
.map(|in_flight| (in_flight, access_token)); .map(|in_flight| (in_flight, access_token));
} else {
// If the revision is not valid, remove the cached credentials
self.inner.cache.http_auth.remove(token);
} }
} }
@@ -81,11 +84,14 @@ impl Authenticator for Server {
// Authenticate // Authenticate
let access_token = self let access_token = self
.authenticate(&AuthRequest::from_credentials( .authenticate(
credentials, &AuthRequest::from_credentials(
session.session_id, credentials,
session.remote_ip, session.session_id,
)) session.remote_ip,
)
.with_api_access(allow_api_access),
)
.await?; .await?;
// Cache credentials // Cache credentials
@@ -153,8 +159,8 @@ fn decode_plain_auth(token: &str) -> Option<Credentials<String>> {
fn decode_bearer_token(token: &str, allow_api_access: bool) -> Option<Credentials<String>> { fn decode_bearer_token(token: &str, allow_api_access: bool) -> Option<Credentials<String>> {
if allow_api_access { if allow_api_access {
if let Some(token) = token.strip_prefix("api_") { if let Some(token) = token.strip_prefix("api_").and_then(decode_plain_auth) {
return decode_plain_auth(token); return Some(token);
} }
} }