Merge pull request 'Try a directory before anything signs in through it' (#95) from feature/directory-test into main
This commit was merged in pull request #95.
This commit is contained in:
@@ -120,6 +120,17 @@ impl ManagementApi for Server {
|
|||||||
jmap::inbuxa::explanation::question(self, &access_token, &subject).await?;
|
jmap::inbuxa::explanation::question(self, &access_token, &subject).await?;
|
||||||
Ok(explain_stream(self.clone(), access_token, question, in_flight))
|
Ok(explain_stream(self.clone(), access_token, question, in_flight))
|
||||||
}
|
}
|
||||||
|
// inbuxa: try a saved directory before anything signs in through it
|
||||||
|
"directory" if is_post && path.get(1).copied() == Some("test") => {
|
||||||
|
let (_in_flight, access_token) = self.authenticate_headers(req, session).await?;
|
||||||
|
jmap::inbuxa::directory_test::assert_allowed(&access_token)?;
|
||||||
|
let request = body
|
||||||
|
.as_deref()
|
||||||
|
.and_then(|body| serde_json::from_slice::<serde_json::Value>(body).ok())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let answer = jmap::inbuxa::directory_test::test(self, &request).await?;
|
||||||
|
Ok(JsonResponse::new(answer).no_cache().into_http_response())
|
||||||
|
}
|
||||||
"account" => {
|
"account" => {
|
||||||
// Authenticate request
|
// Authenticate request
|
||||||
let (_in_flight, access_token) = self.authenticate_headers(req, session).await?;
|
let (_in_flight, access_token) = self.authenticate_headers(req, session).await?;
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Coffey Labs
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
//! `POST /api/directory/test`: try a saved directory before anything signs in
|
||||||
|
//! through it (settings-reorg, guided setup "Connect a sign-in directory").
|
||||||
|
//!
|
||||||
|
//! The body names a directory and an address, and optionally a password:
|
||||||
|
//!
|
||||||
|
//! ```json
|
||||||
|
//! {"directoryId": "b", "address": "[email protected]", "password": "…"}
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! The answer says whether the directory opened, what a recipient lookup of
|
||||||
|
//! the address finds, and, when a password is given, whether it signs in.
|
||||||
|
//! It calls the directory itself, below the sign-in path, so a test never
|
||||||
|
//! creates or updates an account (DIR-14), never counts toward the sign-in
|
||||||
|
//! ban, and doesn't mind which domains use the directory (DIR-6). Nothing is
|
||||||
|
//! cached (DIR-32). A password hash a directory hands back is never returned.
|
||||||
|
//!
|
||||||
|
//! For server-level administrators who may change directories.
|
||||||
|
|
||||||
|
use common::{Server, auth::AccessToken};
|
||||||
|
use directory::{Credentials, Directory, Recipient};
|
||||||
|
use registry::schema::enums::Permission;
|
||||||
|
use serde_json::{Value, json};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use types::id::Id;
|
||||||
|
|
||||||
|
fn message(err: &trc::Error) -> String {
|
||||||
|
err.value_as_str(trc::Key::Reason)
|
||||||
|
.or_else(|| err.value_as_str(trc::Key::Details))
|
||||||
|
.map(str::to_string)
|
||||||
|
.unwrap_or_else(|| err.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kind(directory: &Directory) -> &'static str {
|
||||||
|
match directory {
|
||||||
|
Directory::Ldap(_) => "ldap",
|
||||||
|
Directory::Sql(_) => "sql",
|
||||||
|
Directory::OpenId(_) => "oidc",
|
||||||
|
Directory::Unavailable(d) => match d.directory_type() {
|
||||||
|
registry::schema::enums::DirectoryType::Ldap => "ldap",
|
||||||
|
registry::schema::enums::DirectoryType::Sql => "sql",
|
||||||
|
registry::schema::enums::DirectoryType::Oidc => "oidc",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assert_allowed(access_token: &AccessToken) -> trc::Result<()> {
|
||||||
|
if access_token.tenant_id().is_some() {
|
||||||
|
return Err(trc::JmapEvent::Forbidden
|
||||||
|
.into_err()
|
||||||
|
.details("Directory tests are for server-level administrators."));
|
||||||
|
}
|
||||||
|
access_token.enforce_permission(Permission::SysDirectoryUpdate)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bad(details: &'static str) -> trc::Error {
|
||||||
|
trc::ResourceEvent::BadParameters.into_err().details(details)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn test(server: &Server, body: &Value) -> trc::Result<Value> {
|
||||||
|
let directory_id = body
|
||||||
|
.get("directoryId")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.and_then(|id| Id::from_str(id).ok())
|
||||||
|
.ok_or_else(|| bad("Expected {\"directoryId\": …, \"address\": …}"))?;
|
||||||
|
let address = body
|
||||||
|
.get("address")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.map(|a| a.trim().to_lowercase())
|
||||||
|
.filter(|a| !a.is_empty())
|
||||||
|
.ok_or_else(|| bad("Expected an address to look up"))?;
|
||||||
|
let password = body
|
||||||
|
.get("password")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.filter(|p| !p.is_empty());
|
||||||
|
|
||||||
|
let Some(directory) = server
|
||||||
|
.core
|
||||||
|
.storage
|
||||||
|
.directories
|
||||||
|
.get(&(directory_id.id() as u32))
|
||||||
|
.cloned()
|
||||||
|
else {
|
||||||
|
return Ok(json!({
|
||||||
|
"opened": false,
|
||||||
|
"error": "The server hasn't loaded this directory. Save it, and try again in a few seconds.",
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut out = json!({ "kind": kind(&directory) });
|
||||||
|
if let Directory::Unavailable(d) = directory.as_ref() {
|
||||||
|
out["opened"] = json!(false);
|
||||||
|
out["error"] = json!(message(&d.error()));
|
||||||
|
return Ok(out);
|
||||||
|
}
|
||||||
|
out["opened"] = json!(true);
|
||||||
|
|
||||||
|
if let Some(discovery) = directory.oidc_discovery_document() {
|
||||||
|
out["oidc"] = json!({
|
||||||
|
"issuer": discovery.document.issuer,
|
||||||
|
"jwksUri": discovery.document.jwks_uri,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// What mail for this address would find.
|
||||||
|
if directory.can_lookup_recipients() {
|
||||||
|
out["lookup"] = match directory.recipient(&address).await {
|
||||||
|
Ok(Recipient::Account(a)) => json!({
|
||||||
|
"found": "account",
|
||||||
|
"email": a.email,
|
||||||
|
"aliases": a.email_aliases,
|
||||||
|
"groups": a.groups.unwrap_or_default(),
|
||||||
|
"description": a.description,
|
||||||
|
}),
|
||||||
|
Ok(Recipient::Group(g)) => json!({
|
||||||
|
"found": "group",
|
||||||
|
"email": g.email,
|
||||||
|
"aliases": g.email_aliases,
|
||||||
|
"description": g.description,
|
||||||
|
}),
|
||||||
|
Ok(Recipient::Invalid) => json!({ "found": "none" }),
|
||||||
|
Err(err) => json!({ "error": message(&err) }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whether this person could sign in. OIDC takes tokens, not passwords
|
||||||
|
// (DIR-29), so there's nothing to try there.
|
||||||
|
if let Some(password) = password
|
||||||
|
&& !matches!(directory.as_ref(), Directory::OpenId(_))
|
||||||
|
{
|
||||||
|
let credentials = Credentials::Basic {
|
||||||
|
username: address.clone(),
|
||||||
|
secret: password.to_string(),
|
||||||
|
mfa_token: None,
|
||||||
|
};
|
||||||
|
out["signIn"] = match directory.authenticate(&credentials).await {
|
||||||
|
Ok(a) => json!({
|
||||||
|
"ok": true,
|
||||||
|
"email": a.email,
|
||||||
|
"groups": a.groups.unwrap_or_default(),
|
||||||
|
"description": a.description,
|
||||||
|
}),
|
||||||
|
Err(err) if matches!(err.as_ref(), trc::EventType::Auth(trc::AuthEvent::Failed)) => {
|
||||||
|
json!({ "ok": false, "wrongPassword": true })
|
||||||
|
}
|
||||||
|
Err(err) => json!({ "ok": false, "error": message(&err) }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ pub mod audit_log;
|
|||||||
pub mod ai_limits;
|
pub mod ai_limits;
|
||||||
pub mod log_settings;
|
pub mod log_settings;
|
||||||
pub mod data_inventory;
|
pub mod data_inventory;
|
||||||
|
pub mod directory_test;
|
||||||
pub mod explanation;
|
pub mod explanation;
|
||||||
pub mod protocol_policy;
|
pub mod protocol_policy;
|
||||||
pub mod tenant_protocol_policy;
|
pub mod tenant_protocol_policy;
|
||||||
|
|||||||
Reference in New Issue
Block a user