use std::path::{Path, PathBuf};
use std::sync::Arc;
use rand::RngExt as _;
use crate::infra::cluster::security::{JoinToken, token_fingerprint};
use crate::platform::adapters::data::DataAdapter;
use crate::platform::error::PlatformError;
use crate::platform::model::{
LOCAL_AUTHORITY_EVENT_BREAK_GLASS, LOCAL_AUTHORITY_EVENT_DETACH,
PlatformOfficeLocalAuthorityEvent, now_ts,
};
use super::join_token::office_token_path;
pub const LOCAL_LOGIN_DISABLED_CODE: &str = "OFFICE_LOCAL_LOGIN_DISABLED";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OfficeJoinFile {
Absent,
Held {
office_id: String,
fingerprint: String,
},
Unreadable {
reason: String,
},
}
impl OfficeJoinFile {
pub fn is_joined(&self) -> bool {
!matches!(self, Self::Absent)
}
pub fn office_id(&self) -> &str {
match self {
Self::Held { office_id, .. } => office_id,
_ => "",
}
}
pub fn fingerprint(&self) -> &str {
match self {
Self::Held { fingerprint, .. } => fingerprint,
_ => "",
}
}
}
#[derive(Clone)]
pub struct OfficeLocalAuthorityService {
data: Arc<dyn DataAdapter>,
data_root: PathBuf,
}
impl OfficeLocalAuthorityService {
pub fn new(data: Arc<dyn DataAdapter>, data_root: PathBuf) -> Self {
Self { data, data_root }
}
pub fn join_file(&self) -> OfficeJoinFile {
read_join_file(&self.data_root)
}
pub fn break_glass_in_force(
&self,
) -> Result<Option<PlatformOfficeLocalAuthorityEvent>, PlatformError> {
let join = self.join_file();
let fingerprint = join.fingerprint();
if fingerprint.is_empty() {
return Ok(None);
}
self.data.find_office_break_glass(fingerprint)
}
pub fn local_login_allowed(&self) -> Result<bool, PlatformError> {
if !self.join_file().is_joined() {
return Ok(true);
}
Ok(self.break_glass_in_force()?.is_some())
}
pub fn refusal(&self) -> PlatformError {
let join = self.join_file();
let office = match &join {
OfficeJoinFile::Held { office_id, .. } => format!(" as office '{office_id}'"),
_ => String::new(),
};
let damaged = match &join {
OfficeJoinFile::Unreadable { reason } => format!(
" This office's token file at '{}' is unreadable ({reason}); it is treated as \
joined rather than opened.",
office_token_path(&self.data_root).display()
),
_ => String::new(),
};
PlatformError::new(
LOCAL_LOGIN_DISABLED_CODE,
format!(
"local password login is disabled on this instance. It has joined a controller\
{office}, and while it is joined the controller's identity is the way in: open \
this office from the controller's directory, or ask it for a vouch.{damaged} \
Local authority can be re-enabled from this host, with no controller and no \
quorum, by stopping this server and running `zeb admin break-glass`; \
`zeb admin detach` leaves the controller for good and keeps every project, its \
data, its files, and its shelf."
),
)
}
pub fn break_glass(
&self,
owner: &str,
detail: &str,
) -> Result<PlatformOfficeLocalAuthorityEvent, PlatformError> {
let join = self.join_file();
if !join.is_joined() {
return Err(PlatformError::new(
"OFFICE_NOT_JOINED",
format!(
"this instance holds no join token at '{}', so its local authority was never \
disabled and there is nothing to break out of. Local login already works.",
office_token_path(&self.data_root).display()
),
));
}
let OfficeJoinFile::Held {
office_id,
fingerprint,
} = join
else {
return Err(PlatformError::new(
"OFFICE_JOIN_TOKEN_UNREADABLE",
format!(
"the join token at '{}' cannot be read, so this office cannot say which \
membership is being broken out of. Repair or remove the file — \
`zeb admin detach` removes it and leaves the controller.",
office_token_path(&self.data_root).display()
),
));
};
let entry = PlatformOfficeLocalAuthorityEvent {
event_id: random_event_id(),
office_id,
event: LOCAL_AUTHORITY_EVENT_BREAK_GLASS.to_string(),
join_fingerprint: fingerprint,
owner: owner.trim().to_string(),
detail: detail.trim().to_string(),
acted_at: now_ts(),
reported_at: 0,
};
self.data.put_office_local_authority_event(&entry)?;
Ok(entry)
}
pub fn detach(&self, detail: &str) -> Result<PlatformOfficeLocalAuthorityEvent, PlatformError> {
let join = self.join_file();
if !join.is_joined() {
return Err(PlatformError::new(
"OFFICE_NOT_JOINED",
format!(
"this instance holds no join token at '{}'. It has joined nobody, so there \
is nothing to leave.",
office_token_path(&self.data_root).display()
),
));
}
let entry = PlatformOfficeLocalAuthorityEvent {
event_id: random_event_id(),
office_id: join.office_id().to_string(),
event: LOCAL_AUTHORITY_EVENT_DETACH.to_string(),
join_fingerprint: join.fingerprint().to_string(),
owner: String::new(),
detail: detail.trim().to_string(),
acted_at: now_ts(),
reported_at: 0,
};
self.data.put_office_local_authority_event(&entry)?;
let path = office_token_path(&self.data_root);
std::fs::remove_file(&path).map_err(|err| {
PlatformError::new(
"OFFICE_DETACH_FAILED",
format!("failed removing '{}': {err}", path.display()),
)
})?;
Ok(entry)
}
pub fn list(
&self,
limit: usize,
) -> Result<Vec<PlatformOfficeLocalAuthorityEvent>, PlatformError> {
self.data
.list_office_local_authority_events(limit.clamp(1, 500))
}
pub fn unreported_break_glass(
&self,
) -> Result<Vec<PlatformOfficeLocalAuthorityEvent>, PlatformError> {
self.data.list_unreported_office_break_glass()
}
pub fn mark_reported(&self, event_id: &str, reported_at: i64) -> Result<(), PlatformError> {
self.data
.mark_office_local_authority_reported(event_id, reported_at)
}
pub fn accept_report(
&self,
entry: &PlatformOfficeLocalAuthorityEvent,
office_id: &str,
received_at: i64,
) -> Result<PlatformOfficeLocalAuthorityEvent, PlatformError> {
let stored = PlatformOfficeLocalAuthorityEvent {
event_id: entry.event_id.trim().to_string(),
office_id: office_id.to_string(),
event: LOCAL_AUTHORITY_EVENT_BREAK_GLASS.to_string(),
join_fingerprint: entry.join_fingerprint.trim().to_string(),
owner: entry.owner.trim().to_string(),
detail: entry.detail.trim().to_string(),
acted_at: entry.acted_at,
reported_at: received_at,
};
if stored.event_id.is_empty() {
return Err(PlatformError::new(
"OFFICE_BREAK_GLASS_REPORT_INVALID",
"a reported break-glass must carry the event id the office recorded",
));
}
self.data.put_office_local_authority_event(&stored)?;
Ok(stored)
}
}
pub fn join_fingerprint(secret_digest: &str) -> String {
token_fingerprint(secret_digest)
}
pub fn read_join_file(data_root: &Path) -> OfficeJoinFile {
let path = office_token_path(data_root);
let raw = match std::fs::read_to_string(&path) {
Ok(value) => value,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return OfficeJoinFile::Absent,
Err(err) => {
return OfficeJoinFile::Unreadable {
reason: err.to_string(),
};
}
};
if raw.trim().is_empty() {
return OfficeJoinFile::Absent;
}
match JoinToken::parse(&raw) {
Ok(token) => OfficeJoinFile::Held {
office_id: token.office_id.clone(),
fingerprint: join_fingerprint(&token.secret_digest()),
},
Err(err) => OfficeJoinFile::Unreadable {
reason: err.to_string(),
},
}
}
fn random_event_id() -> String {
let mut bytes = [0u8; 16];
rand::rng().fill(&mut bytes);
hex::encode(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::platform::adapters::data::build_data_adapter;
use crate::platform::model::DataAdapterKind;
fn temp_root(name: &str) -> PathBuf {
let root = std::env::temp_dir().join(format!(
"zebflow-local-authority-{name}-{}-{}",
std::process::id(),
now_ts()
));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).expect("temp root");
root
}
fn service(root: &Path) -> OfficeLocalAuthorityService {
let data = build_data_adapter(DataAdapterKind::Sqlite, root).expect("adapter");
OfficeLocalAuthorityService::new(data, root.to_path_buf())
}
fn minted_token(office_id: &str) -> JoinToken {
let (_, key) =
crate::infra::cluster::security::ControllerSigningKey::generate().expect("key");
JoinToken::mint(office_id, key.verify_key())
}
fn write_token(root: &Path, token: &JoinToken) {
let path = office_token_path(root);
std::fs::create_dir_all(path.parent().expect("parent")).expect("parent");
std::fs::write(&path, format!("{}\n", token.render())).expect("write token");
}
#[test]
fn a_standalone_instance_allows_local_login_and_has_nothing_to_break_out_of() {
let root = temp_root("standalone");
let svc = service(&root);
assert_eq!(svc.join_file(), OfficeJoinFile::Absent);
assert!(svc.local_login_allowed().expect("allowed"));
let err = svc
.break_glass("", "t")
.expect_err("nothing to break out of");
assert_eq!(err.code, "OFFICE_NOT_JOINED");
let err = svc.detach("t").expect_err("nothing to leave");
assert_eq!(err.code, "OFFICE_NOT_JOINED");
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn a_joined_office_refuses_local_login_until_break_glass_and_the_refusal_is_actionable() {
let root = temp_root("joined");
let token = minted_token("office-a");
write_token(&root, &token);
let svc = service(&root);
assert!(!svc.local_login_allowed().expect("disabled while joined"));
let refusal = svc.refusal();
assert_eq!(refusal.code, LOCAL_LOGIN_DISABLED_CODE);
assert!(
refusal.message.contains("joined a controller"),
"{refusal:?}"
);
assert!(refusal.message.contains("office-a"), "{refusal:?}");
assert!(refusal.message.contains("vouch"), "{refusal:?}");
assert!(
refusal.message.contains("zeb admin break-glass"),
"{refusal:?}"
);
for leak in ["password is", "no such", "unknown user", "incorrect"] {
assert!(!refusal.message.contains(leak), "{leak} in {refusal:?}");
}
svc.break_glass("", "test").expect("break glass");
assert!(
svc.local_login_allowed()
.expect("allowed after break glass")
);
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn break_glass_does_not_detach_and_detach_is_a_separate_act() {
let root = temp_root("break-glass-keeps-membership");
let token = minted_token("office-a");
write_token(&root, &token);
let svc = service(&root);
svc.break_glass("", "test").expect("break glass");
assert!(office_token_path(&root).is_file());
assert!(svc.join_file().is_joined());
svc.detach("test").expect("detach");
assert!(!office_token_path(&root).is_file());
assert!(!svc.join_file().is_joined());
assert!(svc.local_login_allowed().expect("local login live"));
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn a_break_glass_does_not_survive_into_a_later_join() {
let root = temp_root("refingerprint");
write_token(&root, &minted_token("office-a"));
let svc = service(&root);
svc.break_glass("", "first").expect("break glass");
assert!(svc.local_login_allowed().expect("allowed"));
write_token(&root, &minted_token("office-a"));
assert!(
!svc.local_login_allowed().expect("disabled again"),
"a re-issued token must not inherit an earlier break-glass"
);
assert!(svc.break_glass_in_force().expect("query").is_none());
assert_eq!(svc.list(50).expect("log").len(), 1);
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn a_break_glass_stays_in_force_however_long_the_log_grows() {
let root = temp_root("deep-log");
let token = minted_token("office-a");
write_token(&root, &token);
let svc = service(&root);
svc.break_glass("", "the act that opened the door")
.expect("break glass");
assert!(svc.local_login_allowed().expect("open"));
let data = svc.data.clone();
for index in 0..500 {
data.put_office_local_authority_event(&PlatformOfficeLocalAuthorityEvent {
event_id: format!("later-{index}"),
office_id: "office-a".to_string(),
event: LOCAL_AUTHORITY_EVENT_DETACH.to_string(),
join_fingerprint: format!("other-fingerprint-{index}"),
owner: String::new(),
detail: "history".to_string(),
acted_at: now_ts() + 1 + index,
reported_at: 0,
})
.expect("later row");
}
assert!(
svc.break_glass_in_force().expect("query").is_some(),
"the row that opened the door must not scroll out of a window"
);
assert!(
svc.local_login_allowed().expect("still open"),
"an office must not re-lock itself because its log got long"
);
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn an_unreported_break_glass_survives_a_long_log() {
let root = temp_root("deep-log-reporting");
write_token(&root, &minted_token("office-a"));
let svc = service(&root);
let event = svc
.break_glass("", "the act to report")
.expect("break glass");
let data = svc.data.clone();
for index in 0..500 {
data.put_office_local_authority_event(&PlatformOfficeLocalAuthorityEvent {
event_id: format!("later-{index}"),
office_id: "office-a".to_string(),
event: LOCAL_AUTHORITY_EVENT_DETACH.to_string(),
join_fingerprint: format!("other-fingerprint-{index}"),
owner: String::new(),
detail: "history".to_string(),
acted_at: now_ts() + 1 + index,
reported_at: 0,
})
.expect("later row");
}
let unreported = svc.unreported_break_glass().expect("unreported");
assert_eq!(
unreported.len(),
1,
"an office offline for a long time still reports on its first reconnect"
);
assert_eq!(unreported[0].event_id, event.event_id);
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn a_damaged_token_file_fails_closed() {
let root = temp_root("damaged");
let path = office_token_path(&root);
std::fs::create_dir_all(path.parent().expect("parent")).expect("parent");
std::fs::write(&path, "not-a-token\n").expect("write");
let svc = service(&root);
assert!(matches!(svc.join_file(), OfficeJoinFile::Unreadable { .. }));
assert!(
!svc.local_login_allowed().expect("closed"),
"a damaged membership record must not open local login"
);
assert!(svc.refusal().message.contains("unreadable"));
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn only_a_break_glass_is_ever_reported() {
let root = temp_root("reporting");
write_token(&root, &minted_token("office-a"));
let svc = service(&root);
let event = svc.break_glass("", "test").expect("break glass");
assert_eq!(svc.unreported_break_glass().expect("unreported").len(), 1);
svc.mark_reported(&event.event_id, 4242).expect("mark");
assert!(svc.unreported_break_glass().expect("unreported").is_empty());
svc.detach("test").expect("detach");
assert!(
svc.unreported_break_glass().expect("unreported").is_empty(),
"a detach gives up the credential that would report it"
);
let _ = std::fs::remove_dir_all(root);
}
}