Keep rotated log files for a set number of days
Personal-data catalog spec, default D1 (settled 2026-09-28): log files were never deleted. inbuxa:LogSettings.keepForDays says how many days rotated log files are kept; unset (null) keeps every file, as before, and a new install sets 30 days. It is a fork-owned setting, stored under T + l as audit retention is, not a field on x:TracerLog: that object is also stored inside x:Bootstrap with a field after it, so a new field would change x:Bootstrap's stored format. Server-level, with the tracers' permissions (sysTracerGet, sysTracerUpdate); changes are in the audit log, before and after. Log files are local, so every node deletes its own: hourly, and at once when the setting changes on that node. Only regular files named <prefix>.<something> in each enabled log tracer's directory, last changed more than the limit ago, are removed; the file being written is never that old, and nothing else in the directory is touched. Minimum one day. The catalog classifies inbuxa:LogSettings and points the log file's retention at it. Tested: unit tests for the file rule (only this log's old files; the current file, other files and directories stay) and a purge on disk; the system suite, which reads, sets, refuses zero, restores null and checks the audit records; fork checks.
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Coffey Labs
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
//! `inbuxa:LogSettings/get` and `/set` under `urn:inbuxa:jmap`: how long
|
||||
//! rotated log files are kept (personal-data catalog spec, D1). A singleton,
|
||||
//! id `singleton`.
|
||||
|
||||
use crate::object::{AnyId, JmapObject, JmapObjectId};
|
||||
use jmap_tools::{Element, Key, Property};
|
||||
use std::{borrow::Cow, str::FromStr};
|
||||
use types::id::Id;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct LogSettings;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum LogSettingsProperty {
|
||||
Id,
|
||||
KeepForDays,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum LogSettingsValue {
|
||||
Id(Id),
|
||||
}
|
||||
|
||||
impl Property for LogSettingsProperty {
|
||||
fn try_parse(_: Option<&Key<'_, Self>>, value: &str) -> Option<Self> {
|
||||
LogSettingsProperty::parse(value)
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
LogSettingsProperty::Id => "id",
|
||||
LogSettingsProperty::KeepForDays => "keepForDays",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl LogSettingsProperty {
|
||||
fn parse(value: &str) -> Option<Self> {
|
||||
hashify::tiny_map!(value.as_bytes(),
|
||||
b"id" => LogSettingsProperty::Id,
|
||||
b"keepForDays" => LogSettingsProperty::KeepForDays,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for LogSettingsProperty {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
LogSettingsProperty::parse(s).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for LogSettingsValue {
|
||||
type Property = LogSettingsProperty;
|
||||
|
||||
fn try_parse<P>(key: &Key<'_, Self::Property>, value: &str) -> Option<Self> {
|
||||
match key {
|
||||
Key::Property(LogSettingsProperty::Id) => {
|
||||
Id::from_str(value).ok().map(LogSettingsValue::Id)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
LogSettingsValue::Id(id) => id.to_string().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObject for LogSettings {
|
||||
type Property = LogSettingsProperty;
|
||||
|
||||
type Element = LogSettingsValue;
|
||||
|
||||
type Id = Id;
|
||||
|
||||
type Filter = ();
|
||||
|
||||
type Comparator = ();
|
||||
|
||||
type GetArguments = ();
|
||||
|
||||
type SetArguments<'de> = ();
|
||||
|
||||
type QueryArguments = ();
|
||||
|
||||
type CopyArguments = ();
|
||||
|
||||
type ParseArguments = ();
|
||||
|
||||
const ID_PROPERTY: Self::Property = LogSettingsProperty::Id;
|
||||
}
|
||||
|
||||
impl From<Id> for LogSettingsValue {
|
||||
fn from(id: Id) -> Self {
|
||||
LogSettingsValue::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for LogSettingsValue {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
match self {
|
||||
LogSettingsValue::Id(id) => Some(*id),
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
match self {
|
||||
LogSettingsValue::Id(id) => Some(AnyId::Id(*id)),
|
||||
}
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, new_id: AnyId) -> bool {
|
||||
if let AnyId::Id(id) = new_id {
|
||||
*self = LogSettingsValue::Id(id);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for LogSettingsProperty {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, _: AnyId) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ pub mod email_submission;
|
||||
pub mod fastmail_masked_email; // inbuxa: masked email
|
||||
pub mod inbuxa_account_lock; // inbuxa: account lock with delegation
|
||||
pub mod inbuxa_ai_limits; // inbuxa: AI spam classification
|
||||
pub mod inbuxa_log_settings; // inbuxa: personal-data catalog, D1
|
||||
pub mod inbuxa_audit; // inbuxa: the audit log
|
||||
pub mod inbuxa_legal_hold; // inbuxa: legal hold
|
||||
pub mod inbuxa_hold_export; // inbuxa: legal hold exports
|
||||
|
||||
@@ -61,6 +61,9 @@ impl Response<'_> {
|
||||
GetResponseMethod::AiLimits(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
GetResponseMethod::LogSettings(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
GetResponseMethod::AuditEvent(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ impl Response<'_> {
|
||||
GetRequestMethod::MaskedEmail(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::DeletedAccount(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::AiLimits(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::LogSettings(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::AuditEvent(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::AuditSettings(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::AccountLock(request) => request.resolve_references(self)?,
|
||||
@@ -98,6 +99,9 @@ impl Response<'_> {
|
||||
SetRequestMethod::AiLimits(request) => {
|
||||
request.resolve_references(self, 1, false)?
|
||||
}
|
||||
SetRequestMethod::LogSettings(request) => {
|
||||
request.resolve_references(self, 1, false)?
|
||||
}
|
||||
SetRequestMethod::Explanation(request) => {
|
||||
request.resolve_references(self, 1, false)?
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ pub enum MethodObject {
|
||||
DeletedAccount,
|
||||
// inbuxa: AI call limits
|
||||
AiLimits,
|
||||
LogSettings,
|
||||
// inbuxa: "Explain this" with the local model
|
||||
Explanation,
|
||||
// inbuxa: the audit log
|
||||
@@ -89,6 +90,7 @@ impl MethodObject {
|
||||
MethodObject::MaskedEmail => Capability::FastmailMaskedEmail,
|
||||
MethodObject::DeletedAccount => Capability::Inbuxa,
|
||||
MethodObject::AiLimits => Capability::Inbuxa,
|
||||
MethodObject::LogSettings => Capability::Inbuxa,
|
||||
MethodObject::Explanation => Capability::Inbuxa,
|
||||
MethodObject::AuditEvent
|
||||
| MethodObject::AuditSettings
|
||||
@@ -276,6 +278,8 @@ impl MethodName {
|
||||
(MethodFunction::Set, MethodObject::DeletedAccount) => "inbuxa:DeletedAccount/set",
|
||||
(MethodFunction::Get, MethodObject::AiLimits) => "inbuxa:AiLimits/get",
|
||||
(MethodFunction::Set, MethodObject::AiLimits) => "inbuxa:AiLimits/set",
|
||||
(MethodFunction::Get, MethodObject::LogSettings) => "inbuxa:LogSettings/get",
|
||||
(MethodFunction::Set, MethodObject::LogSettings) => "inbuxa:LogSettings/set",
|
||||
(MethodFunction::Set, MethodObject::Explanation) => "inbuxa:Explanation/set",
|
||||
(MethodFunction::Get, MethodObject::AuditEvent) => "inbuxa:AuditEvent/get",
|
||||
(MethodFunction::Query, MethodObject::AuditEvent) => "inbuxa:AuditEvent/query",
|
||||
@@ -424,6 +428,8 @@ impl MethodName {
|
||||
"inbuxa:DeletedAccount/set" => (MethodObject::DeletedAccount, MethodFunction::Set),
|
||||
"inbuxa:AiLimits/get" => (MethodObject::AiLimits, MethodFunction::Get),
|
||||
"inbuxa:AiLimits/set" => (MethodObject::AiLimits, MethodFunction::Set),
|
||||
"inbuxa:LogSettings/get" => (MethodObject::LogSettings, MethodFunction::Get),
|
||||
"inbuxa:LogSettings/set" => (MethodObject::LogSettings, MethodFunction::Set),
|
||||
"inbuxa:Explanation/set" => (MethodObject::Explanation, MethodFunction::Set),
|
||||
"inbuxa:AuditEvent/get" => (MethodObject::AuditEvent, MethodFunction::Get),
|
||||
"inbuxa:AuditEvent/query" => (MethodObject::AuditEvent, MethodFunction::Query),
|
||||
@@ -494,6 +500,7 @@ impl Display for MethodObject {
|
||||
MethodObject::MaskedEmail => "MaskedEmail",
|
||||
MethodObject::DeletedAccount => "inbuxa:DeletedAccount",
|
||||
MethodObject::AiLimits => "inbuxa:AiLimits",
|
||||
MethodObject::LogSettings => "inbuxa:LogSettings",
|
||||
MethodObject::Explanation => "inbuxa:Explanation",
|
||||
MethodObject::AuditEvent => "inbuxa:AuditEvent",
|
||||
MethodObject::AuditSettings => "inbuxa:AuditSettings",
|
||||
|
||||
@@ -116,6 +116,7 @@ pub enum GetRequestMethod {
|
||||
MaskedEmail(Box<GetRequest<crate::object::fastmail_masked_email::FastmailMaskedEmail>>),
|
||||
DeletedAccount(Box<GetRequest<crate::object::inbuxa_deleted_account::DeletedAccount>>),
|
||||
AiLimits(Box<GetRequest<crate::object::inbuxa_ai_limits::AiLimits>>),
|
||||
LogSettings(Box<GetRequest<crate::object::inbuxa_log_settings::LogSettings>>),
|
||||
AuditEvent(Box<GetRequest<crate::object::inbuxa_audit::AuditEvent>>),
|
||||
AuditSettings(Box<GetRequest<crate::object::inbuxa_audit::AuditSettings>>),
|
||||
AccountLock(Box<GetRequest<crate::object::inbuxa_account_lock::AccountLock>>),
|
||||
@@ -148,6 +149,7 @@ pub enum SetRequestMethod<'x> {
|
||||
MaskedEmail(Box<SetRequest<'x, crate::object::fastmail_masked_email::FastmailMaskedEmail>>),
|
||||
DeletedAccount(Box<SetRequest<'x, crate::object::inbuxa_deleted_account::DeletedAccount>>),
|
||||
AiLimits(Box<SetRequest<'x, crate::object::inbuxa_ai_limits::AiLimits>>),
|
||||
LogSettings(Box<SetRequest<'x, crate::object::inbuxa_log_settings::LogSettings>>),
|
||||
Explanation(Box<SetRequest<'x, crate::object::inbuxa_explanation::Explanation>>),
|
||||
AuditSettings(Box<SetRequest<'x, crate::object::inbuxa_audit::AuditSettings>>),
|
||||
AuditExport(Box<SetRequest<'x, crate::object::inbuxa_audit::AuditExport>>),
|
||||
|
||||
@@ -169,6 +169,13 @@ impl<'de> Visitor<'de> for CallVisitor {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Get, MethodObject::LogSettings) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::LogSettings(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Get, MethodObject::ProtocolPolicy) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::ProtocolPolicy(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
@@ -350,6 +357,13 @@ impl<'de> Visitor<'de> for CallVisitor {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Set, MethodObject::LogSettings) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::LogSettings(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Set, MethodObject::Explanation) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::Explanation(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
|
||||
@@ -103,6 +103,7 @@ pub enum GetResponseMethod {
|
||||
MaskedEmail(GetResponse<crate::object::fastmail_masked_email::FastmailMaskedEmail>),
|
||||
DeletedAccount(GetResponse<crate::object::inbuxa_deleted_account::DeletedAccount>),
|
||||
AiLimits(GetResponse<crate::object::inbuxa_ai_limits::AiLimits>),
|
||||
LogSettings(GetResponse<crate::object::inbuxa_log_settings::LogSettings>),
|
||||
AuditEvent(GetResponse<crate::object::inbuxa_audit::AuditEvent>),
|
||||
AuditSettings(GetResponse<crate::object::inbuxa_audit::AuditSettings>),
|
||||
AccountLock(GetResponse<crate::object::inbuxa_account_lock::AccountLock>),
|
||||
@@ -136,6 +137,7 @@ pub enum SetResponseMethod {
|
||||
MaskedEmail(Box<SetResponse<crate::object::fastmail_masked_email::FastmailMaskedEmail>>),
|
||||
DeletedAccount(Box<SetResponse<crate::object::inbuxa_deleted_account::DeletedAccount>>),
|
||||
AiLimits(Box<SetResponse<crate::object::inbuxa_ai_limits::AiLimits>>),
|
||||
LogSettings(Box<SetResponse<crate::object::inbuxa_log_settings::LogSettings>>),
|
||||
AuditSettings(Box<SetResponse<crate::object::inbuxa_audit::AuditSettings>>),
|
||||
AuditExport(Box<SetResponse<crate::object::inbuxa_audit::AuditExport>>),
|
||||
AuditVerification(Box<SetResponse<crate::object::inbuxa_audit::AuditVerification>>),
|
||||
@@ -349,12 +351,24 @@ impl<'x> From<GetResponse<crate::object::inbuxa_ai_limits::AiLimits>> for Respon
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<GetResponse<crate::object::inbuxa_log_settings::LogSettings>> for ResponseMethod<'x> {
|
||||
fn from(value: GetResponse<crate::object::inbuxa_log_settings::LogSettings>) -> Self {
|
||||
ResponseMethod::Get(GetResponseMethod::LogSettings(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<SetResponse<crate::object::inbuxa_ai_limits::AiLimits>> for ResponseMethod<'x> {
|
||||
fn from(value: SetResponse<crate::object::inbuxa_ai_limits::AiLimits>) -> Self {
|
||||
ResponseMethod::Set(SetResponseMethod::AiLimits(Box::new(value)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<SetResponse<crate::object::inbuxa_log_settings::LogSettings>> for ResponseMethod<'x> {
|
||||
fn from(value: SetResponse<crate::object::inbuxa_log_settings::LogSettings>) -> Self {
|
||||
ResponseMethod::Set(SetResponseMethod::LogSettings(Box::new(value)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<SetResponse<crate::object::inbuxa_explanation::Explanation>> for ResponseMethod<'x> {
|
||||
fn from(value: SetResponse<crate::object::inbuxa_explanation::Explanation>) -> Self {
|
||||
ResponseMethod::Set(SetResponseMethod::Explanation(Box::new(value)))
|
||||
|
||||
Reference in New Issue
Block a user