summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormdecimus <mauro@stalw.art>2024-10-01 16:01:51 +0200
committermdecimus <mauro@stalw.art>2024-10-01 16:01:51 +0200
commit6e2cd7847084c145a15fb9717ad587a773cc8be0 (patch)
tree1d9714392965ed19024da4ac688af4241a863f76
parent200d8d7c45d8b0b8f957df641ca59b6fef412bc3 (diff)
OAuth fixesHEADmain
-rw-r--r--crates/common/src/auth/oauth/config.rs2
-rw-r--r--crates/common/src/config/network.rs4
-rw-r--r--crates/common/src/enterprise/license.rs26
-rw-r--r--crates/common/src/enterprise/mod.rs2
-rw-r--r--crates/directory/src/backend/internal/manage.rs53
-rw-r--r--crates/directory/src/core/mod.rs61
-rw-r--r--crates/directory/src/core/principal.rs13
-rw-r--r--crates/jmap/src/auth/oauth/auth.rs9
-rw-r--r--tests/src/jmap/enterprise.rs4
-rw-r--r--tests/src/jmap/mod.rs2
10 files changed, 107 insertions, 69 deletions
diff --git a/crates/common/src/auth/oauth/config.rs b/crates/common/src/auth/oauth/config.rs
index 79db96f7..ab875b8a 100644
--- a/crates/common/src/auth/oauth/config.rs
+++ b/crates/common/src/auth/oauth/config.rs
@@ -186,7 +186,7 @@ impl OAuthConfig {
.property_or_default("oauth.client-registration.anonymous", "false")
.unwrap_or(false),
require_client_authentication: config
- .property_or_default("oauth.client-registration.required", "false")
+ .property_or_default("oauth.client-registration.require", "false")
.unwrap_or(true),
oidc_signing_secret,
oidc_signature_algorithm,
diff --git a/crates/common/src/config/network.rs b/crates/common/src/config/network.rs
index d4c6d026..48c95809 100644
--- a/crates/common/src/config/network.rs
+++ b/crates/common/src/config/network.rs
@@ -91,8 +91,8 @@ impl ContactForm {
.property_or_default::<bool>("form.validate-domain", "true")
.unwrap_or(true),
from_email: FieldOrDefault::parse(config, "form.email", "postmaster@localhost"),
- from_subject: FieldOrDefault::parse(config, "form.subject", "Contact Form"),
- from_name: FieldOrDefault::parse(config, "form.name", "Contact Form"),
+ from_subject: FieldOrDefault::parse(config, "form.subject", "Contact form submission"),
+ from_name: FieldOrDefault::parse(config, "form.name", "Anonymous"),
field_honey_pot: config.value("form.honey-pot.field").map(|v| v.to_string()),
rate: config
.property_or_default::<Option<Rate>>("form.rate-limit", "5/1h")
diff --git a/crates/common/src/enterprise/license.rs b/crates/common/src/enterprise/license.rs
index 718af5ab..136dcfd2 100644
--- a/crates/common/src/enterprise/license.rs
+++ b/crates/common/src/enterprise/license.rs
@@ -40,7 +40,7 @@ pub struct LicenseGenerator {
pub struct LicenseKey {
pub valid_to: u64,
pub valid_from: u64,
- pub hostname: String,
+ pub domain: String,
pub accounts: u32,
}
@@ -93,27 +93,27 @@ impl LicenseValidator {
.try_into()
.unwrap(),
);
- let hostname_len = u32::from_le_bytes(
+ let domain_len = u32::from_le_bytes(
key.get((U64_LEN * 2) + U32_LEN..(U64_LEN * 2) + (U32_LEN * 2))
.ok_or(LicenseError::Parse)?
.try_into()
.unwrap(),
) as usize;
- let hostname = String::from_utf8(
- key.get((U64_LEN * 2) + (U32_LEN * 2)..(U64_LEN * 2) + (U32_LEN * 2) + hostname_len)
+ let domain = String::from_utf8(
+ key.get((U64_LEN * 2) + (U32_LEN * 2)..(U64_LEN * 2) + (U32_LEN * 2) + domain_len)
.ok_or(LicenseError::Parse)?
.to_vec(),
)
.map_err(|_| LicenseError::Parse)?;
let signature = key
- .get((U64_LEN * 2) + (U32_LEN * 2) + hostname_len..)
+ .get((U64_LEN * 2) + (U32_LEN * 2) + domain_len..)
.ok_or(LicenseError::Parse)?;
if valid_from == 0
|| valid_to == 0
|| valid_from >= valid_to
|| accounts == 0
- || hostname.is_empty()
+ || domain.is_empty()
{
return Err(LicenseError::InvalidParameters);
}
@@ -121,7 +121,7 @@ impl LicenseValidator {
// Validate signature
self.public_key
.verify(
- &key[..(U64_LEN * 2) + (U32_LEN * 2) + hostname_len],
+ &key[..(U64_LEN * 2) + (U32_LEN * 2) + domain_len],
signature,
)
.map_err(|_| LicenseError::Validation)?;
@@ -129,7 +129,7 @@ impl LicenseValidator {
let key = LicenseKey {
valid_from,
valid_to,
- hostname,
+ domain,
accounts,
};
@@ -142,7 +142,7 @@ impl LicenseValidator {
}
impl LicenseKey {
- pub fn new(hostname: String, accounts: u32, expires_in: u64) -> Self {
+ pub fn new(domain: String, accounts: u32, expires_in: u64) -> Self {
let now = SystemTime::UNIX_EPOCH
.elapsed()
.unwrap_or_default()
@@ -150,7 +150,7 @@ impl LicenseKey {
LicenseKey {
valid_from: now - 300,
valid_to: now + expires_in + 300,
- hostname,
+ domain,
accounts,
}
}
@@ -176,7 +176,7 @@ impl LicenseKey {
pub fn into_validated_key(self, hostname: impl AsRef<str>) -> Result<Self, LicenseError> {
let local_domain = psl::domain_str(hostname.as_ref()).unwrap_or("invalid-hostname");
- let license_domain = psl::domain_str(&self.hostname).expect("Invalid license hostname");
+ let license_domain = psl::domain_str(&self.domain).expect("Invalid license domain");
if local_domain != license_domain {
Err(LicenseError::DomainMismatch {
issued_to: license_domain.to_string(),
@@ -200,8 +200,8 @@ impl LicenseGenerator {
bytes.extend_from_slice(&key.valid_from.to_le_bytes());
bytes.extend_from_slice(&key.valid_to.to_le_bytes());
bytes.extend_from_slice(&key.accounts.to_le_bytes());
- bytes.extend_from_slice(&(key.hostname.len() as u32).to_le_bytes());
- bytes.extend_from_slice(key.hostname.as_bytes());
+ bytes.extend_from_slice(&(key.domain.len() as u32).to_le_bytes());
+ bytes.extend_from_slice(key.domain.as_bytes());
bytes.extend_from_slice(self.key_pair.sign(&bytes).as_ref());
STANDARD.encode(&bytes)
}
diff --git a/crates/common/src/enterprise/mod.rs b/crates/common/src/enterprise/mod.rs
index 9f634564..6ffe400d 100644
--- a/crates/common/src/enterprise/mod.rs
+++ b/crates/common/src/enterprise/mod.rs
@@ -121,7 +121,7 @@ impl Server {
trc::event!(
Server(trc::ServerEvent::Licensing),
Details = "Stalwart Enterprise Edition license key is valid",
- Hostname = enterprise.license.hostname.clone(),
+ Domain = enterprise.license.domain.clone(),
Total = enterprise.license.accounts,
ValidFrom =
DateTime::from_timestamp(enterprise.license.valid_from as i64).to_rfc3339(),
diff --git a/crates/directory/src/backend/internal/manage.rs b/crates/directory/src/backend/internal/manage.rs
index d672af4d..eecbef8b 100644
--- a/crates/directory/src/backend/internal/manage.rs
+++ b/crates/directory/src/backend/internal/manage.rs
@@ -359,18 +359,20 @@ impl ManageDirectory for Store {
}
// Make sure the e-mail is not taken and validate domain
- for email in principal.iter_mut_str(PrincipalField::Emails) {
- *email = email.to_lowercase();
- if self.rcpt(email).await.caused_by(trc::location!())? {
- return Err(err_exists(PrincipalField::Emails, email.to_string()));
- }
- if let Some(domain) = email.split('@').nth(1) {
- if valid_domains.insert(domain.to_string()) {
- self.get_principal_info(domain)
- .await
- .caused_by(trc::location!())?
- .filter(|v| v.typ == Type::Domain && v.has_tenant_access(tenant_id))
- .ok_or_else(|| not_found(domain.to_string()))?;
+ if principal.typ != Type::OauthClient {
+ for email in principal.iter_mut_str(PrincipalField::Emails) {
+ *email = email.to_lowercase();
+ if self.rcpt(email).await.caused_by(trc::location!())? {
+ return Err(err_exists(PrincipalField::Emails, email.to_string()));
+ }
+ if let Some(domain) = email.split('@').nth(1) {
+ if valid_domains.insert(domain.to_string()) {
+ self.get_principal_info(domain)
+ .await
+ .caused_by(trc::location!())?
+ .filter(|v| v.typ == Type::Domain && v.has_tenant_access(tenant_id))
+ .ok_or_else(|| not_found(domain.to_string()))?;
+ }
}
}
}
@@ -678,7 +680,6 @@ impl ManageDirectory for Store {
};
let changes = params.changes;
let tenant_id = params.tenant_id;
- let validate = params.validate;
// Fetch principal
let mut principal = self
@@ -689,6 +690,7 @@ impl ManageDirectory for Store {
.caused_by(trc::location!())?
.ok_or_else(|| not_found(principal_id))?;
principal.inner.id = principal_id;
+ let validate_emails = params.validate && principal.inner.typ != Type::OauthClient;
// Obtain members and memberOf
let mut member_of = self
@@ -986,7 +988,7 @@ impl ManageDirectory for Store {
.collect::<Vec<_>>();
for email in &emails {
if !principal.inner.has_str_value(PrincipalField::Emails, email) {
- if validate {
+ if validate_emails {
if self.rcpt(email).await.caused_by(trc::location!())? {
return Err(err_exists(
PrincipalField::Emails,
@@ -1032,7 +1034,7 @@ impl ManageDirectory for Store {
.inner
.has_str_value(PrincipalField::Emails, &email)
{
- if validate {
+ if validate_emails {
if self.rcpt(&email).await.caused_by(trc::location!())? {
return Err(err_exists(PrincipalField::Emails, email));
}
@@ -1394,6 +1396,27 @@ impl ManageDirectory for Store {
.inner
.retain_int(change.field, |v| *v != permission);
}
+ (PrincipalAction::Set, PrincipalField::Urls, PrincipalValue::StringList(urls)) => {
+ if !urls.is_empty() {
+ principal.inner.set(change.field, urls);
+ } else {
+ principal.inner.remove(change.field);
+ }
+ }
+ (PrincipalAction::AddItem, PrincipalField::Urls, PrincipalValue::String(url)) => {
+ if !principal.inner.has_str_value(change.field, &url) {
+ principal.inner.append_str(change.field, url);
+ }
+ }
+ (
+ PrincipalAction::RemoveItem,
+ PrincipalField::Urls,
+ PrincipalValue::String(url),
+ ) => {
+ if principal.inner.has_str_value(change.field, &url) {
+ principal.inner.retain_str(change.field, |v| *v != url);
+ }
+ }
(_, field, value) => {
return Err(error(
diff --git a/crates/directory/src/core/mod.rs b/crates/directory/src/core/mod.rs
index a91852a2..4f420693 100644
--- a/crates/directory/src/core/mod.rs
+++ b/crates/directory/src/core/mod.rs
@@ -15,27 +15,29 @@ pub mod secret;
impl Permission {
pub fn description(&self) -> &'static str {
match self {
- Permission::Impersonate => "Allows acting on behalf of another user",
- Permission::UnlimitedRequests => "Removes request limits or quotas",
- Permission::UnlimitedUploads => "Removes upload size or frequency limits",
- Permission::DeleteSystemFolders => "Allows deletion of critical system folders",
+ Permission::Impersonate => "Act on behalf of another user",
+ Permission::UnlimitedRequests => "Perform unlimited requests",
+ Permission::UnlimitedUploads => "Upload unlimited data",
+ Permission::DeleteSystemFolders => "Delete of system folders",
Permission::MessageQueueList => "View message queue",
Permission::MessageQueueGet => "Retrieve specific messages from the queue",
Permission::MessageQueueUpdate => "Modify queued messages",
Permission::MessageQueueDelete => "Remove messages from the queue",
- Permission::OutgoingReportList => "View reports for outgoing emails",
- Permission::OutgoingReportGet => "Retrieve specific outgoing email reports",
- Permission::OutgoingReportDelete => "Remove outgoing email reports",
- Permission::IncomingReportList => "View reports for incoming emails",
- Permission::IncomingReportGet => "Retrieve specific incoming email reports",
- Permission::IncomingReportDelete => "Remove incoming email reports",
+ Permission::OutgoingReportList => "View outgoing DMARC and TLS reports",
+ Permission::OutgoingReportGet => "Retrieve specific outgoing DMARC and TLS reports",
+ Permission::OutgoingReportDelete => "Remove outgoing DMARC and TLS reports",
+ Permission::IncomingReportList => "View incoming DMARC, TLS and ARF reports",
+ Permission::IncomingReportGet => {
+ "Retrieve specific incoming DMARC, TLS and ARF reports"
+ }
+ Permission::IncomingReportDelete => "Remove incoming DMARC, TLS and ARF reports",
Permission::SettingsList => "View system settings",
Permission::SettingsUpdate => "Modify system settings",
Permission::SettingsDelete => "Remove system settings",
Permission::SettingsReload => "Refresh system settings",
- Permission::IndividualList => "View list of individual users",
- Permission::IndividualGet => "Retrieve specific user information",
- Permission::IndividualUpdate => "Modify user information",
+ Permission::IndividualList => "View list of user accounts",
+ Permission::IndividualGet => "Retrieve specific account information",
+ Permission::IndividualUpdate => "Modify user account information",
Permission::IndividualDelete => "Remove user accounts",
Permission::IndividualCreate => "Add new user accounts",
Permission::GroupList => "View list of user groups",
@@ -48,7 +50,7 @@ impl Permission {
Permission::DomainCreate => "Add new email domains",
Permission::DomainUpdate => "Modify domain information",
Permission::DomainDelete => "Remove email domains",
- Permission::TenantList => "View list of tenants (in multi-tenant setup)",
+ Permission::TenantList => "View list of tenants",
Permission::TenantGet => "Retrieve specific tenant information",
Permission::TenantCreate => "Add new tenants",
Permission::TenantUpdate => "Modify tenant information",
@@ -63,16 +65,16 @@ impl Permission {
Permission::RoleCreate => "Create new roles",
Permission::RoleUpdate => "Modify role information",
Permission::RoleDelete => "Remove roles",
- Permission::PrincipalList => "View list of principals (users or system entities)",
+ Permission::PrincipalList => "View list of principals",
Permission::PrincipalGet => "Retrieve specific principal information",
Permission::PrincipalCreate => "Create new principals",
Permission::PrincipalUpdate => "Modify principal information",
Permission::PrincipalDelete => "Remove principals",
- Permission::BlobFetch => "Retrieve binary large objects",
- Permission::PurgeBlobStore => "Clear the blob storage",
- Permission::PurgeDataStore => "Clear the data storage",
- Permission::PurgeLookupStore => "Clear the lookup storage",
- Permission::PurgeAccount => "Completely remove an account and all associated data",
+ Permission::BlobFetch => "Retrieve arbitrary blobs",
+ Permission::PurgeBlobStore => "Purge the blob storage",
+ Permission::PurgeDataStore => "Purge the data storage",
+ Permission::PurgeLookupStore => "Purge the lookup storage",
+ Permission::PurgeAccount => "Purge user accounts",
Permission::FtsReindex => "Rebuild the full-text search index",
Permission::Undelete => "Restore deleted items",
Permission::DkimSignatureCreate => "Create DKIM signatures for email authentication",
@@ -80,19 +82,19 @@ impl Permission {
Permission::UpdateSpamFilter => "Modify spam filter settings",
Permission::UpdateWebadmin => "Modify web admin interface settings",
Permission::LogsView => "Access system logs",
- Permission::SieveRun => "Execute Sieve scripts for email filtering",
+ Permission::SieveRun => "Execute Sieve scripts from the REST API",
Permission::Restart => "Restart the email server",
- Permission::TracingList => "View list of system traces",
+ Permission::TracingList => "View stored traces",
Permission::TracingGet => "Retrieve specific trace information",
- Permission::TracingLive => "View real-time system traces",
- Permission::MetricsList => "View list of system metrics",
- Permission::MetricsLive => "View real-time system metrics",
- Permission::Authenticate => "Perform authentication",
- Permission::AuthenticateOauth => "Perform OAuth authentication",
+ Permission::TracingLive => "Perform real-time tracing",
+ Permission::MetricsList => "View stored metrics",
+ Permission::MetricsLive => "View real-time metrics",
+ Permission::Authenticate => "Authenticate",
+ Permission::AuthenticateOauth => "Authenticate via OAuth",
Permission::EmailSend => "Send emails",
Permission::EmailReceive => "Receive emails",
- Permission::ManageEncryption => "Handle encryption settings and operations",
- Permission::ManagePasswords => "Manage user passwords",
+ Permission::ManageEncryption => "Manage encryption-at-rest settings",
+ Permission::ManagePasswords => "Manage account passwords",
Permission::JmapEmailGet => "Retrieve emails via JMAP",
Permission::JmapMailboxGet => "Retrieve mailboxes via JMAP",
Permission::JmapThreadGet => "Retrieve email threads via JMAP",
@@ -223,6 +225,7 @@ mod test {
.then_some(CHECK)
.unwrap_or_default()
);
+ //println!("({:?},{:?}),", permission.name(), permission.description(),);
}
}
}
diff --git a/crates/directory/src/core/principal.rs b/crates/directory/src/core/principal.rs
index 4eba98e9..ecd8350b 100644
--- a/crates/directory/src/core/principal.rs
+++ b/crates/directory/src/core/principal.rs
@@ -591,8 +591,8 @@ impl Type {
Self::Tenant => "tenant",
Self::Role => "role",
Self::Domain => "domain",
- Self::ApiKey => "api-key",
- Self::OauthClient => "oauth-client",
+ Self::ApiKey => "apiKey",
+ Self::OauthClient => "oauthClient",
}
}
@@ -623,8 +623,8 @@ impl Type {
"superuser" => Some(Type::Individual), // legacy
"role" => Some(Type::Role),
"domain" => Some(Type::Domain),
- "api-key" => Some(Type::ApiKey),
- "oauth-client" => Some(Type::OauthClient),
+ "apiKey" => Some(Type::ApiKey),
+ "oauthClient" => Some(Type::OauthClient),
_ => None,
}
}
@@ -1141,6 +1141,11 @@ impl Permission {
| Permission::JmapPrincipalGet
| Permission::JmapPrincipalQueryChanges
| Permission::JmapPrincipalQuery
+ | Permission::ApiKeyList
+ | Permission::ApiKeyGet
+ | Permission::ApiKeyCreate
+ | Permission::ApiKeyUpdate
+ | Permission::ApiKeyDelete
) || self.is_user_permission()
}
diff --git a/crates/jmap/src/auth/oauth/auth.rs b/crates/jmap/src/auth/oauth/auth.rs
index a669b60b..eba15ccc 100644
--- a/crates/jmap/src/auth/oauth/auth.rs
+++ b/crates/jmap/src/auth/oauth/auth.rs
@@ -292,7 +292,14 @@ impl OAuthApiHandler for Server {
"code token".to_string(),
"id_token token".to_string(),
],
- scopes_supported: vec!["openid".to_string(), "offline_access".to_string()],
+ scopes_supported: vec![
+ "openid".to_string(),
+ "offline_access".to_string(),
+ "urn:ietf:params:jmap:core".to_string(),
+ "urn:ietf:params:jmap:mail".to_string(),
+ "urn:ietf:params:jmap:submission".to_string(),
+ "urn:ietf:params:jmap:vacationresponse".to_string(),
+ ],
issuer: base_url,
})
.into_http_response())
diff --git a/tests/src/jmap/enterprise.rs b/tests/src/jmap/enterprise.rs
index d81eccd8..a04b73a3 100644
--- a/tests/src/jmap/enterprise.rs
+++ b/tests/src/jmap/enterprise.rs
@@ -86,7 +86,7 @@ pub async fn test(params: &mut JMAPTest) {
license: LicenseKey {
valid_to: now() + 3600,
valid_from: now() - 3600,
- hostname: String::new(),
+ domain: String::new(),
accounts: 100,
},
undelete: Undelete {
@@ -162,7 +162,7 @@ impl EnterpriseCore for Core {
license: LicenseKey {
valid_to: now() + 3600,
valid_from: now() - 3600,
- hostname: String::new(),
+ domain: String::new(),
accounts: 100,
},
undelete: None,
diff --git a/tests/src/jmap/mod.rs b/tests/src/jmap/mod.rs
index fe0302ef..58f4ba52 100644
--- a/tests/src/jmap/mod.rs
+++ b/tests/src/jmap/mod.rs
@@ -291,7 +291,7 @@ refresh-token-renew = "2s"
[oauth.client-registration]
anonymous = true
-required = true
+require = true
[oauth.oidc]
signature-key = '''-----BEGIN PRIVATE KEY-----