Personal-data catalog spec, §7 (settled 2026-09-28). sysComplianceGet (673) sees the data inventory and compliance overview: superusers and, for their tenant's slice, tenant administrators, by default and through the one-time grants on servers that already have their roles stored. A Compliance Officer role at server level holds it with reading and exporting the audit log, placing, widening, releasing and exporting legal holds, seeing account locks, and reading accounts, lists, domains, tenants and roles. It changes no server setting, creates or deletes no account, and can't shorten audit retention. A tenant's accounts can hold only roles of their own tenant (MT-3), so the tenant role is one "Compliance Officer" role per tenant, without holds (LH-13): made once for every tenant a server has, and whenever a tenant is created. While nobody holds it, it is removed with its tenant so it doesn't block the delete, and put back if the delete is refused for another reason. Both roles carry a user's own permissions too, since roles given to a person replace the default user role, which a tenant's accounts can't hold anyway. Every server makes these once, new or existing -- the built-in roles are only made on a server with none -- and records each under P c, so a role an administrator deletes stays deleted. Tested: unit tests (neither role changes a setting beyond a user's own; holds for the server's officer only; per-place records); a new compliance system test (one server-level role; an officer reads the audit log, places and releases a hold, and is refused a setting, an account and audit retention; a tenant gets its role, whose holder reads the tenant's audit log and no holds; a tenant with an unused role is deleted and the role goes with it); the system, audit, legal hold, account lock and SCIM suites; fork checks. The directory suite needs its LDAP container and wasn't run here.
89 lines
2.8 KiB
Rust
89 lines
2.8 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
|
*
|
|
* Modified by Coffey Labs in 2026 for INBUXA.
|
|
*/
|
|
|
|
use crate::USER_AGENT;
|
|
use hyper::HeaderMap;
|
|
use mail_auth::flate2;
|
|
use std::{
|
|
io::{BufReader, Read},
|
|
time::Duration,
|
|
};
|
|
use utils::HttpLimitResponse;
|
|
|
|
pub mod application;
|
|
pub mod backup;
|
|
pub mod boot;
|
|
pub mod compliance_roles; // inbuxa: personal-data catalog, the compliance roles
|
|
pub mod console;
|
|
pub mod defaults;
|
|
pub mod first_party;
|
|
pub mod granted_permissions; // inbuxa: permissions added after roles were stored
|
|
pub mod restore;
|
|
pub mod spam_rules; // inbuxa: rules bundled with the server
|
|
|
|
pub const SPAM_TRAINER_KEY: &[u8] = "INBUXA_SPAM_TRAIN_DATA.lz4".as_bytes();
|
|
pub const SPAM_CLASSIFIER_KEY: &[u8] = "INBUXA_SPAM_CLASSIFIER_MODEL.lz4".as_bytes();
|
|
|
|
pub async fn fetch_resource(
|
|
url: &str,
|
|
headers: Option<HeaderMap>,
|
|
timeout: Duration,
|
|
max_size: usize,
|
|
) -> Result<Vec<u8>, String> {
|
|
if let Some(path) = url.strip_prefix("file://") {
|
|
tokio::fs::read(path)
|
|
.await
|
|
.map_err(|err| format!("Failed to read {path}: {err}"))
|
|
} else {
|
|
let response = utils::http::http_client_builder(is_localhost_url(url))
|
|
.timeout(timeout)
|
|
.user_agent(USER_AGENT)
|
|
.build()
|
|
.unwrap_or_default()
|
|
.get(url)
|
|
.headers(headers.unwrap_or_default())
|
|
.send()
|
|
.await
|
|
.map_err(|err| format!("Failed to fetch {url}: {err}"))?;
|
|
|
|
if response.status().is_success() {
|
|
response
|
|
.bytes_with_limit(max_size)
|
|
.await
|
|
.map_err(|err| format!("Failed to fetch {url}: {err}"))
|
|
.and_then(|bytes| bytes.ok_or_else(|| format!("Resource too large: {url}")))
|
|
} else {
|
|
let code = response.status().canonical_reason().unwrap_or_default();
|
|
let reason = response.text().await.unwrap_or_default();
|
|
|
|
Err(format!(
|
|
"Failed to fetch {url}: Code: {code}, Details: {reason}",
|
|
))
|
|
}
|
|
}
|
|
.and_then(|bytes| {
|
|
if url.ends_with(".gz") || url.ends_with(".gzip") {
|
|
BufReader::new(flate2::read::GzDecoder::new(&bytes[..]))
|
|
.bytes()
|
|
.collect::<Result<Vec<u8>, _>>()
|
|
.map_err(|err| format!("Failed to decompress {url}: {err}"))
|
|
} else {
|
|
Ok(bytes)
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn is_localhost_url(url: &str) -> bool {
|
|
url.split_once("://")
|
|
.map(|(_, url)| url.split_once('/').map_or(url, |(host, _)| host))
|
|
.is_some_and(|host| {
|
|
let host = host.rsplit_once(':').map_or(host, |(host, _)| host);
|
|
host == "localhost" || host == "127.0.0.1" || host == "[::1]"
|
|
})
|
|
}
|