Files
inbuxa-server/crates/common/src/manager/mod.rs
T
jcoffey-dev 63adb4e2b8
ci / fork-checks (pull_request) Successful in 31s
ci / build (pull_request) Successful in 11m31s
Add the compliance permission and the Compliance Officer roles
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.
2026-09-28 08:50:17 -07:00

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]"
})
}